WKWebViewを利用した実装でメモリリークしやすいパターン2つ

WKWebViewを利用したアプリを開発していてメモリリークの問題に当たったので、その原因を整理してメモしておく

# メモリリークするパターン

以下のように、`Tab`というクラスがWKWebViewのインスタンスを強参照しているとする。

```swift
class Tab {
   var webView: WKWebView
}
```

WKWebViewをインスタンス化してプロダクト専用の処理や設定を構成する過程で、特に注意せずに実装すると**WKWebViewインスタンスが`Tab`クラスを強参照**することになるパターンがある。
そうなると、WKWebViewインスタンスと`Tab`クラスがお互いを強参照する**循環参照**の状態となり、`Tab`を破棄したつもりでもメモリ上に残ったままとなり、メモリリークが発生してしまう。

このようなパターンの具体的な例を、以下に2つ挙げる。

## KVO登録

WKWebViewにKVOの仕組みを用いて、特定のプロパティが変化したときに`Tab`内の特定の処理を呼び出すという実装を行うとき、メモリリークが発生しやすい。

ここでは、ページ読み込み完了の割合を取得できる`estimatedProgress`のKVOを登録する例を示す。

```swift:メモリリークするパターン
class Tab {
    var webView: WKWebView
    var webViewKeyValueObservers: NSKeyValueObservation?

    func configure() {
        self.webViewKeyValueObservers = webView.observe(\.estimatedProgress) { (webView, _) in
            // 普通に self と書くと強参照になる
            self.estimatedProgressDidChange(webView)
        }
    }

    func estimatedProgressDidChange(_ webView: WKWebView) {
        // ...
    }
}
```

上記のパターンでは、KVO登録時のクロージャ内で`self`つまり`Tab`クラスを強参照しているため、WKWebViewインスタンスが`Tab`を強参照することになる。そのため、循環参照が発生し、メモリリークが発生する。

解決策としては、以下のようにクロージャ内で`self`を弱参照にする実装が挙げられる。

```swift:メモリリークしないパターン
class Tab {
    var webView: WKWebView
    var webViewKeyValueObservers: NSKeyValueObservation?

    func configure() {
        self.webViewKeyValueObservers = webView.observe(\.estimatedProgress) { [weak self] (webView, _) in
            // [weak self] を付与することで self が弱参照になる
            self?.estimatedProgressDidChange(webView)
        }
    }

    func estimatedProgressDidChange(_ webView: WKWebView) {
        // ...
    }
}
```

上記のようにすることで、WKWebViewが`Tab`を強参照しなくなり、循環参照が発生せず、メモリリークが発生しない。

## UserContentController登録

WKWebViewからネイティブの処理を実行する仕組み`UserContentController`を使う時、ネイティブの実装として`Tab`を指定する場合に、メモリリークが発生しやすい。

```swift:メモリリークするパターン
class Tab {
    var webView: WKWebView

    func configure() {
        // 普通に self を渡すと強参照になる
        webView.configuration.userContentController.add(self, name: "test")
    }
}

extension Tab: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard message.name == "test" else { return }
        // ...
    }
}
```

UserContentControllerの`add`は引数に指定したインスタンスを強参照する仕様になっており、上記のパターンでは、WKWebViewのインスタンスが`self`つまり`Tab`クラスを強参照することになる。そのため、循環参照が発生し、メモリリークが発生してしまう。

解決策としては、[このページ](https://stackoverflow.com/questions/26383031/wkwebview-causes-my-view-controller-to-leak)の流用となるが、以下のように`Tab`を一旦弱参照にするクラスをブリッジとして入れる実装が挙げられる。

```swift:メモリリークしないパターン
class Tab {
    var webView: WKWebView

    func configure() {
        // self を弱参照にして渡す
        webView.configuration.userContentController.add(
            WKScriptMessageHandlerWithWeakReference(to: self), name: "test")
    }
}

extension Tab: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard message.name == "test" else { return }
        // ...
    }
}

class WKScriptMessageHandlerWithWeakReference: NSObject, WKScriptMessageHandler {
    private weak var delegate: WKScriptMessageHandler?

    init(to delegate: WKScriptMessageHandler) {
        self.delegate = delegate
        super.init()
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        self.delegate?.userContentController(userContentController, didReceive: message)
    }
}
```

上記のようにすることで、WKWebViewが`Tab`を強参照しなくなり、循環参照が発生せず、メモリリークが発生しない。

# 参考
- https://www.cocoawithlove.com/blog/key-value-observing-wrapper.html