1. ホーム
  2. ios

[解決済み] WKWebViewでビューコントローラがリークする

2023-05-24 20:28:13

質問

私のビューコントローラはWKWebViewを表示します。私はメッセージハンドラをインストールしました。これはWeb Kitのクールな機能で、私のコードがWebページ内部から通知されることを可能にします。

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    let url = // ...
    self.wv.loadRequest(NSURLRequest(URL:url))
    self.wv.configuration.userContentController.addScriptMessageHandler(
        self, name: "dummy")
}

func userContentController(userContentController: WKUserContentController,
    didReceiveScriptMessage message: WKScriptMessage) {
        // ...
}

ここまでは良かったのですが、今度はビューコントローラーが漏れていることがわかりました。

deinit {
    println("dealloc") // never called
}

メッセージハンドラとして自分自身をインストールするだけで、retainサイクルが発生し、その結果リークが発生するようです!

どのように解決するのですか?

いつものように正しい、キングフライデー。結局、WKUserContentControllerの はそのメッセージハンドラ . メッセージハンドラが存在しなくなったら、メッセージハンドラにメッセージを送ることはできないので、これはある程度理にかなっています。これは、例えば、CAAnimation がそのデリゲートを保持する方法と並行しています。

しかし、WKUserContentController自体がリークしているため、retainサイクルが発生します。それ自体はあまり重要ではありませんが(たった16Kです)、retainサイクルとビューコントローラーのリークが悪いのです。

私の回避策は、WKUserContentController とメッセージハンドラの間にトランポリンオブジェクトを介在させることです。トランポリンオブジェクトは実際のメッセージハンドラへの弱い参照を持つだけなので、retain cycleはありません。これが、trampoline オブジェクトです。

class LeakAvoider : NSObject, WKScriptMessageHandler {
    weak var delegate : WKScriptMessageHandler?
    init(delegate:WKScriptMessageHandler) {
        self.delegate = delegate
        super.init()
    }
    func userContentController(userContentController: WKUserContentController,
        didReceiveScriptMessage message: WKScriptMessage) {
            self.delegate?.userContentController(
                userContentController, didReceiveScriptMessage: message)
    }
}

これで、メッセージハンドラをインストールするときに、trampoline オブジェクトの代わりに self :

self.wv.configuration.userContentController.addScriptMessageHandler(
    LeakAvoider(delegate:self), name: "dummy")

うまくいきましたね。今すぐ deinit が呼び出され、リークがないことが証明されました。LeakAvoider オブジェクトを作成し、その参照を保持していないので、うまくいかないように見えますが、WKUserContentController 自身がそれを保持しているので、問題はないことを思い出してください。

完全を期すために、今 deinit が呼び出されたので、そこでメッセージハンドラをアンインストールすることができますが、これは実際には必要ないと思います。

deinit {
    println("dealloc")
    self.wv.stopLoading()
    self.wv.configuration.userContentController.removeScriptMessageHandlerForName("dummy")
}