[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

循環参照がおきていないパターン

以下のコードはクロージャの中で selfcompanyRepository を使っていますが、循環参照はおきていないです。

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のツールを使ってチェックしたいと思いました。

ではでは!