![[iOS] そのアプリ、メモリリークしてませんか?](https://devio2023-media.developers.io/wp-content/uploads/2015/12/ios.png)
[iOS] そのアプリ、メモリリークしてませんか?
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
こんにちは。モバイルアプリサービス部の加藤潤です。 今回はiOSアプリ開発において気をつけていないと発生しがちなメモリリークについて、よくある例を書いてみたいと思います。
開発環境
- Xcode 7.3.1 (7D1014)
- iPhone 6s シミュレータ / iOS 9.3 (13E230)
- Swift 2.2
お互いをプロパティで強参照し合うケース
これは典型的な循環参照が発生するパターンです。
まずはXcodeで新規プロジェクトを作成します。iOS > Application > Single View Application を選択します。
プロジェクトが作成できたらNew File... > Swift Fileを選択します。ファイル名はModelAとでもしておきましょう。
同様にModelBも作成しましょう。
ファイルが作成できたら以下のようにModelAとModelBという2つのクラスを作成します。
import Foundation
class ModelA {
var modelB: ModelB?
deinit {
print("ModelA deinit")
}
}
import Foundation
class ModelB {
var modelA: ModelA?
deinit {
print("ModelB deinit")
}
}
ModelAはModelBを、ModelBはModelAをそれぞれプロパティで保持しています。
deinitはオブジェクトが破棄される時に呼ばれるものですが、今回はオブジェクトが正しく破棄されているかを確認するためにprintしています。
それでは上記のクラスを使ってみましょう。
ViewController.swiftを以下のように変更します。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// ModelA, ModelBのインスタンスを生成
let a = ModelA()
let b = ModelB()
// プロパティにセット
a.modelB = b
b.modelA = a
}
}
変更したらアプリを実行してみましょう。
コンソールにdeinitのログは出ましたか?
出ませんか?
はい、これは出ないのが正しいんです。
理由は簡単で、ModelAとModelBがお互いを強参照しあっているためです。
ModelAが破棄されるタイミングでプロパティに保持したModelBが破棄されるModelBが破棄されるタイミングでプロパティに保持したModelAが破棄される
上記のようにお互い解放待ちの状態になってしまっています。メモリリークの完成です。
メモリリークしないようにするには
ではメモリリークを防ぐにはどうしたら良いでしょうか?
メモリリークの原因はModelAとModelBがお互いを強参照しあっていることにあるので、どちらか一方を弱参照にしてあげればメモリリークは発生しなくなります。
ここではModelBが保持しているModelAプロパティにweak修飾子をつけてみましょう。
import Foundation
class ModelB {
weak var modelA: ModelA?
deinit {
print("ModelB deinit")
}
}
この状態でアプリを実行してみます。
ちゃんとdeinitのログが出ました。オブジェクトが正しく破棄されていることの証明です。
メモリリークを防ぐことができました。
ModelA deinit ModelB deinit
プロパティに保持したクロージャにselfが強参照でキャプチャされているケース
続いてはクロージャを使用したメモリリークの例です。
まずは以下のクラスを見てください。
import Foundation
class ClosureHolder {
private var myClosure: (() -> Void)?
deinit {
print("ClosureHolder deinit")
}
init() {
myClosure = { () in
self.innerFunc()
}
myClosure?()
}
func innerFunc() {
print("innerFunc")
}
}
4行目でクロージャを保持するプロパティを定義しています。
11〜14行目でクロージャの中身を定義し、すぐさまクロージャを実行しています。
上記のクラスを使ってみましょう。以下のようにClosureHolderを生成するだけです。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let _ = ClosureHolder()
}
}
実行結果は以下のようになりました。
innerFunc
innerFuncが呼ばれたログは出ていますが、deinitのログが出ていません。ちゃんとClosureHolderが解放されていないんです。
理由はクロージャによってselfが強参照でキャプチャされ、そのクロージャがselfによって強参照で保持されている、つまり、クロージャとselfの間で循環参照が発生しているためです。
メモリリークしないようにするには
この場合はクロージャのキャプチャ時の参照方法を変えることでメモリリークを防ぐことができます。
具体的には以下のように[weak self]を付与することでクロージャがselfを弱参照するようにすればOKです。
また、weakにすることでselfがOptionalになるためselfの処理を呼び出す時は?を付けてアンラップする必要があります。
import Foundation
class ClosureHolder {
private var myClosure: (() -> Void)?
deinit {
print("ClosureHolder deinit")
}
init() {
myClosure = { [weak self] () in // selfのキャプチャをweakに変更する
self?.innerFunc() // weakにすることでselfがOptionalになるため?が必要
}
myClosure?()
}
func innerFunc() {
print("innerFunc")
}
}
この状態でアプリを実行すると以下のような結果になります。
innerFunc ClosureHolder deinit
ちゃんとdeinitのログが出て、メモリリークを防ぐことができました。
まとめ
今回はメモリリークのよくある例をご紹介しました。
オブジェクトの参照方法(強参照なのか弱参照なのか)について何も考えずに実装していると、知らず知らずのうちにメモリリークが発生してしまうかもしれません。
Xcode 4.2からARC(Automatic Reference Counting)が導入されたことでだいぶ参照カウント周りの実装が楽になりましたが、メモリリークが発生しないように意識するのはプログラマの責任です。









