しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

\[weak self]が必要な時を調べてみる

循環参照対策で使う[weak self]ですが、常に付ける必要があるのか、それとも付けなくて良い時があるのかを調べてみました。

UIAlertControllerを使った検証

最初に、下のようなUIAlertControllerで[weak self]が必要なのかを調べてみました。

let alert = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { [weak self] _ in
    self?.dismiss(animated: true, completion: nil)
}))
present(alert, animated: true, completion: nil)

確認の為、下のようなUIViewControllerを作成しました。

import UIKit

class MyViewController: UIViewController {
    deinit {
        print("deinit")
    }
    
    override func viewDidAppear(_ animated: Bool) {
        let alert = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { _ in
            self.dismiss(animated: true, completion: nil)
        }))
        present(alert, animated: true, completion: nil)
    }
}

アプリを起動すると、下のようにUIAlertControllerが表示されます。
この状態で循環参照が発生するか調べてみました。

f:id:llcc:20171013131339p:plain

確認した所、無事にdeinitメソッドが呼ばれたので循環参照は発生していませんでした。

f:id:llcc:20171013131439p:plain

次はUIAlertControllerをローカル変数ではなくインスタンス変数にしました。

class MyViewController: UIViewController {
    let alert = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
    
    deinit {
        print("deinit")
    }
    
    override func viewDidAppear(_ animated: Bool) {
        alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { _ in
            self.dismiss(animated: true, completion: nil)
        }))
        present(alert, animated: true, completion: nil)
    }
}

今回はdeinitが呼ばれなかったので、循環参照が発生していたようです。

f:id:llcc:20171013131626p:plain

内部で起こっている事を予想する

内部のメモリの動きを想像してみました。

UIAlertControllerがローカル変数だった時

  1. UIViewControllerを表示する(UIViewControllerの参照カウントが1になる)
  2. UIAlertControllerインスタンスをローカル変数として作成、それをpresentする(UIViewControllerとUIAlertControllerの参照カウントがそれぞれ+1されて2と1になる)
  3. OKボタンを押す事で、UIAlertControllerが消える(dismissによってUIAlertControllerへの参照が消えてUIAlertControllerの参照カウントは-1されて0になる)
  4. UIAlertControllerの参照カウントが0なので、メモリ解放される(それによってUIViewControllerへの参照も-1されて循環参照が起こらなくなる)

UIAlertControllerがインスタンス変数だった時

  1. UIViewControllerを表示する(UIViewControllerの参照カウントが1になる)
  2. UIAlertControllerをインスタンス変数として作成、それをpresentする(インスタンス変数にしたこととpresentした事でUIAlertControllerの参照カウントは2になる)
  3. OKボタンを押す事で、UIAlertControllerが消える(dismissによってUIAlertControllerへの参照が消えてUIAlertControllerの参照カウントは-1されて1になる)
  4. UIAlertControllerの参照カウントが1なので、メモリ解放されない(それによってUIViewControllerへの参照も+1が残り続けて参照カウントが0にならない)

独自クラスで検証する

次はUIAlertControllerではなく、独自クラスで検証してみます。
まずは下のようにブロックを実行するViewControllerを作ってみます。

class MyViewController: UIViewController {
    deinit {
        print("deinit")
    }
    
    override func viewDidAppear(_ animated: Bool) {
        let block = {
            self.dismiss(animated: true, completion: nil)
        }
        block()
    }
}

このようにブロックをローカル変数として持つと、deinitが呼ばれました。

f:id:llcc:20171013132815p:plain

しかしブロックをインスタンス変数として持つとdeinitは呼ばれませんでした。

class MyViewController: UIViewController {
    var block: (() -> ())?
    
    deinit {
        print("deinit")
    }
    
    override func viewDidAppear(_ animated: Bool) {
        block = {
            self.dismiss(animated: true, completion: nil)
        }
        block?()
    }
}

この時、[weak self]を使えばdeinitが呼ばれるようになります。

class MyViewController: UIViewController {
    var block: (() -> ())?
    
    deinit {
        print("deinit")
    }
    
    override func viewDidAppear(_ animated: Bool) {
        block = { [weak self] in
            self?.dismiss(animated: true, completion: nil)
        }
        block?()
    }
}

f:id:llcc:20171013133032p:plain

それと下のようにブロック中でselfを使わない場合は[weak self]しなくてもdeinitが呼ばれます。

class MyViewController: UIViewController {
    var block: (() -> ())?
    
    deinit {
        print("deinit")
    }
    
    override func viewDidAppear(_ animated: Bool) {
        block = {}
        self.dismiss(animated: true, completion: nil)
    }
}

まとめ

  • UIAlertControllerでは[weak self]を使わなくて良い時はある
  • 「UIAlertControllerをインスタンス変数で持つ」など、present以外で参照カウントが+1されるされる時は[weak self]が必要