2014-10-15 7 views
40

Il controller di visualizzazione visualizza una WKWebView. Ho installato un gestore di messaggi, una caratteristica fredda Kit Web che permette il mio codice da notificare dall'interno della pagina web:WKWebView causa perdite nel controller di visualizzazione

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

Fin qui tutto bene, ma ora ho scoperto che il mio controller di vista perde - quando si suppone essere deallocato, non è così:

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

sembra che solo l'installazione di me stesso come un gestore di messaggi provoca un ciclo e quindi una perdita di conservare!

risposta

80

Corretto come al solito, King Friday. Risulta che il WKUserContentController conserva il suo gestore di messaggi. Questo ha un certo senso, dal momento che potrebbe difficilmente inviare un messaggio al suo gestore di messaggi se il suo gestore di messaggi aveva cessato di esistere. Ad esempio, è parallelo al modo in cui un CAAnimation mantiene il proprio delegato.

Tuttavia, causa anche un ciclo di conservazione, poiché il WKUserContentController perde. Non importa molto da solo (è solo 16K), ma il ciclo di conservazione e la perdita del controller di visualizzazione sono pessimi.

La mia soluzione è interporre un oggetto trampolino tra WKUserContentController e il gestore di messaggi. L'oggetto trampolino ha solo un debole riferimento al vero gestore del messaggio, quindi non c'è un ciclo di conservazione. Ecco l'oggetto trampolino:

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

Ora, quando installiamo il gestore di messaggi, installiamo l'oggetto trampolino invece di self:

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

Funziona! Ora viene chiamato deinit, a dimostrazione dell'assenza di perdite. Sembra che questo non dovrebbe funzionare, perché abbiamo creato il nostro oggetto LeakAvoider e non abbiamo mai avuto un riferimento ad esso; ma ricorda, lo stesso WKUserContentController lo sta mantenendo, quindi non ci sono problemi.

Per completezza, ora che deinit si chiama, è possibile disinstallare il gestore di messaggi lì, anche se non credo che questo è effettivamente necessario:

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

Sorpreso upvote non superiore. Grande aiuto – Nick

+0

un'anima gentile può tradurre questo in codici equivalenti oggettivec? – mkto

+0

@mkto - Pubblicato una versione obj-c dell'implementazione. – johan

12

La perdita è causata da userContentController.addScriptMessageHandler(self, name: "handlerName") che manterrà un riferimento al gestore di messaggi self.

Per evitare perdite, è sufficiente rimuovere il gestore di messaggi tramite userContentController.removeScriptMessageHandlerForName("handlerName") quando non è più necessario. Se aggiungi addScriptMessageHandler a viewDidAppear, è consigliabile rimuoverlo in viewDidDisappear.

+0

"quando non ne hai più bisogno" Il problema è: quando è? Idealmente sarebbe nella tua vista 'deinit' (Objective-C' dealloc'), ma non viene mai chiamato perché (aspettalo) stiamo perdendo! Questo è il problema che la mia soluzione di tappeto elastico risolve. A proposito, questo stesso problema e questa stessa soluzione continuano in iOS 9. – matt

+0

Dipende davvero dal tuo caso d'uso. Diciamo che se lo si presenta tramite presentViewController, è il momento in cui lo si chiude. Quando lo si inserisce in un controller della vista di navigazione, l'ora è quando lo si apre. Non verrà deinito perché WKWebView non chiamerà mai deinit in quanto si sta conservando. – siuying

+0

Come ho già detto, se hai chiamato addScriptMessageHandler in viewDidAppear, l'opposto removeScriptMessageHandlerForName in viewDidDisapper funzionerà. – siuying

13

La soluzione pubblicata da matt è proprio ciò che è necessario. Ho pensato di tradurre al codice 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 

Poi fare uso di esso in questo modo:

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