この記事は何?
クロージャにおいて [weak self] は本当に必要なのか?
実際のところケースバイケースですが、今回は DispatchQueue.main を例にしてクイズ・解説をしたいと思います。
ということで、早速クイズです。
環境
- Xcode Version 11.3.1 (11C504)
クイズ
第一問
次のコードに [weak self] は必要ですか?
DispatchQueue.main.async { [weak self] in
guard let `self` = self else { return }
// self 使った処理
}
第二問
次のコードに [weak self] は必要ですか?
UIView.animate(withDuration: 10) { [weak self] in
guard let `self` = self else { return }
// self 使った処理
}
いかがでしょうか?
特段の理由なく [weak self] を記述しているのであれば、引き続きクイズ回答・解説をご覧ください。
クイズ回答
第一問
A. [weak self] は必須ではない。処理の内容によっては [weak self] が妥当な場合もある。
第二問
A. [weak self] は不要。
解説
一般的な話として、循環参照を回避する目的で [weak self] を指定する場合がありますが、
上記の処理は、クロージャ実行後に循環参照が解決されるので [weak self] は必須ではありません。
かんたんな実験によって、このことを確認することができます。
class Detail1ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.async { [object = SomeObject()] in
object.doSomething()
}
// あるいはこういうコード
// UIView.animate(withDuration: 30) { [object = SomeObject()] in
// object.doSomething()
// }
}
}
ここで、SomeObject には動作確認のための実装をしておきます。
class SomeObject {
init() { log() }
func doSomething() { log() }
deinit { log() }
private func log(_ function: String = #function) {
print("🔰🔰🔰\t\(Self.self).\(function)")
}
}
そうすることで、実行時には次のコンソール出力を確認することができます。
🔰🔰🔰 SomeObject.init()
🔰🔰🔰 SomeObject.doSomething()
🔰🔰🔰 SomeObject.deinit
doSomething() の実行後に deinit が呼ばれることが確認できると思います。
SomeObject を強参照しているのはクロージャだけなので、
これは、クロージャの実行後にクロージャ自体がメモリから開放されたことを意味しています。
つまり、DispatchQueue や UIView.animate は [weak self] せずとも、循環参照によるメモリリークは発生しないことを意味しています。
なので、循環参照の回避を目的とした weak キャプチャは不要です。
ただし、非同期処理の実行時にオブジェクトが開放されていても良い場合や、
開放されていることが妥当な場合は、[weak self] を指定することが好ましいと思います。
たとえば、DispatchQueue...asyncAfter が実行される前に、ナビゲーションコントローラから該当のビューコントローラがポップされた場合を考えてみます。
この場合は、次のコードのように [weak self] や [weak view = view] のような指定を行うことで、無意味な処理をスキップすることができます。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 強参照によるキャプチャを行った場合、
// クロージャの実行が完了するまでビューコントローラが開放されることはありません。
// ビューコントローラをポップしたあとはビューを操作しても意味が無いので weak キャプチャが妥当です。
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
self.view?.backgroundColor = .orange
}
}
}
DispatchQueue のように、非同期処理の実行後にクロージャが破棄される処理では、次のことに気をつけましょう。
- weak キャプチャせずとも循環参照は発生しない
- 必要に応じて weak キャプチャを使うことで無意味な処理をスキップできる
一方で UIView.animate については、単純にキャプチャリストは不要です。
クロージャ実行後、クロージャを破棄する点では DispatchQueue と同じですが、実はクロージャを同期処理として実行するので、weak キャプチャは不要です。
また、UIView.AnimationOptions.repeat などを指定してもクロージャの実行は1回きりです。
かんたんな実験によって、この事実の動作確認ができます。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// `animations` で指定したクロージャは即実行されるので、weak キャプチャは無意味。
//
// `completion` で指定したクロージャは、`animations` で指定したアニメーションが停止したタイミングで実行される
// 例えば、次のいずれかの処理を行うと `completion` がコールされる。
// - ビューコントローラをナビゲーションコントローラからポップする
// - view.layer.removeAllAnimations() をコールする
//
// `animations`, `completion` ともに実行は一度きりで、
// 実行後にクロージャは開放される
UIView.animate(
withDuration: 10,
delay: 0,
options: .repeat,
animations: { self.view?.backgroundColor = .orange },
completion: { [o = SomeObject()] _ in o.doSomething() }
)
}
deinit {
print("🔰🔰🔰\t\(Self.self).\(#function)")
}
}
このように [weak self] の無いコードを書いても、ナビゲーションコントローラから該当のビューコントローラをポップすることで、ビューコントローラの deinit が呼ばれてメモリリークしないことが確認できます。
まとめ
非同期処理のコールバックハンドラとしてクロージャを扱う場合でも、クロージャの実行後に、そのクロージャがメモリから開放されれば、循環参照が解決されたことになるのでメモリリークは発生しません。
そして、 DispatchQueue は上記に該当するので、[weak self] がなくてもメモリリークしません。[weak self] の有無は、循環参照が引き起こすメモリリーク問題とは切り離して、処理のスキップが必要かどうかで判断しましょう。