使用WKScriptMessageHandler时存在内存泄漏问题。

14

我不确定自己是在WebKit中遇到了一个bug还是自己做错了什么,但我无法弄清楚如何在不泄漏WKScriptMessage.body中包含的任何值的情况下使用WKScriptMessageHandler

我已经尝试创建了一个最简化的Mac项目来隔离这个问题,但是没有任何效果。

在主视图控制器中:

class ViewController: NSViewController {
  var webView: WKWebView?

  override func viewDidLoad() {
    super.viewDidLoad()
    let userContentController = WKUserContentController()
    userContentController.addScriptMessageHandler(self, name: "handler")
    let configuration = WKWebViewConfiguration()
    configuration.userContentController = userContentController
    webView = WKWebView(frame: CGRectZero, configuration: configuration)
    view.addSubview(webView!)

    let path = NSBundle.mainBundle().pathForResource("index", ofType: "html")
    let url = NSURL(fileURLWithPath: path!)!
    webView?.loadRequest(NSURLRequest(URL: url))
  }
}

extension ViewController: WKScriptMessageHandler {
  func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
     print(message.body)
   }
}

然后在 index.html 文件中:

<html>
  <head></head>
  <body>
    <script type="text/javascript">
      webkit.messageHandlers.handler.postMessage("Here's a random number for you: " + Math.random() * 10)
    </script>
  </body>
</html>

当我运行该项目并在Instruments中打开内存调试器时,我看到以下泄漏:

leak

如果我添加了一个重新加载请求的按钮,并且这样做几十次,应用程序的内存占用量会持续增长,并在某个阈值之后崩溃。在这个最小示例中可能需要一段时间才能崩溃,但在我的应用程序中,我每秒收到几条消息,它少于10秒就会崩溃。

整个项目可以在此处下载

有什么想法是怎么回事吗?

4个回答

31

我在 iOS 9 SDK 上遇到了同样的问题。

我注意到 userContentController.addScriptMessageHandler(self, name: "handler") 会保留处理程序的引用。为了防止内存泄漏,当您不再需要它时,请简单地删除该消息处理程序。例如,当您关闭该控制器时,调用一个清理方法,该方法将调用 removeScriptMessageHandlerForName()

您可以考虑将 addScriptMessageHandler() 移动到 viewWillAppear 中,并在 viewWillDisappear 中添加相应的 removeScriptMessageHandlerForName() 调用。


4
原问题涉及消息正文传递的价值被泄露。这个回答强调了WKUserContentController会保留已注册处理程序直到被删除,这是很重要的知识,但与消息正文的泄漏无关。这是个好答案,但是回答了另一个问题。 - bdash

8
你所看到的是一个WebKit的bug:https://bugs.webkit.org/show_bug.cgi?id=136140。这个问题在WebKit trunk中已经修复了一段时间,但似乎没有合并到任何WebKit更新中。
你可以通过为WKScriptMessage添加-dealloc来解决这个问题,以弥补过度保留的问题。代码可能如下所示:
//
//  WKScriptMessage+WKScriptMessageLeakFix.m
//  TestWebkitMessages
//
//  Created by Mark Rowe on 6/27/15.
//  Copyright © Mark Rowe.
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
//  associated documentation files (the "Software"), to deal in the Software without restriction,
//  including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
//  and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
//  subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all copies or substantial
//  portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
//  LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
//  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
//  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#import <mach-o/dyld.h>
#import <objc/runtime.h>
#import <WebKit/WebKit.h>

// Work around <https://webkit.org/b/136140> WKScriptMessage leaks its body

@interface WKScriptMessage (WKScriptMessageLeakFix)
@end

@implementation WKScriptMessage (WKScriptMessageLeakFix)

+ (void)load
{
    // <https://webkit.org/b/136140> was fixed in WebKit trunk prior to the first v601 build being released.
    // Enable the workaround in WebKit versions < 601. In the unlikely event that the fix is backported, this
    // version check will need to be updated.
    int32_t version = NSVersionOfRunTimeLibrary("WebKit");
    int32_t majorVersion = version >> 16;
    if (majorVersion > 600)
        return;

    // Add our -dealloc to WKScriptMessage. If -[WKScriptMessage dealloc] already existed
    // we'd need to swap implementations instead.
    Method fixedDealloc = class_getInstanceMethod(self, @selector(fixedDealloc));
    IMP fixedDeallocIMP = method_getImplementation(fixedDealloc);
    class_addMethod(self, @selector(dealloc), fixedDeallocIMP, method_getTypeEncoding(fixedDealloc));
}

- (void)fixedDealloc
{
    // Compensate for the over-retain in -[WKScriptMessage _initWithBody:webView:frameInfo:name:].
    [self.body release];

    // Call our WKScriptMessage's superclass -dealloc implementation.
    [super dealloc];
}

@end

将此代码段放入您的项目中的一个Objective-C文件中,将该文件的编译器标志设置为包含-fno-objc-arc,它会为您处理泄漏问题。


太棒了!非常感谢! - Reda Lemeden

4

你这里有一个保留循环。

在你的代码中,ViewController 保留了 WKWebView,WKWebView 保留了 WKWebViewConfiguration,WKWebViewConfiguration 保留了 WKUserContentController,而你的 WKUserContentController 又保留了 ViewController。就像上面的评论一样,在关闭视图控制器之前,你必须通过调用 removeScriptMessageHandlerForName 来删除 scriptHandler。


1
为了解决保留循环问题,您可以使用基于NSProxy的常见解决方案来处理任何协议:
@interface WeakProxy: NSProxy

@property (nonatomic, weak) id object;

@end

@implementation WeakProxy

+ (instancetype)weakProxy:(id)object {
    return [[WeakProxy alloc] initWithObject:object];
}

- (instancetype)initWithObject:(id)object {
    self.object = object;
    return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [self.object methodSignatureForSelector:selector];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.object];
}

@end

在你的代码中,你可以写下:

let proxy = (id<WKScriptMessageHandler>)[WeakProxy weakProxy:self];
[configuration.userContentController addScriptMessageHandler:proxy name:KLoginResponseHandler];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接