3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

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

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

class Tab {
   var webView: WKWebView
}

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

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

KVO登録

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

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

メモリリークするパターン
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を弱参照にする実装が挙げられる。

メモリリークしないパターン
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を指定する場合に、メモリリークが発生しやすい。

メモリリークするパターン
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クラスを強参照することになる。そのため、循環参照が発生し、メモリリークが発生してしまう。

解決策としては、このページの流用となるが、以下のようにTabを一旦弱参照にするクラスをブリッジとして入れる実装が挙げられる。

メモリリークしないパターン
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を強参照しなくなり、循環参照が発生せず、メモリリークが発生しない。

参考

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?