私のView ControllerはWKWebViewを表示します。メッセージハンドラーをインストールしました。これは、Webページ内からコードを通知できるクールな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) {
// ...
}
これまでのところは良いですが、今、私はView Controllerがリークしていることを発見しました。
deinit {
println("dealloc") // never called
}
メッセージハンドラとして自分をインストールするだけで、保持サイクルが発生し、リークが発生するようです。
いつもどおり正しい、金曜日。 WKUserContentController メッセージハンドラを保持であることがわかります。メッセージハンドラーが存在しなくなった場合、メッセージハンドラーにメッセージを送信することはほとんどできなかったため、これにはある程度の意味があります。たとえば、CAAnimationがデリゲートを保持する方法と並行しています。
ただし、WKUserContentController自体がリークしているため、保持サイクルも発生します。それ自体はそれほど重要ではありません(わずか16Kです)が、View Controllerの保持サイクルとリークは悪いです。
私の回避策は、WKUserContentControllerとメッセージハンドラーの間にトランポリンオブジェクトを挿入することです。トランポリンオブジェクトには実際のメッセージハンドラへの弱い参照しかないため、保持サイクルはありません。トランポリンオブジェクトは次のとおりです。
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)
}
}
ここで、メッセージハンドラをインストールするときに、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")
}
リークは、メッセージハンドラーself
への参照を保持するuserContentController.addScriptMessageHandler(self, name: "handlerName")
によって引き起こされます。
リークを防ぐには、不要になったらuserContentController.removeScriptMessageHandlerForName("handlerName")
を使用してメッセージハンドラを削除します。 addScriptMessageHandlerをviewDidAppear
に追加する場合は、viewDidDisappear
で削除することをお勧めします。
Mattが投稿したソリューションは、まさに必要なものです。私はそれをobjective-cコードに翻訳すると思った
@interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>
@property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate;
- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;
@end
@implementation WeakScriptMessageDelegate
- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate
{
self = [super init];
if (self) {
_scriptDelegate = scriptDelegate;
}
return self;
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
[self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
}
@end
次に、次のように使用します。
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"name"];
基本的な問題:WKUserContentControllerは、それに追加されたすべてのWKScriptMessageHandlersへの強力な参照を保持しています。それらを手動で削除する必要があります。
これはSwift 4.2およびiOS 11の問題であるため、UIWebViewを保持するView Controllerとは別のハンドラーを使用するソリューションを提案したいと思います。通常どおりに初期化し、ハンドラーにもクリーンアップするように指示します。
私の解決策は次のとおりです。
UIViewController:
import UIKit
import WebKit
class MyViewController: JavascriptMessageHandlerDelegate {
private let javascriptMessageHandler = JavascriptMessageHandler()
private lazy var webView: WKWebView = WKWebView(frame: .zero, configuration: self.javascriptEventHandler.webViewConfiguration)
override func viewDidLoad() {
super.viewDidLoad()
self.javascriptMessageHandler.delegate = self
// TODO: Add web view to the own view properly
self.webView.load(URLRequest(url: myUrl))
}
deinit {
self.javascriptEventHandler.cleanUp()
}
}
// MARK: - JavascriptMessageHandlerDelegate
extension MyViewController {
func handleHelloWorldEvent() {
}
}
ハンドラ:
import Foundation
import WebKit
protocol JavascriptMessageHandlerDelegate: class {
func handleHelloWorld()
}
enum JavascriptEvent: String, CaseIterable {
case helloWorld
}
class JavascriptMessageHandler: NSObject, WKScriptMessageHandler {
weak var delegate: JavascriptMessageHandlerDelegate?
private let contentController = WKUserContentController()
var webViewConfiguration: WKWebViewConfiguration {
for eventName in JavascriptEvent.allCases {
self.contentController.add(self, name: eventName.rawValue)
}
let config = WKWebViewConfiguration()
config.userContentController = self.contentController
return config
}
/// Remove all message handlers manually because the WKUserContentController keeps a strong reference on them
func cleanUp() {
for eventName in JavascriptEvent.allCases {
self.contentController.removeScriptMessageHandler(forName: eventName.rawValue)
}
}
deinit {
print("Deinitialized")
}
}
// MARK: - WKScriptMessageHandler
extension JavascriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// TODO: Handle messages here and call delegate properly
self.delegate?.handleHelloWorld()
}
}
また、分解中にメッセージハンドラーを削除する必要があることにも注意してください。そうしないと、ハンドラーは(webviewに関する他のすべての割り当てが解除された場合でも)残ります。
WKUserContentController *controller =
self.webView.configuration.userContentController;
[controller removeScriptMessageHandlerForName:@"message"];