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!
The leak is caused by
userContentController.addScriptMessageHandler(self, name: "handlerName")
which will keep a reference to the message handlerself
.To prevent leaks, simply remove the message handler via
userContentController.removeScriptMessageHandlerForName("handlerName")
when you no longer need it. If you add the addScriptMessageHandler atviewDidAppear
, its a good idea to remove it inviewDidDisappear
.Basic problem: The WKUserContentController holds a strong reference to all WKScriptMessageHandlers that were added to it. You have to remove them manually.
Since this is still a problem with Swift 4.2 and iOS 11 I want to suggest a solution which is using a handler which is separate from the view controller that holds the UIWebView. This way the view controller can deinit normally and tell the handler to clean up as well.
Here is my solution:
UIViewController:
Handler:
The solution posted by matt is just what's needed. Thought I'd translate it to objective-c code
Then make use of it like this:
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:
Now when we install the message handler, we install the trampoline object instead of
self
: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: