addScriptMessageHandler 内存泄露

今天使用 addScriptMessageHandlerWKWebView 注入方法给 js 调用时发现有内存泄露问题。

出现问题的代码

1
2
3
4
5
WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init];
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:self name:@"nativeProcess"];
webViewConfiguration.userContentController= userContentController;
_webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height) configuration:webViewConfiguration];

原因

调用 addScriptMessageHandleruserContentController 会 retain self。而 self 又间接 retain userContentController,形成了循环引用。

解决方案

搜了下,网上已经有解决方案了。后来在 cordova-plugin-wkwebview-engine 里也看到处理这个问题。这里记录下3个解决方案。

方案1

在适当的时候调用 removeScriptMessageHandlerForName 方法。这个方案缺点时是不好找到适当的时间点,比如该 viewController 被其他地方 dismiss 这时候就不好处理。

方案2

新建一个类来代理 self,这样 userContentController 就 retain 这个新类的对象,新类对象只是弱引用 self,这样循环引用就解开了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface CDVWKWeakScriptMessageHandler : NSObject <WKScriptMessageHandler>

@property (nonatomic, weak, readonly) id<WKScriptMessageHandler>scriptMessageHandler;

- (instancetype)initWithScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler;

@end
@implementation CDVWKWeakScriptMessageHandler

- (instancetype)initWithScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler
{
self = [super init];
if (self) {
_scriptMessageHandler = scriptMessageHandler;
}
return self;
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
[self.scriptMessageHandler userContentController:userContentController didReceiveScriptMessage:message];
}

@end
1
2
3
4
5
6
7
8
9
10
11
// CDVWKWebViewEngine.m
CDVWKWeakScriptMessageHandler *weakScriptMessageHandler = [[CDVWKWeakScriptMessageHandler alloc] initWithScriptMessageHandler:self];

WKUserContentController* userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:weakScriptMessageHandler name:CDV_BRIDGE_NAME];

WKWebViewConfiguration* configuration = [self createConfigurationFromSettings:settings];
configuration.userContentController = userContentController;

WKWebView* wkWebView = [[WKWebView alloc] initWithFrame:self.engineWebView.frame configuration:configuration];
self.engineWebView = wkWebView;

方案3

该方案跟方案2基本一样,不过新类集成 NSProxy,并把所有接收到的方法都转发给 self。该方案使用于所有类似场景,更具通用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//
// WeakProxy.h
//
// Created by Ashoka on 2020/5/30.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface WeakProxy : NSProxy

+ (instancetype)weakProxy:(id)object;

@property (nonatomic, weak) id object;

@end

NS_ASSUME_NONNULL_END

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//
// WeakProxy.m
//
// Created by Ashoka on 2020/5/30.
//

#import "WeakProxy.h"

@implementation WeakProxy

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

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

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

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

@end

1
2
3
4
5
WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init];
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[WeakProxy weakProxy:self] name:@"nativeProcess"];
webViewConfiguration.userContentController= userContentController;
_webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height) configuration:webViewConfiguration];