循環参照対策で使う[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が表示されます。
この状態で循環参照が発生するか調べてみました。
確認した所、無事にdeinitメソッドが呼ばれたので循環参照は発生していませんでした。
次は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が呼ばれなかったので、循環参照が発生していたようです。
内部で起こっている事を予想する
内部のメモリの動きを想像してみました。
UIAlertControllerがローカル変数だった時
- UIViewControllerを表示する(UIViewControllerの参照カウントが1になる)
- UIAlertControllerインスタンスをローカル変数として作成、それをpresentする(UIViewControllerとUIAlertControllerの参照カウントがそれぞれ+1されて2と1になる)
- OKボタンを押す事で、UIAlertControllerが消える(dismissによってUIAlertControllerへの参照が消えてUIAlertControllerの参照カウントは-1されて0になる)
- UIAlertControllerの参照カウントが0なので、メモリ解放される(それによってUIViewControllerへの参照も-1されて循環参照が起こらなくなる)
UIAlertControllerがインスタンス変数だった時
- UIViewControllerを表示する(UIViewControllerの参照カウントが1になる)
- UIAlertControllerをインスタンス変数として作成、それをpresentする(インスタンス変数にしたこととpresentした事でUIAlertControllerの参照カウントは2になる)
- OKボタンを押す事で、UIAlertControllerが消える(dismissによってUIAlertControllerへの参照が消えてUIAlertControllerの参照カウントは-1されて1になる)
- 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が呼ばれました。
しかしブロックをインスタンス変数として持つと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?() } }
それと下のようにブロック中で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]が必要