Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WKWebView causes my view controller to leak

My view controller displays a WKWebView. I installed a message handler, a cool Web Kit feature that allows my code to be notified from inside the web page:

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 far so good, but now I've discovered that my view controller is leaking - when it is supposed to be deallocated, it isn't:

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

It appears that merely installing myself as a message handler causes a retain cycle and hence a leak!

like image 214
matt Avatar asked Oct 15 '14 12:10

matt


People also ask

How can I clear the contents of a UIWebView WKWebview?

To clear old contents of webview With UIWebView you would use UIWebViewDelegate 's - webViewDidFinishLoad: .

What is a WKWebview?

Overview. A WKWebView object is a platform-native view that you use to incorporate web content seamlessly into your app's UI. A web view supports a full web-browsing experience, and presents HTML, CSS, and JavaScript content alongside your app's native views.

What is WKUserContentController?

Overview. A WKUserContentController object provides a bridge between your app and the JavaScript code running in the web view. Use this object to do the following: Inject JavaScript code into webpages running in your web view. Install custom JavaScript functions that call through to your app's native code.


2 Answers

Correct as usual, King Friday. It turns out that the WKUserContentController retains its message handler. This makes a certain amount of sense, since it could hardly send a message to its message handler if its message handler had ceased to exist. It's parallel to the way a CAAnimation retains its delegate, for example.

However, it also causes a retain cycle, because the WKUserContentController itself is leaking. That doesn't matter much on its own (it's only 16K), but the retain cycle and leak of the view controller are bad.

My workaround is to interpose a trampoline object between the WKUserContentController and the message handler. The trampoline object has only a weak reference to the real message handler, so there's no retain cycle. Here's the trampoline object:

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

Now when we install the message handler, we install the trampoline object instead of self:

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

It works! Now deinit is called, proving that there is no leak. It looks like this shouldn't work, because we created our LeakAvoider object and never held a reference to it; but remember, the WKUserContentController itself is retaining it, so there's no problem.

For completeness, now that deinit is called, you can uninstall the message handler there, though I don't think this is actually necessary:

deinit {     println("dealloc")     self.wv.stopLoading()     self.wv.configuration.userContentController.removeScriptMessageHandlerForName("dummy") } 
like image 90
matt Avatar answered Sep 24 '22 15:09

matt


The leak is caused by userContentController.addScriptMessageHandler(self, name: "handlerName") which will keep a reference to the message handler self.

To prevent leaks, simply remove the message handler via userContentController.removeScriptMessageHandlerForName("handlerName") when you no longer need it. If you add the addScriptMessageHandler at viewDidAppear, its a good idea to remove it in viewDidDisappear.

like image 35
siuying Avatar answered Sep 23 '22 15:09

siuying