web-dev-qa-db-ja.com

WKWebViewによりView Controllerがリークする

私の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
}

メッセージハンドラとして自分をインストールするだけで、保持サイクルが発生し、リークが発生するようです。

61
matt

いつもどおり正しい、金曜日。 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")
}
121
matt

リークは、メッセージハンドラーselfへの参照を保持するuserContentController.addScriptMessageHandler(self, name: "handlerName")によって引き起こされます。

リークを防ぐには、不要になったらuserContentController.removeScriptMessageHandlerForName("handlerName")を使用してメッセージハンドラを削除します。 addScriptMessageHandlerをviewDidAppearに追加する場合は、viewDidDisappearで削除することをお勧めします。

22
siuying

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"];
18
johan

基本的な問題: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()
    }
}
1
Philipp Otto

また、分解中にメッセージハンドラーを削除する必要があることにも注意してください。そうしないと、ハンドラーは(webviewに関する他のすべての割り当てが解除された場合でも)残ります。

WKUserContentController *controller = 
self.webView.configuration.userContentController;

[controller removeScriptMessageHandlerForName:@"message"];
0
coderSeb