iosmacosmemory-leakswebkitjavascriptcore

Memory leak when using WKScriptMessageHandler


Not sure if I hit a bug in WebKit or I am doing something horribly wrong, but I can't figure out how to use WKScriptMessageHandler without causing whatever value contained in WKScriptMessage.body to leak.

I was able to put together a minimum Mac project to isolate the issue, but to no avail.

In the main view controller:

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)
   }
}

And then in the index.html file:

<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>

When I run the project then open the memory debugger in Instruments, I see the following leak:

leak

If I add a button that reloads the request, and do so few dozen times, the memory footprint of the app keeps growing, and crashes after a certain threshold. It might take a while before crashing in this minimal example, but in my app where I receive several messages per second, it takes less than 10s to crash.

The whole project can be downloaded here.

Any idea of what's going on?


Solution

  • What you're seeing is a WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=136140. It was fixed in WebKit trunk a while ago, but does not appear to have been merged into any WebKit updates.

    You can work around this by adding a -dealloc to WKScriptMessage that compensates for the over-retain. It could look something like this:

    //
    //  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
    

    Drop this in an Objective-C file in your project, set the compiler flags for this file to contain -fno-objc-arc, and it should take care of the leak for you.