使用WKScriptMessageHandler时内存泄漏

时间:2015-06-27 22:20:09

标签: ios macos memory-leaks webkit javascriptcore

不确定我是否遇到了WebKit中的错误或我正在做一些可怕的错误,但我无法弄清楚如何使用WKScriptMessageHandler而不会导致WKScriptMessage.body中包含的任何值泄漏。

我能够将最小的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秒。

整个项目可以是downloaded here

知道发生了什么事吗?

4 个答案:

答案 0 :(得分:17)

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

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

您可以考虑将addScriptMessageHandler()移至viewWillAppear并在removeScriptMessageHandlerForName()中添加相应的viewWillDisappear来电。

答案 1 :(得分:6)

您所看到的是WebKit错误:https://bugs.webkit.org/show_bug.cgi?id=136140。它是fixed in WebKit trunk a while ago,但似乎没有合并到任何WebKit更新中。

您可以通过添加WKScriptMessage// // 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 来解决此问题,以补偿过度保留。它可能看起来像这样:

-fno-objc-arc

将它放在项目的Objective-C文件中,将此文件的编译器标志设置为包含 <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="WebApplication2.WebForm1" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <script> function addNumbers() { var firstNumber = parseFloat(document.getElementById("TextBox1").value); var secondNumber = parseFloat(document.getElementById("TextBox2").value); document.getElementById("TextBox3").value = firstNumber + secondNumber; } </script> </head> <body> <form id="form1" runat="server"> <div> <table> <tr> <td>First Number: </td> <td> <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox></td> </tr> <tr> <td>Second Number: </td> <td> <asp:TextBox ID="TextBox2" runat="server"></asp:TextBox></td> </tr> <tr> <td>Result: </td> <td> <asp:TextBox ID="TextBox3" runat="server"></asp:TextBox></td> </tr> <tr> <td></td> <td> <asp:Button ID="Button1" runat="server" Text="Add" OnClick="addNumbers()" /></td> </tr> </table> </div> </form> </body> </html> ,并且它应该为您处理泄漏。

答案 2 :(得分:4)

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

答案 3 :(得分:0)

要修复保留周期,您可以针对任何协议使用基于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];