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")
}
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…