WKWebView - WKScriptMessageHandler 循环引用

Too young too naive…

之前项目中 H5 和 原生之间互通消息一直依赖于 Cordova,还没正式用过 WKScriptMessageHandler,前两天发现确实像网友所说的会引起循环引用,导致控制器没办法释放。🔽这篇文章写的挺清楚,我就不废话了…

【转载自】:addScriptMessageHandler 内存泄露 | The Catcher in the Rye


今天使用 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
25
26
27
28
29
30
31
32
33
34
35
@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
// 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
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//
// 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
//
// 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];

全文完


关于 WKWebView 的几篇文章:

WKWebView 基础篇
WKWebView 协议篇
WKWebView 实战篇
WKWebView Cookie 试错
WKWebView - WKScriptMessageHandler 循环引用

Demo

WebView 的 Demo