Lei's Blog

深入解析WebViewJavascriptBridge

正文前的补充

在进行WebViewJavascriptBridge源码解析前,我们首先要了解iOS中两种web容器UIWebView, WKWebView,以及他们各自和JS的交互方式

UIWebView

UIWebView从iOS2 开始就作为App内展示Web内容的容器,但是长久以来一直遭受开发者的诟病;系统级的内存泄露、极高内存峰值、较差的稳定性、Touch Delay以及Javascript的运行性能及通信限制等等。在iOS12以后已经标记为Deprecated不再维护

UIWebView基本用法相信对于很多小伙们来说,用text文档都能写出来,这里也非本文的重点,这里重点说UIWebViewJS的交互方式

UIWebView OC调用JS

  • 调用stringByEvaluatingJavaScriptFromString方法
    这也是iOS开发早期最常用的方法,例如
1
self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];

虽然代码较为方便,但是有两个缺点

  1. 返回值单一,是string类型,如果返回其它数据类型,还要转换,不够灵活
  2. js报错和无返回值都返回nil,无法区分
  • JavaScriptCore(iOS 7.0 +)

JavascriptCore 一直作为WebKit中内置的JS引擎使用,在iOS7之后,Apple对原有C/C++代码进行了OC封装,成为了系统级的framewok提供给开发者使用,在页面加载完成之后,我们可以通过[self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]KVC的方式拿到JS执行的上下文JSContext,然后通过当前JSContext执行evaluateScript方法,达到调用JS的目的,例如

1
2
3
4
5
6
7
//获取该UIWebView的javascript上下文
JSContext *jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//这也是一种获取标题的方法。
JSValue *value = [self.jsContext evaluateScript:@"document.title"];
//更新标题
self.navigationItem.title = value.toString;

注意这里的JSValue可以是JavaScript的ArrayNumberString对象等数据类型,该方法就解决了stringByEvaluatingJavaScriptFromString返回值单一的问题。

如果执行了一个不存在的方法, 例如

1
[self.jsContext evaluateScript:@"document.xxx"];

那么肯定会报错,可以通过JSContext上的exceptionHandler属性来捕获异常。

1
2
3
4
5
6
//在调用前,设置异常回调
[self.jsContext setExceptionHandler:^(JSContext *context, JSValue *exception){
NSLog(@"%@", exception);
}];
//执行方法
JSValue *value = [self.jsContext evaluateScript:@"document.xxx"];

该属性解决了stringByEvaluatingJavaScriptFromString调用js方法后出现错误,捕获不到的缺点

JS调用OC

1 通过设置 URL Scheme 的方式

(注) WebViewJavascriptBridge就是采用这种方式

比如在html文件中有一个a标签,a标签在前端开发中可以配置并跳转到一个新的链接,我们正是利用跳转时触发UIWebViewDelegate协议里的shouldStartLoadWithRequest方法进行拦截,示例如下

1
2
// H5
<a href="darkangel://smsLogin?username=12323123&code=892845">短信验证登录</a>
1
2
3
4
5
6
7
8
9
10
11
12
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//标准的URL包含scheme、host、port、path、query、fragment等
NSURL *URL = request.URL;
if ([URL.scheme isEqualToString:@"darkangel"]) {
if ([URL.host isEqualToString:@"smsLogin"]) {
NSLog(@"短信验证码登录,参数为 %@", URL.query);
return NO;
}
}
return YES;
}

当点击按钮时,控制台就会打印出username=12323123&code=892845如果是复杂的参数,可以采用Json格式

JavaScriptCore(iOS 7.0 +)

第二种方式利用JavaScriptCore,还是需要页面加载完成,获取js上下文,然后进行方法映射代码示例如下

先定义一个分享的方法

1
2
3
function share(title, imgUrl, link) {
//这里需要OC实现
}

OC代码实现如下

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
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//将js的function映射到OC的方法
[self convertJSFunctionsToOCMethods];
}
- (void)convertJSFunctionsToOCMethods
{
//获取该UIWebview的javascript上下文
//self持有jsContext
//@property (nonatomic, strong) JSContext *jsContext;
self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//js调用oc
//其中share就是js的方法名称,赋给是一个block 里面是oc代码
//此方法最终将打印出所有接收到的参数,js参数是不固定的
self.jsContext[@"share"] = ^() {
NSArray *args = [JSContext currentArguments];//获取到share里的所有参数
//args中的元素是JSValue,需要转成OC的对象
NSMutableArray *messages = [NSMutableArray array];
for (JSValue *obj in args) {
[messages addObject:[obj toObject]];
}
NSLog(@"点击分享js传回的参数:\n%@", messages);
};
}

上面的示例是同步,且无回调的,下面示例是异步接受回调,比如H5页面分享按钮,调用之后要从原生得到结果,分享成功还是失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//声明
function share(shareData) {
var title = shareData.title;
var imgUrl = shareData.imgUrl;
var link = shareData.link;
var result = shareData.result;
//do something
//这里模拟异步操作
setTimeout(function(){
//2s之后,回调true分享成功
result(true);
}, 2000);
}
//调用的时候需要这么写
share({
title: "title",
imgUrl: "http://img.dd.com/xxx.png",
link: location.href,
result: function(res) { //函数作为参数
console.log(res ? "success" : "failure");
}
});

JS代码释义, 先定义一个share函数,在调用函数时启动一个延时2秒执行的方法此写法类似于OC的block

OC实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//异步回调
self.jsContext[@"share"] = ^(JSValue *shareData) { //首先这里要注意,回调的参数不能直接写NSDictionary类型,为何呢?
//仔细看,打印出的确实是一个NSDictionary,但是result字段对应的不是block而是一个NSDictionary
NSLog(@"%@", [shareData toObject]);
//获取shareData对象的result属性,这个JSValue对应的其实是一个javascript的function。
JSValue *resultFunction = [shareData valueForProperty:@"result"];
//回调block,将js的function转换为OC的block
void (^result)(BOOL) = ^(BOOL isSuccess) {
[resultFunction callWithArguments:@[@(isSuccess)]];
};
//模拟异步回调
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"回调分享成功");
result(YES);
});
};

此代码的执行结果是将在2s中打印一个sucess由js触发,3s中打印一个sucess由oc触发

WKWebView

在iOS8中,Apple引入了新一代的WebKit framework,同时提供了WKWebView用来替代传统的UIWebView。它更加的稳定、拥有60fps滚动刷新率、丰富的手势、KVO、高效的Web和Native通信,默认进度条等等功能,而最重要的,是使用了和safari相同的Nitro引擎极大提升了Javascript的运行速度。WKWebView独立的进程管理,也降低了内存占用及Crash对主App的影响。

OC -> JS

WKWebView提供一个类似于JavaScriptCore的方法

1
2
//执行一段js,并将结果返回,如果出错,error则不为空
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id result, NSError * _Nullable error))completionHandler;

该方法很好的解决了UIWebVIew使用stringByEvaluatingJavaScriptFromString方法的两个缺点(1.返回值单一 2.报错无法捕获),例如想获取某页面的title

1
2
3
[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
NSLog(@"调用evaluateJavaScript异步获取title:%@", title);
}];

JS -> OC

URL Scheme 拦截

这里与WebViewURL拦截方法一致,只是拦截的代理方法发生了变化例如

1
2
// js
<a href="darkangel://smsLogin?username=12323123&code=892845">短信验证登录</a>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
//可以通过navigationAction.navigationType获取跳转类型,如新链接、后退等
NSURL *URL = navigationAction.request.URL;
//判断URL是否符合自定义的URL Scheme
if ([URL.scheme isEqualToString:@"darkangel"]) {
//根据不同的业务,来执行对应的操作,且获取参数
if ([URL.host isEqualToString:@"smsLogin"]) {
NSString *param = URL.query;
NSLog(@"短信验证码登录, 参数为%@", param);
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
decisionHandler(WKNavigationActionPolicyAllow);
NSLog(@"%@", NSStringFromSelector(_cmd));
}

scriptMessageHandler

这是Apple在WebKit里新增加的方法,位于WKUserContentController.h。在此处只举例,不作详细介绍

1
[controller addScriptMessageHandler:self name:@"currentCookies"]; //这里self要遵循协 WKScriptMessageHandler

在js中调用下面的方法时

1
window.webkit.messageHandlers.currentCookies.postMessage(document.cookie);

OC中将会收到WKScriptMessageHandler的回调

1
2
3
4
5
6
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"currentCookies"]) {
NSString *cookiesStr = message.body; //message.body返回的是一个id类型的对象,所以可以支持很多种js的参数类型(js的function除外)
NSLog(@"当前的cookie为: %@", cookiesStr);
}
}

切记要在适当的地方调用 removeScriptMessageHandler

1
2
3
4
- (void)dealloc {
//记得移除
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"currentCookies"];
}

这样WKWebView就完成了一次完整的 JS->OC的交互


正文

在进行探讨前,先讲解两个代码中的小技巧
#define __wvjb_js_func__(x) #x 宏定义中#号的意思是将参数字符传化,例如

1
2
3
#define example1(instr) #instr
string str=example1(abc); 将会展成:string str="abc"

static NSString * preprocessorJSCode = @__wvjb_js_func__()等价于
static NSString * preprocessorJSCode = @""

JS;(function() {}函数前面跟一个一元表达式会立即执行后面的匿名函数等价于(function(){})()

在这篇文章中,我们将以QA的方式来探讨WebViewJavascriptBridge的原理及实现方式,某些代码可能需要一些JS基础,没有也没关系,我会尽量多的添加注释,让看过本篇文章的人有所收获,达到共同进步的目的

1 WebViewJavascriptBridge如何初始化,并注入JS代码

iOS使用WebViewJavascriptBridge做桥接的时候,需要在前端引入如下代码

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
function setupWebViewJavascriptBridge(callback) {
// 如果window对象上有WebViewJavascriptBridge这个属性的话,会执行callback方法,并return
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
// 如果window上有WVJBCallbacks属性,会将callback(匿名函数)添加到WVJBCallbacks数组中并返回
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
// 为window 属性添加一个WVJBCallbacks属性
window.WVJBCallbacks = [callback];
// 创建一个iframe标签,并设置src
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge(function(bridge) {
/* Initialize your app here */
bridge.registerHandler('JS Echo', function(data, responseCallback) {
console.log("JS Echo called with:", data)
responseCallback(data)
})
bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
console.log("JS received response:", responseData)
})
})

上述代码的意思是, 定义一个setupWebViewJavascriptBridge函数, 然后调用这个函数,并传入一个匿名函数作为参数,在JS中,匿名函数作为参数,匿名函数并不会立即执行,而是在显式调用下才会执行,例如

1
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }

初次调用setupWebViewJavascriptBridge,会创建一个WVJBCallbacks属性挂载到window对象上,因为WebViewJavascriptBridge这个属性在执行iframe标签之后才会被创建, 所以WVJBCallbacks的作用就是记录调用setupWebViewJavascriptBridge时的匿名函数参数,数组里就是这个东东

1
2
3
4
5
6
7
8
9
10
11
12
function(bridge) {
/* Initialize your app here */
bridge.registerHandler('JS Echo', function(data, responseCallback) {
console.log("JS Echo called with:", data)
responseCallback(data)
})
bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
console.log("JS received response:", responseData)
})
}

然后创建一个iframe标签,并设置src属性的值为https://__bridge_loaded__,此时会触发WebView代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 代码有删减
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}

当URL包含__bridge_loaded__时,会执行WebViewJavascriptBridge_JS里的JS代码,生成WebViewJavascriptBridge属性,并挂载到window上,代码如下

1
2
3
4
5
6
7
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};

为了将存储在WVJBCallbacks里的函数注册到WebViewJavascriptBridge属性中,
WebViewJavascriptBridge_JS会开一个异步方法,遍历并执行WVJBCallbacks里的函数,并传入WebViewJavascriptBridge对象

1
2
3
4
5
6
7
8
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}

至此你应该明白,注册事件的函数虽然写在了前端页面的JS里,但实际上是在SDK里注册的。

_disableJavascriptAlertBoxSafetyTimeout不要轻易调用,如果调用这个方法,那么处理OC调用JS方法_dispatchMessageFromObjC里的处理逻辑就变成了同步执行,逻辑复杂的话容易发生阻塞。

2JS怎么调用Native注册的函数,如何拿到返回值

JS调用OC是采用修改iframesrc属性的方式触发web代理方法,我们以github上的代码为例

1
2
3
bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
console.log("JS received response:", responseData)
})

先看callHandler方法的内部实现

1
2
3
4
5
6
7
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}

此处会先对参数的长度作判断,假如参数长度等2, 并且data是一个function的话,会将data赋值给responseCallback,因为JS参数比较灵活,这样写是为了避免没有参数,但是有回调时发生错误
然后将 handlerNamedata封装成一个对象,连同responseCallback一起传递给_doSened函数

1
2
3
4
5
6
7
8
9
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

_doSend函数里,会先判断有没有回调函数,如果有的话在利用时间戳和定义的无符整形生成一个callbackID,然后将回调函数以Key(callbackID) Value(responseCallback)的形式存入到responseCallbacks对象中,同时也将callbackId存入message对象,这么做的目的是为了函数的调用和回调能唯一对应。

message对象插入到全局的数组sendMessageQueue中,更改iframe的src属性为https://__wvjb_queue_message__,此时又触发了web的代理方法,为了简化,我们只贴出关键代码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
// 代码有改动
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
[self WKFlushMessageQueue];
}
- (void)WKFlushMessageQueue {
[_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
[_base flushMessageQueue:result];
}];
}

这里有一个比较精妙的思想,JS通过iframe通知Native我有代码调用,但是并没告我Native是触发的那个方法,需要Native自己主动去获取,就是下面这个方法

1
2
3
4
// oc
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
1
2
3
4
5
6
// js
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}

我们之前调用_doSend的时候将message放入到了sendMessageQueue中,在此处将它取出来,并将数组重新置空,来保证每次取值的唯一性

在此处客户端已经获取到了JS调用的事件名,以及参数,以及callbackID都在message对象中,下一步就是解析这个对象,也就是调用WebViewJavascriptBridgeBase里的flushMessageQueue函数

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
48
- (void)flushMessageQueue:(NSString *)messageQueueString{
// 代码有删减
// 先返序列化出messages 对象
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
if (responseId) {
// OC 调用 JS 时回调使用
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
handler(message[@"data"], responseCallback);
}
}
}

此处的代码逻辑分两部分 , 一部分判断responseId,这是 OC调用JS有回调所要用到的,此处暂且不提,我们关心的是callbackId的判断逻辑

当没有callbackId时,证明没有回调,只需从本地messageHandlers取出对应的handler,也就是block,并执行就可以了

当有callbackId时, 在本地创建一个responseCallback,并将它当做参数传递到handler中,当handle代码执行完,之后会调用responseCallback,就会执行如下代码

1
2
3
4
5
6
7
8
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
}

block内部会重建一个对象, 最终会调用WebViewJavascriptBridge_handleMessageFromObjC属性,注意此处是在主线程进行

1
2
3
4
5
6
7
8
9
10
11
12
13
function _doDispatchMessageFromObjC() {
// 代码有删减
var message = JSON.parse(messageJSON);
var responseCallback;
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
}
}

此处的代码逻辑与NativeflushMessageQueue方法相对应,用responseIdresponseCallbacks取出之前存入的回调函数, 执行并将responseId当参数传递过去,然后从responseCallbacks对象中删除responseId

1
2
3
bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
至此我们就能在这里愉快的打印responseData
})

Native如何调用JS,并拿到返回值

Native调用JS相对简单,可以直接执行evaluateJavaScriptWKWebView中,但是拿到回调值却要复杂一些。示例

1
2
[weakSelf.bridge callHandler:@"jsCallBack" data:jsonParameter responseCallback:^(id responseData) {
}];

Nativebridge调用callHandler时,会执行WebViewJavascriptBridgeBase类里的sendData方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];
if (data) {
message[@"data"] = data;
}
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}

这里与JS调用Native一样,同样是将参数和事件名封装成一个对象,然后生成一个callbackId,并将回调函数存入全局的responseCallbacks

然后直接调用WebViewJavascriptBridge对象的_handleMessageFromObjC属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function _doDispatchMessageFromObjC() {
// 代码有删减
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}

JS调用Native一样, 如果没有callbackId,则直接从全局的messageHandlers对象中,取出函数执行,并传入message.data作为参数,此时 OC调用JS就达到了目的,但是还要考虑有回调函数的情况,也就是有callbackId

定义一个匿名函数,匿名函数内实现JS调用Native_doSend方法,我们前面已经介绍过这个方法,将匿名函数当做参数传递给handler例如handler就是下面的JS Echo

1
2
3
4
bridge.registerHandler('JS Echo', function(data, responseCallback) {
console.log("JS Echo called with:", data)
responseCallback(data)
})

那么执行完responseCallback(data)就会将结果传递到

1
2
3
4
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}

responseCallback匿名函数中去,此时会执行一次JS调用OC的过程,最终会执行在WebViewJavascriptBridgeBase类里的flushMessageQueue方法里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)flushMessageQueue:(NSString *)messageQueueString{
// 代码有删减
// 反序列化
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
}
}
}

此处会根据responseId从本地的_responseCallbacks取出一个block并执行,同时将 responseData传递进去,并从全局对象_responseCallbacks移除当前responseId

至此 Native调用JS流程也讲解完毕

结语

  • 本文大多采用代码片段来描述代码执行过程,想要了解真正的精髓还需仔细研究源码
  • 文章写的比较仓促,文中难免有误,欢迎发现错误的同学批评指正。

    打个小广告,在下篇文章中,我将介绍一个提升Hybrid开发的神器,传送门在这里,也欢迎各位同学伸出发财小手帮我点个star,谢谢!