こんばんは!今回は題名の通りSwiftでの失敗談です。
無知は罪なりとはよく言ったものですがそれを再確認させられました🤤
何をやらかしてしまったのか
Swiftのクロージャ内でself
を強参照してしまった為に、メモリ解放が行われずメモリリークが発生してしまいました。
クロージャとは
Swift公式から引用します。
Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.
クロージャは、コード内で受け渡して使用できる機能の自己完結型のブロックです。
引用元: https://docs.swift.org/swift-book/LanguageGuide/Closures.html
JavaScriptのアロー関数に近い概念ですね。関数そのものを変数やプロパティに対して値として渡したい時に使う処理の固まりといったところでしょうか。
さて、話は戻って今回問題となったコードですが、具体的には以下のサンプルのようなソースコードを書いていました。
// とあるHogeControllerクラスの拡張 extention HogeController: ViewController { func resetApp() { let dialog = DialogManager() dialog.confirm( title: "確認", message: "アプリを再度読み込み直します。", select1: "はい", select2: "いいえ", viewController: self, select1Action: { () -> Void in self.reloadWkWebView(wkWebView: self.wkWebView, targetUrl: devSettings.webViewUrl) // クロージャ内でselfを強参照している }, select2Action: { () -> Void in // 何もしない }) } }
HogeController
はselect1Action
に指定しているクロージャを強参照します。
一方でクロージャ内のself
はHogeController
を強参照します。
HogeController
とクロージャの間で循環参照が起きてしまっている為に、HogeController
のインスタンスはいつまで経っても破棄されず、何度も繰り返し処理を行ううちにメモリリークを起こしてしまっていました。
割とSwift界隈では初歩的なミスなようですが、やらかしてしまいました…🤮 原因がすぐに特定できず、割と胃がキリキリ痛んだやらかしでした…🤮
どのように対応したか
クロージャ内のself
を強参照から弱参照に変更しました。
extention HogeController: ViewController { func resetApp() { let dialog = DialogManager() dialog.confirm( title: "確認", message: "アプリを再度読み込み直します。", select1: "はい", select2: "いいえ", viewController: self, select1Action: { [weak self] () -> Void in // キャプチャリストでweakを指定しselfを弱参照させる self.reloadWkWebView(wkWebView: self.wkWebView, targetUrl: devSettings.webViewUrl) }, select2Action: { () -> Void in // 何もしない }) } }
Swiftではself
とだけ記述するとself
を強参照します。
そこでクロージャに[weak self]
を指定し、クロージャ内のself
は明示的に弱参照させます。
弱参照は参照カウンタに影響を与えないので、他のコードに問題が無い限りは循環参照は起こらず、self(=HogeController)
の参照カウンタが0になった時点でHogeController
インスタンスは破棄されるようになります。
結果、メモリは解放されメモリリークは回避することができました。
あとがき
「ロジックを考えてソースを書く」「言語仕様を理解する」、両方やらなくちゃならないのがプログラマの辛いところ…😎