我的视图控制器显示WKWebView。我安装了一个消息处理程序,一个很酷的Web Kit功能,允许从网页内部通知我的代码:
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) {
// ...
}
到目前为止一切都很好,但现在我发现我的视图控制器正在泄漏 - 当它应该被释放时,它不是:
deinit {
println("dealloc") // never called
}
看起来只是将自己安装为消息处理程序会导致保留周期,从而导致泄漏!
国王星期五像往常一样正确。事实证明,WKUserContentController保留了它的消息处理程序。这有一定意义,因为如果消息处理程序不再存在,它很难向消息处理程序发送消息。例如,它与CAAnimation保留其委托的方式并行。
但是,它也会导致保留周期,因为WKUserContentController本身正在泄漏。这本身并不重要(它只有16K),但保留周期和视图控制器的泄漏都很糟糕。
我的解决方法是在WKUserContentController和消息处理程序之间插入一个trampoline对象。 trampoline对象只有对真实消息处理程序的弱引用,因此没有保留周期。这是蹦床对象:
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)
}
}
现在,当我们安装消息处理程序时,我们安装了trampoline对象而不是self
:
self.wv.configuration.userContentController.addScriptMessageHandler(
LeakAvoider(delegate:self), name: "dummy")
有用!现在调用deinit
,证明没有泄漏。看起来这应该不起作用,因为我们创建了LeakAvoider对象并且从未对其进行过引用;但请记住,WKUserContentController本身保留它,所以没有问题。
为了完整性,现在调用deinit
,你可以在那里卸载消息处理程序,虽然我不认为这实际上是必要的:
deinit {
println("dealloc")
self.wv.stopLoading()
self.wv.configuration.userContentController.removeScriptMessageHandlerForName("dummy")
}
泄漏是由userContentController.addScriptMessageHandler(self, name: "handlerName")
引起的,它将保留对消息处理程序self
的引用。
要防止泄漏,只需在不再需要时通过userContentController.removeScriptMessageHandlerForName("handlerName")
删除消息处理程序。如果你在viewDidAppear
添加addScriptMessageHandler,最好在viewDidDisappear
中删除它。
由matt发布的解决方案正是我们所需要的。以为我会把它翻译成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
然后像这样使用它:
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"name"];
基本问题:WKUserContentController包含对添加到其中的所有WKScriptMessageHandler的强引用。您必须手动删除它们。
由于这仍然是Swift 4.2和iOS 11的问题,我想建议一个使用处理程序的解决方案,该处理程序与保存UIWebView的视图控制器分开。这样,视图控制器可以正常退出并告诉处理程序也可以清理。
这是我的解决方案:
UIViewController中:
import UIKit
import WebKit
class MyViewController: JavascriptMessageHandlerDelegate {
private let javascriptMessageHandler = JavascriptMessageHandler()
private lazy var webView: WKWebView = WKWebView(frame: .zero, configuration: self.javascriptEventHandler.webViewConfiguration)
override func viewDidLoad() {
super.viewDidLoad()
self.javascriptMessageHandler.delegate = self
// TODO: Add web view to the own view properly
self.webView.load(URLRequest(url: myUrl))
}
deinit {
self.javascriptEventHandler.cleanUp()
}
}
// MARK: - JavascriptMessageHandlerDelegate
extension MyViewController {
func handleHelloWorldEvent() {
}
}
转会:
import Foundation
import WebKit
protocol JavascriptMessageHandlerDelegate: class {
func handleHelloWorld()
}
enum JavascriptEvent: String, CaseIterable {
case helloWorld
}
class JavascriptMessageHandler: NSObject, WKScriptMessageHandler {
weak var delegate: JavascriptMessageHandlerDelegate?
private let contentController = WKUserContentController()
var webViewConfiguration: WKWebViewConfiguration {
for eventName in JavascriptEvent.allCases {
self.contentController.add(self, name: eventName.rawValue)
}
let config = WKWebViewConfiguration()
config.userContentController = self.contentController
return config
}
/// Remove all message handlers manually because the WKUserContentController keeps a strong reference on them
func cleanUp() {
for eventName in JavascriptEvent.allCases {
self.contentController.removeScriptMessageHandler(forName: eventName.rawValue)
}
}
deinit {
print("Deinitialized")
}
}
// MARK: - WKScriptMessageHandler
extension JavascriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// TODO: Handle messages here and call delegate properly
self.delegate?.handleHelloWorld()
}
}