Memory leak when using WKScriptMessageHandler

2020-05-20 00:15发布

问题:

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:

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?

回答1:

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.



回答2:

I've encountered same problem on iOS 9 SDK.

I noticed userContentController.addScriptMessageHandler(self, name: "handler") will keep the reference of the handler. To prevent leaks, simply remove the message handler when you no longer need it. e.g. when you dismiss the said controller, call a clean up method that will call removeScriptMessageHandlerForName().

You might consider move the addScriptMessageHandler() to viewWillAppear and add a corresponding removeScriptMessageHandlerForName() calls in viewWillDisappear.



回答3:

You've got a retain cycle here. In your code, ViewController retains WKWebView, WKWebView retains WKWebViewConfiguration, WKWebViewConfiguration retains WKUserContentController and your WKUserContentController retains your ViewController. Just like in comment above, you have to remove scriptHandler by calling removeScriptMessageHandlerForName, before closing your view controller.



回答4:

To fix a retain cycle you can use next common solution that is based on NSProxy for any protocols:

@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

And somewhere in your code you can write:

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