[iOS] メモリリークをXcodeでチェックして、リークしないようにしたい!
こんにちは。きんくまです。
今回はiOSでのメモリリークのお話です。
検索してみたところ以前このブログでもいくつか記事が上がっていました。
[iOS] そのアプリ、メモリリークしてませんか?
気がつかないでメモリリークをやってしまったりするので、チェックしたり防止しましょう。
そもそもメモリリークって?
メモリリークは、あるオブジェクトをメモリに確保したあと、そのオブジェクトが解放されずにメモリ内に残ってしまう状態です。
何が怖いかといいますと最終的にメモリが足りなくなりアプリが落ちてしまうことです。
解放されないオブジェクトが1つだけだとしても、そこから数珠つなぎに複数のオブジェクトが参照されている場合は、思いがけずメモリをたくさん確保してしまったりします。
また、画面の行き来でリークが起きると、行き来のたびにリークの数が貯まっていくことになります。
それで、どういうときにメモリが解放されないかというと循環参照がおきるときです。
Appleのドキュメントがわかりやすいです。
Strong Reference Cycles Between Class Instances
2つのオブジェクト同士がお互いをプロパティとして 強参照で保持しあう ときに起きます。
これを解消するには、2つの参照のうち 片方の参照を weak のキーワードをつけて弱参照に変えれば 可能です。
Xcodeでメモリリークをチェックしよう!
この機能がいつからついたのかは知らないのですが、最近のXcodeでは簡単にメモリチェックが行えます。(現在私の手持ちは10.1)
シミュレータでアプリを立ち上げた後、Xcodeのボタンを押します。
ボタンを押すとこのような画面に切り替わります。
紫のビックリマークがメモリリークの起こっているところです。
上のキャプチャでは、素敵な感じにメモリリークが起こっていますね♪
メモリリークがおきていない場合は、こんな感じに直線で参照されている場合が多いです。 ただ直線だからといって、リークがおきないわけではないです。循環参照がおきているかどうかに注目です。
メモリリークを防止しよう
一番はじめにご紹介した、加藤氏の記事ではこうなっていました。
- お互いをプロパティで強参照し合うケース
- プロパティに保持したクロージャにselfが強参照でキャプチャされているケース
これの補足パターンをご紹介します。
Delegateパターンでの強参照
メモリリークしてる!protocolを使ったDelegateパターン
DetailViewController
protocol DetailViewControllerDelegate { func sample() } class DetailViewController: UIViewController { var delegate: DetailViewControllerDelegate? }
ListViewController
class ListViewController: UIViewController, DetailViewControllerDelegate { var detailViewController: DetailViewController? func pushDetailView() { let storyboard = UIStoryboard(name: "DetailViewController", bundle: nil) if let detailViewControllerNotNil = storyboard.instantiateInitialViewController() as? DetailViewController { //お互いに強参照する detailViewController = detailViewControllerNotNil detailViewControllerNotNil.delegate = self navigationController?.pushViewController(detailViewControllerNotNil, animated: true) } } func sample() { } }
- ListViewControllerのdetailViewControllerでDetailViewControllerを強参照 - DetailViewControllerのdelegateで、ListViewControllerを強参照
ListViewControllerとDetailViewControllerがお互いに強参照しています。
メモリリークを解消!protocolを使ったDelegateパターン
この場合以下のようにすれば、メモリリークが解消します。
- protocolはclassを対象にする
- weakプロパティにする
// protocolはclassを対象にする protocol DetailViewControllerDelegate: class { func sample() } class DetailViewController: UIViewController { // weakプロパティにする weak var delegate: DetailViewControllerDelegate? }
- ListViewControllerのdetailViewControllerでDetailViewControllerを強参照 - DetailViewControllerのdelegateで、ListViewControllerを弱参照 <- 変更された
解消されて良かった良かった!
クロージャで循環参照がおきるのはどういう時か?
次にクロージャです。クロージャでもリークは起きてしまいます、、。
クロージャは参照型です(Reference Types)。
Closures Are Reference Types
なのでメモリまわりについては、クラスを作ったように考えると良いと思います。
こんな感じにお互いに強参照でプロパティを持ち合うときに循環参照がおきます。
あるオブジェクトのプロパティがクロージャを強参照 <--> クロージャ内でオブジェクトをキャプチャ
Appleのドキュメントを読むとよりイメージしやすいです
Strong Reference Cycles for Closures
循環参照がおきていないパターン
以下のコードはクロージャの中で self
や companyRepository
を使っていますが、循環参照はおきていないです。
class CompanyRepository { var message: String? func getEmployeeTotal(completion:@escaping (Int) -> Void) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { completion(5) } } } class ListViewController: UIViewController { var networking: Bool = false var companyRepository: CompanyRepository? func getEmployeeTotal() { networking = true companyRepository = CompanyRepository() if let companyRepository = companyRepository { let closure: (Int) -> Void = { (total) in self.networking = false companyRepository.message = "Hello world" } companyRepository.getEmployeeTotal(completion: closure) } } }
では self
について循環参照を起こしてみます。
selfで循環参照がおきるパターン
class CompanyRepository { var message: String? func getEmployeeTotal(completion:@escaping (Int) -> Void) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { completion(5) } } } class ListViewController: UIViewController { var networking: Bool = false var companyRepository: CompanyRepository? //プロパティ追加 var sampleClosure: ((Int) -> Void)? func getEmployeeTotal() { networking = true companyRepository = CompanyRepository() if let companyRepository = companyRepository { let closure: (Int) -> Void = { (total) in //クロージャ内でself(ListViewController)をキャプチャ self.networking = false companyRepository.message = "Hello world" } // クロージャをプロパティで保持 sampleClosure = closure companyRepository.getEmployeeTotal(completion: closure) } } }
ListViewController内でプロパティとしてクロージャを保持しました。
- ListViewControllerのsampleClosureでクロージャを強参照 - クロージャ内でListViewControllerをキャプチャ(強参照)
上記のように循環参照がおこったので、メモリリークが起こります。これを解消するには、おなじみの [weak self] を使います。
selfで循環参照がおきるパターンを解消
// [weak self] にする let closure: (Int) -> Void = { [weak self](total) in self?.networking = false companyRepository.message = "Hello world" }
- ListViewControllerのsampleClosureでクロージャを強参照 - クロージャ内でListViewControllerをキャプチャ(弱参照) <- 変更された
次に companyRepository
で循環参照を起こしてみます。
companyRepositoryで循環参照がおきるパターン
class CompanyRepository { var message: String? //クロージャプロパティ追加 var apiCompletion: ((Int) -> Void)? func getEmployeeTotal(completion:@escaping (Int) -> Void) { //クロージャをプロパティで保持 apiCompletion = completion DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { completion(5) } } } class ListViewController: UIViewController { var networking: Bool = false var companyRepository: CompanyRepository? var sampleClosure: ((Int) -> Void)? func getEmployeeTotal() { networking = true companyRepository = CompanyRepository() if let companyRepository = companyRepository { let closure: (Int) -> Void = { [weak self](total) in self?.networking = false //クロージャ内で companyRepository をキャプチャ companyRepository.message = "Hello world" } sampleClosure = closure companyRepository.getEmployeeTotal(completion: closure) } } }
- CompanyRepositoryのapiCompletionでクロージャを強参照 - クロージャ内でcompanyRepository(CompanyRepositoryのインスタンス)をキャプチャ(強参照)
上記のことが起こったので循環参照が起きました。
ではこれを解消しましょう!今度は [weak companyRepository] を追加します。
// [weak companyRepository] を追加 let closure: (Int) -> Void = { [weak self, weak companyRepository](total) in self?.networking = false companyRepository?.message = "Hello world" }
- CompanyRepositoryのapiCompletionでクロージャを強参照 - クロージャ内でcompanyRepository(CompanyRepositoryのインスタンス)をキャプチャ(弱参照)<- 変更された
これでメモリリークが解消しました!
まとめ
クロージャがあるときは、何も考えずに weak self をしてしまっていたのですが、毎回やる必要はなかったようです、、。(やってても特に問題はないと思いますが)
なので、どういうときに循環参照がおきるのかを意識しつつ、心配なところは最初にご紹介したXcodeのツールを使ってチェックしたいと思いました。
ではでは!