[iOS] そのアプリ、メモリリークしてませんか?
はじめに
こんにちは。モバイルアプリサービス部の加藤潤です。 今回は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)が導入されたことでだいぶ参照カウント周りの実装が楽になりましたが、メモリリークが発生しないように意識するのはプログラマの責任です。