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/
ソフトウェアエンジニア|Swift中心にモバイルアプリやウェブ開発をやっています。 ESP32や3Dプリンタ(Ender3 S1 Pro)を活用して、自宅の作業環境をカスタマイズ中。 シンプルで使いやすいものを作るのが理想。
WKUserContentControllerを 使うときの罠 鈴木孝宏 (sussan0416), 2025-04-05, iPhone Dev Sapporo
自己紹介 Uターン 16年ぶり札幌 鈴木孝宏(sussan0416, sussan-po.com) • 株式会社Helpfeel / 開発部プロダクトエンジニア • Gyazoの開発を担当 • メインスキル • iOS, macOSアプリの開発 • React, Rails側も一部担当 • ものづくりが好き • 電子工作(ESP32)、3Dプリンター(Ender3)
本日の話題
WKWebViewのあるViewControllerが なかなかdeinitされなくて困った
調査したこと & 思考過程をシェアしたい
今回のアプリ (※) 本職ではなく、個人的に携わっている案件 UIの階層構造 NVC rootVC ViewController ViewController ViewController WKWebView WKWebView WKWebView …… WebViewでタップしたリンクに応じて、VCをプッシュ遷移
症状 ある日、開発チームからの連絡 • アプリをしばらく使っていると、 WebViewの画面がチカチカと点滅したり、画面が白くなってしまう
再現確認 • ページ読み込み時に、全体が白くなる • 部分的に表示されたり、されなかったりする 実際の映像 • スクロール中に、要素が消えたり現れたりする (会場のみで再生) 再現 発表のための再現用アプリ → ・表示内容は実際のもの ・VCの階層構造も同様
調査 • 仮説 • WebページのDOMの構造が崩れている? • 結果 • ページの中に白いものは混ざっておらず、正常だった
調査 webView.isInspectable = true 再現 Inspectorを起動して、DOMや通信を確認した
不自然な挙動を発見
再現 閉じたはずのページが、残っていた 実際に症状がある時は、15〜20個くらいあった
調査 • 仮説 • 前の画面に戻っても、ViewControllerがdeinitされてない? • 調査と結果 • ViewControllerのdeinitにprintを追加 • deinit自体が呼ばれていなかった
何かが、ViewControllerを参照し続けている……?
Debug Memory Graphを使う • インスタンスの参照関係が、グラフィカルに確認できるツール
Backtrace機能: 選択したインスタンスを生成したコードに ジャンプできる
Backtrace機能を有効にするには ここをチェックする
原因 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)") } }
循環参照 WKWebView WKWebViewConfiguration self.view経由 WKUserContentController add(̲:name) self (ViewController)
実装の修正 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)") } }
実装の修正 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)") } }
実装の修正 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)") } }
循環参照の修正 Before WKWebView WKWebViewConfiguration self.view経由 WKUserContentController add(̲:name) self (ViewController)
循環参照の修正 After WKWebView deinit できる!! self.view経由 WKWebViewConfiguration self (ViewController) WKUserContentController weak add(̲:name) MyScriptHandler
まとめ • WKWebView で WKUserContentController を使う時の罠 • WKUserContentController の add(_:name) で渡す WKScriptMessageHandler として、self (ViewController)を渡すと、 循環参照になり、VCが deinit されない • WKScriptMessageHandler に準拠した、NSObject クラスを別で作ること で循環状態を解決した • Debug Memory Graph 機能が便利!