エンジニアのはしがき

プログラミングの日々の知見を書き連ねているブログです

Swiftで循環参照によるメモリリークを起こしてしまった話

f:id:tansantktk:20210122185759p:plain

こんばんは!今回は題名の通り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
               // 何もしない
           })
    }
}

HogeControllerselect1Actionに指定しているクロージャを強参照します。

一方でクロージャ内のselfHogeControllerを強参照します。

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インスタンスは破棄されるようになります。

結果、メモリは解放されメモリリークは回避することができました。

あとがき

「ロジックを考えてソースを書く」「言語仕様を理解する」、両方やらなくちゃならないのがプログラマの辛いところ…😎