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