web-dev-qa-db-fra.com

WKWebView provoque la fuite de mon contrôleur de vue

Mon contrôleur de vue affiche un WKWebView. J'ai installé un gestionnaire de messages, une fonctionnalité intéressante du Kit Web qui permet de notifier mon code depuis la page 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) {
        // ...
}

Jusqu'ici, tout va bien, mais j'ai maintenant découvert que mon contrôleur de vue fuyait. Lorsqu'il est supposé être désalloué, il ne l'est pas:

deinit {
    println("dealloc") // never called
}

Il semble que le simple fait de m'installer en tant que gestionnaire de messages provoque un cycle de rétention et donc une fuite!

52
matt

Correct comme d'habitude, King Friday. Il s'avère que le WKUserContentController conserve son gestionnaire de messages . Cela a un certain sens, car il pourrait difficilement envoyer un message à son gestionnaire de messages si son gestionnaire de messages avait cessé d’exister. C'est parallèle à la manière dont CAAnimation conserve son délégué, par exemple.

Cependant, cela provoque également un cycle de conservation, car le WKUserContentController lui-même présente une fuite. Cela n'a pas beaucoup d'importance en soi (ce n'est que 16K), mais le cycle de conservation et la fuite du contrôleur de vue sont mauvais.

Ma solution consiste à interposer un objet trampoline entre WKUserContentController et le gestionnaire de messages. L'objet trampoline n'a qu'une faible référence au véritable gestionnaire de messages, il n'y a donc pas de cycle de conservation. Voici l'objet de trampoline:

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)
    }
}

Maintenant, lorsque nous installons le gestionnaire de messages, nous installons l’objet trampoline au lieu de self:

self.wv.configuration.userContentController.addScriptMessageHandler(
    LeakAvoider(delegate:self), name: "dummy")

Ça marche! On appelle maintenant deinit, prouvant qu'il n'y a pas de fuite. Il semble que cela ne devrait pas fonctionner, car nous avons créé notre objet LeakAvoider et n'avons jamais utilisé de référence. mais rappelez-vous que le WKUserContentController lui-même le conserve, il n'y a donc aucun problème.

Pour être complet, maintenant que deinit est appelé, vous pouvez désinstaller le gestionnaire de messages à cet emplacement, bien que je ne pense pas que cela soit réellement nécessaire:

deinit {
    println("dealloc")
    self.wv.stopLoading()
    self.wv.configuration.userContentController.removeScriptMessageHandlerForName("dummy")
}
111
matt

La fuite est provoquée par userContentController.addScriptMessageHandler(self, name: "handlerName") qui gardera une référence au gestionnaire de messages self

Pour éviter les fuites, supprimez simplement le gestionnaire de messages via userContentController.removeScriptMessageHandlerForName("handlerName") lorsque vous n'en avez plus besoin. Si vous ajoutez addScriptMessageHandler à viewDidAppear, nous vous conseillons de le supprimer dans viewDidDisappear

19
siuying

La solution proposée par matt est juste ce qu'il faut. Je pensais que je le traduirais en code 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

Alors utilisez-le comme ceci:

WKUserContentController *userContentController = [[WKUserContentController alloc] init];    
[userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"name"];
16
johan

Problème de base: WKUserContentController contient une référence forte à tous les WKScriptMessageHandlers qui lui ont été ajoutés. Vous devez les supprimer manuellement.

Comme cela reste un problème avec Swift 4.2 et iOS 11, je souhaite suggérer une solution utilisant un gestionnaire distinct du contrôleur de vue contenant le UIWebView. De cette façon, le contrôleur de vue peut définir normalement et demander au gestionnaire de nettoyer également.

Voici ma solution:

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 MinigameViewController {
    func handleHelloWorldEvent() {

    }
}

Gestionnaire:

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()
    }
}
0
Philipp Otto

J'ai également noté que vous devez également supprimer le (s) gestionnaire (s) de messages pendant le démantèlement, sinon le (s) gestionnaire (s) demeurera en vie (même si tout le reste de la vue Web est désalloué):

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

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