2014-10-15 2 views
40

Mein View-Controller zeigt ein WKWebView an. Ich installierte einen Meldungshandler, eine coole Web Kit-Funktion, die mein Code ermöglicht es aus dem Innern der Webseite informiert werden:WKWebView führt zu einem Leck in meinem View-Controller

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) { 
     // ... 
} 

So weit so gut, aber jetzt habe ich entdeckt, dass mein View-Controller ist undicht - wenn es soll aufgehoben werden, ist es nicht:

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

es scheint, dass nur mich als Nachrichten-Handler die Installation eines Zyklus beibehalten verursacht und somit ein Leck!

Antwort

80

Wie üblich, König Freitag. Es stellt sich heraus, dass der WKUserContentController seinen Nachrichtenhandler beibehält. Dies macht eine gewisse Menge Sinn, da es kaum eine Nachricht an seinen Nachrichtenhandler senden könnte, wenn sein Nachrichtenhandler aufgehört hätte zu existieren. Es ist parallel zu der Art und Weise, wie eine CAAnimation beispielsweise ihren Delegaten behält.

Es verursacht jedoch auch einen Retain-Zyklus, da der WKUserContentController selbst undicht ist. Das ist nicht sonderlich wichtig (es sind nur 16K), aber der Retain-Zyklus und das Leck des View-Controllers sind schlecht.

Meine Problemumgehung besteht darin, ein Trampoline-Objekt zwischen dem WKUserContentController und dem Nachrichtenhandler einzufügen. Das Trampoline-Objekt hat nur eine schwache Referenz auf den echten Nachrichtenhandler, so dass es keinen Retain-Zyklus gibt. Hier ist das Trampolin Objekt:

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

Nun, wenn wir die Nachrichten-Handler installieren wir das Trampolin Objekt statt self installieren:

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

Es funktioniert! Jetzt wird deinit aufgerufen, was beweist, dass kein Leck vorhanden ist. Es sieht so aus, als ob dies nicht funktionieren sollte, da wir unser LeakAvoider-Objekt erstellt haben und nie einen Verweis darauf hatten; aber denken Sie daran, der WKUserContentController selbst behält es, so dass es kein Problem gibt.

Für Vollständigkeit, jetzt, wo deinit genannt wird, können Sie die Nachrichten-Handler es deinstallieren, obwohl ich nicht glaube, dies tatsächlich erforderlich ist:

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

überrascht nicht höher upvote. Große Hilfe. – Nick

+0

kann irgendeine Art Seele dieses zu den objektiven äquivalenten Codes übersetzen? – mkto

+0

@mkto - Posted eine obj-c-Version der Implementierung. – johan

12

Das Leck durch userContentController.addScriptMessageHandler(self, name: "handlerName") verursacht wird, die einen Verweis halten zum Nachrichtenhandler self.

Um Undichtigkeiten zu vermeiden, entfernen Sie den Nachrichtenhandler einfach über userContentController.removeScriptMessageHandlerForName("handlerName"), wenn Sie ihn nicht mehr benötigen. Wenn Sie den addScriptMessageHandler unter viewDidAppear hinzufügen, ist es eine gute Idee, ihn in viewDidDisappear zu entfernen.

+0

"Wenn Sie es nicht mehr brauchen" Das Problem ist: Wann ist das? Idealerweise wäre es in Ihrem View-Controller 'deinit' (Objective-C' dealloc'), aber es wird nie aufgerufen, weil (warten Sie darauf) wir lecken! Das ist das Problem, das meine Trampolinlösung löst. Übrigens, das gleiche Problem und die gleiche Lösung geht weiter in iOS 9. – matt

+0

Es hängt wirklich von Ihrem Anwendungsfall. Sagen wir, wenn Sie es über presentViewController präsentieren, ist der Zeitpunkt, wann Sie es entlassen. Wenn Sie es in einen Nav-View-Controller schieben, ist es an der Zeit, wenn Sie es öffnen. Es wird nicht deinit, weil WKWebView deinit nie aufrufen wird, da es sich selbst behält. – siuying

+0

Wie ich bereits erwähnt habe, wenn Sie addScriptMessageHandler in viewDidAppear aufgerufen haben, wird das umgekehrte removeScriptMessageHandlerForName in viewDidDisapper funktionieren. – siuying

13

Die Lösung von matt ist genau das Richtige. Dachte ich, es zu Objective-C-Code übersetzen würde

@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 

Dann nutzen Sie es so machen:

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