文字っぽいの。

文字を書いています。写真も混ざります。

`WKUserContentController.add(_:name:)` はちゃんと解放しないとメモリリークする。

開発環境

背景

WKWebViewを使っていると、JavaScriptとSwiftでやり取りをしたいことがあります。そんな時に調べていると、こういうサンプルコードを良く見ます。

final class SomeViewController: UIViewController {
    @IBOutlet weak var webView: WKWebView! {
        didSet {
            webView.configuration.userContentController.add(self, name: "EventName")
        }
    }
}

extension SomeViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "EventName" {
            // JS側で `webkit.messageHandlers.EventName.postMessage(message);` を呼ぶとここが呼ばれる。
        }
    }
}

課題

上記コードの

webView.configuration.userContentController.add(self, name: "EventName")

では self を渡していますがこの self によって循環参照が発生して、メモリリークします。そのため、SomeViewControllerの画面を閉じたとしてもメモリが解放されず、SomeViewControllerを開けば開くほどメモリを食いつぶしていきます。

解決方法1

使い終わった際にはちゃんと解放してあげましょう。

webView.configuration.userContentController.removeScriptMessageHandler(forName: "EventName")

JavaScriptとSwiftの連携が不要になったタイミングや、viewDidDisappearなどで呼んでおくと良さそうです。

解決方法2

解決方法1では、大量にJSのEvent登録をしたり、removeScriptMessageHandler を呼ぶタイミングが難しいときに扱いが困難になります。そこで、渡した self がweakになるように間に1つclassをかませてあげます。

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

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

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

使う時はこんな感じ

wkWebView.configuration.userContentController.add(WKScriptMessageHandlerWithWeakReference(self), name: "EventName")

これによってわざわざ removeScriptMessageHandler を呼ばなくても良くなります。

感想

昔のNotificationCenterやKVOを思い出しました。

参考文献

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