WKWebView导致我的视​​图控制器泄漏

mat*_*att 63 memory-leaks webkit ios ios8 wkwebview

我的视图控制器显示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) {
        // ...
}
Run Code Online (Sandbox Code Playgroud)

到目前为止一切都很好,但现在我发现我的视图控制器正在泄漏 - 当它应该被释放时,它不是:

deinit {
    println("dealloc") // never called
}
Run Code Online (Sandbox Code Playgroud)

看起来只是将自己安装为消息处理程序会导致保留周期,从而导致泄漏!

mat*_*att 128

国王星期五像往常一样正确.事实证明,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)
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,当我们安装消息处理程序时,我们安装trampoline对象而不是self:

self.wv.configuration.userContentController.addScriptMessageHandler(
    LeakAvoider(delegate:self), name: "dummy")
Run Code Online (Sandbox Code Playgroud)

有用!现在deinit被称为,证明没有泄漏.看起来这应该不起作用,因为我们创建了LeakAvoider对象并且从未对其进行过引用; 但请记住,WKUserContentController本身保留它,所以没有问题.

为了完整性,现在deinit调用了,你可以在那里卸载消息处理程序,虽然我不认为这实际上是必要的:

deinit {
    println("dealloc")
    self.wv.stopLoading()
    self.wv.configuration.userContentController.removeScriptMessageHandlerForName("dummy")
}
Run Code Online (Sandbox Code Playgroud)

  • 对我来说,除非我在viewWillDisappear中删除脚本消息处理程序,否则deinit实际上永远不会被调用.另外现在LeakAvoider被泄露了. (3认同)
  • 仍在试图理解_为什么_它不起作用。如果我的“WKUserContentController”保留了导致泄漏的消息处理程序(self),则不应使用“weak self”导致 ARC 不会增加我的“self”的引用计数。那么当 self 的另一个唯一引用停止指向它时,它应该被释放吗? (2认同)
  • 相当大的解决方案,只需在清理时调用userContentController.removeScriptMessageHandler(String),就可以了! (2认同)

小智 22

泄漏是由于userContentController.addScriptMessageHandler(self, name: "handlerName")它将保留对消息处理程序的引用self.

要防止泄漏,只需在userContentController.removeScriptMessageHandlerForName("handlerName")不再需要时删除消息处理程序.如果添加addScriptMessageHandler viewDidAppear,最好将其删除viewDidDisappear.

  • "当你不再需要它时"问题是:什么时候?理想情况下它会在您的视图控制器的`deinit`(Objective-C的`dealloc`),但它永远不会被调用,因为(等待它),我们正在泄漏!这是我的蹦床解决方案解决的问题.顺便说一下,同样的问题和同样的解决方案继续进入iOS 9. (3认同)

joh*_*han 18

由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
Run Code Online (Sandbox Code Playgroud)

然后像这样使用它:

WKUserContentController *userContentController = [[WKUserContentController alloc] init];    
[userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"name"];
Run Code Online (Sandbox Code Playgroud)


小智 5

我还注意到,您还需要在拆卸期间删除消息处理程序,否则处理程序仍将继续存在(即使有关 webview 的所有其他内容都已解除分配):

WKUserContentController *controller = 
self.webView.configuration.userContentController;

[controller removeScriptMessageHandlerForName:@"message"];
Run Code Online (Sandbox Code Playgroud)