WKUserContentControllerを使うときの罠

338 Views

April 05, 25

スライド概要

このスライドでは、iOSアプリ開発において、WKWebView と WKUserContentController を組み合わせて使用した際に発生したメモリリークの問題について、調査過程と解決策を共有しています。

WKWebViewをもつViewControllerを、WKScriptMessageHandler に準拠させて WKUserContentController に渡すと、循環参照が生じます。ViewControllerが deinit されなくなるため、画面遷移を伴うアプリの場合、WebViewが残り続けてしまい、その量が多い場合、描画に異常が発生することがあります。
これを解消するため、WKScriptMessageHandler を実装した別のクラスを作成し、ViewControllerへの参照を weak にすることで循環を断ち、メモリリークを防止しました。Xcodeの Debug Memory Graph を活用した原因特定もポイントです。

この資料は、iPhone Dev Sapporo — April 5, 2025 で発表した資料です。
https://devsap.connpass.com/event/347851/

profile-image

ソフトウェアエンジニア|Swift中心にモバイルアプリやウェブ開発をやっています。 ESP32や3Dプリンタ(Ender3 S1 Pro)を活用して、自宅の作業環境をカスタマイズ中。 シンプルで使いやすいものを作るのが理想。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

WKUserContentControllerを 使うときの罠 鈴木孝宏 (sussan0416), 2025-04-05, iPhone Dev Sapporo

2.

自己紹介 Uターン 16年ぶり札幌 鈴木孝宏(sussan0416, sussan-po.com) • 株式会社Helpfeel / 開発部プロダクトエンジニア • Gyazoの開発を担当 • メインスキル • iOS, macOSアプリの開発 • React, Rails側も一部担当 • ものづくりが好き • 電子工作(ESP32)、3Dプリンター(Ender3)

3.

本日の話題

4.

WKWebViewのあるViewControllerが なかなかdeinitされなくて困った

5.

調査したこと & 思考過程をシェアしたい

6.

今回のアプリ (※) 本職ではなく、個人的に携わっている案件 UIの階層構造 NVC rootVC ViewController ViewController ViewController WKWebView WKWebView WKWebView …… WebViewでタップしたリンクに応じて、VCをプッシュ遷移

7.

症状 ある日、開発チームからの連絡 • アプリをしばらく使っていると、 WebViewの画面がチカチカと点滅したり、画面が白くなってしまう

8.

再現確認 • ページ読み込み時に、全体が白くなる • 部分的に表示されたり、されなかったりする 実際の映像 • スクロール中に、要素が消えたり現れたりする (会場のみで再生) 再現 発表のための再現用アプリ → ・表示内容は実際のもの ・VCの階層構造も同様

9.

調査 • 仮説 • WebページのDOMの構造が崩れている? • 結果 • ページの中に白いものは混ざっておらず、正常だった

10.

調査 webView.isInspectable = true 再現 Inspectorを起動して、DOMや通信を確認した

11.

不自然な挙動を発見

12.

再現 閉じたはずのページが、残っていた 実際に症状がある時は、15〜20個くらいあった

13.

調査 • 仮説 • 前の画面に戻っても、ViewControllerがdeinitされてない? • 調査と結果 • ViewControllerのdeinitにprintを追加 • deinit自体が呼ばれていなかった

14.

何かが、ViewControllerを参照し続けている……?

15.

Debug Memory Graphを使う • インスタンスの参照関係が、グラフィカルに確認できるツール

17.

Backtrace機能: 選択したインスタンスを生成したコードに ジャンプできる

18.

Backtrace機能を有効にするには ここをチェックする

19.

原因 class WebViewController: UIViewController { override func viewDidLoad() { let contentController = WKUserContentController() contentController.add(self, name: "messageHandler") // ここで強参照してた let configuration = WKWebViewConfiguration() configuration.userContentController = contentController let webView = WKWebView(frame: .zero, configuration: configuration) ... } } extension WebViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { print("Received message: \(message.body)") } }

20.

循環参照 WKWebView WKWebViewConfiguration self.view経由 WKUserContentController add(̲:name) self (ViewController)

21.

実装の修正 Before class WebViewController: UIViewController { override func viewDidLoad() { let contentController = WKUserContentController() contentController.add(self, name: "messageHandler") let configuration = WKWebViewConfiguration() configuration.userContentController = contentController let webView = WKWebView(frame: .zero, configuration: configuration) ... } } extension WebViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { print("Received message: \(message.body)") } }

22.

実装の修正 After class WebViewController: UIViewController { override func viewDidLoad() { let handler = MyMessageHandler() let contentController = WKUserContentController() contentController.add(handler, name: "messageHandler") let configuration = WKWebViewConfiguration() configuration.userContentController = contentController let webView = WKWebView(frame: .zero, configuration: configuration) ... } } final class MyMessageHandler: NSObject, WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { print("Received message: \(message.body)") } }

23.

実装の修正 After class WebViewController: UIViewController { override func viewDidLoad() { let handler = MyMessageHandler() handler.webViewController = self let contentController = WKUserContentController() contentController.add(handler, name: "messageHandler") let configuration = WKWebViewConfiguration() configuration.userContentController = contentController let webView = WKWebView(frame: .zero, configuration: configuration) ... } } final class MyMessageHandler: NSObject, WKScriptMessageHandler { weak var webViewController: WebViewController? // 親VCがどうしても必要なら、weakにしたらよい func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { print("Received message: \(message.body)") } }

24.

循環参照の修正 Before WKWebView WKWebViewConfiguration self.view経由 WKUserContentController add(̲:name) self (ViewController)

25.

循環参照の修正 After WKWebView deinit できる!! self.view経由 WKWebViewConfiguration self (ViewController) WKUserContentController weak add(̲:name) MyScriptHandler

26.

まとめ • WKWebView で WKUserContentController を使う時の罠 • WKUserContentController の add(_:name) で渡す WKScriptMessageHandler として、self (ViewController)を渡すと、 循環参照になり、VCが deinit されない • WKScriptMessageHandler に準拠した、NSObject クラスを別で作ること で循環状態を解決した • Debug Memory Graph 機能が便利!