使用 WKScriptMessageHandler 时内存泄漏
Memory leak when using WKScriptMessageHandler
不确定我是否遇到了 WebKit
中的错误,或者我做错了什么,但我不知道如何使用 WKScriptMessageHandler
而不导致 [=14] 中包含的任何值=] 泄漏。
我能够组建一个最小的 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 中打开内存调试器时,我看到以下泄漏:
如果我添加一个重新加载请求的按钮,并且这样做几十次,应用程序的内存占用量会不断增加,并在达到特定阈值后崩溃。在这个最小的示例中,崩溃可能需要一段时间,但在我每秒收到多条消息的应用程序中,崩溃只需不到 10 秒。
整个项目可以downloaded here.
知道发生了什么事吗?
您看到的是一个 WebKit 错误:https://bugs.webkit.org/show_bug.cgi?id=136140. It was fixed in WebKit trunk a while ago,但似乎没有合并到任何 WebKit 更新中。
您可以通过将 -dealloc
添加到 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
将其放入项目的 Objective-C 文件中,将此文件的编译器标志设置为包含 -fno-objc-arc
,它应该会为您解决泄漏问题。
我在 iOS 9 SDK 上遇到了同样的问题。
我注意到 userContentController.addScriptMessageHandler(self, name: "handler")
将保留处理程序的引用。为防止泄漏,只需在不再需要时删除消息处理程序即可。例如当您关闭所述控制器时,调用将调用 removeScriptMessageHandlerForName()
的清理方法。
您可以考虑将 addScriptMessageHandler()
移动到 viewWillAppear
并在 viewWillDisappear
.
中添加相应的 removeScriptMessageHandlerForName()
调用
这里有一个保留周期。
在您的代码中,ViewController 保留 WKWebView,WKWebView 保留 WKWebViewConfiguration,WKWebViewConfiguration 保留 WKUserContentController,您的 WKUserContentController 保留您的 ViewController。就像上面的评论一样,您必须在关闭视图控制器之前通过调用 removeScriptMessageHandlerForName 来删除 scriptHandler。
要修复保留周期,您可以对任何协议使用基于 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];
不确定我是否遇到了 WebKit
中的错误,或者我做错了什么,但我不知道如何使用 WKScriptMessageHandler
而不导致 [=14] 中包含的任何值=] 泄漏。
我能够组建一个最小的 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 中打开内存调试器时,我看到以下泄漏:
如果我添加一个重新加载请求的按钮,并且这样做几十次,应用程序的内存占用量会不断增加,并在达到特定阈值后崩溃。在这个最小的示例中,崩溃可能需要一段时间,但在我每秒收到多条消息的应用程序中,崩溃只需不到 10 秒。
整个项目可以downloaded here.
知道发生了什么事吗?
您看到的是一个 WebKit 错误:https://bugs.webkit.org/show_bug.cgi?id=136140. It was fixed in WebKit trunk a while ago,但似乎没有合并到任何 WebKit 更新中。
您可以通过将 -dealloc
添加到 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
将其放入项目的 Objective-C 文件中,将此文件的编译器标志设置为包含 -fno-objc-arc
,它应该会为您解决泄漏问题。
我在 iOS 9 SDK 上遇到了同样的问题。
我注意到 userContentController.addScriptMessageHandler(self, name: "handler")
将保留处理程序的引用。为防止泄漏,只需在不再需要时删除消息处理程序即可。例如当您关闭所述控制器时,调用将调用 removeScriptMessageHandlerForName()
的清理方法。
您可以考虑将 addScriptMessageHandler()
移动到 viewWillAppear
并在 viewWillDisappear
.
removeScriptMessageHandlerForName()
调用
这里有一个保留周期。 在您的代码中,ViewController 保留 WKWebView,WKWebView 保留 WKWebViewConfiguration,WKWebViewConfiguration 保留 WKUserContentController,您的 WKUserContentController 保留您的 ViewController。就像上面的评论一样,您必须在关闭视图控制器之前通过调用 removeScriptMessageHandlerForName 来删除 scriptHandler。
要修复保留周期,您可以对任何协议使用基于 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];