聊一聊Hybrid的基石
Hybrid开发模式在拥有便利性、动态性等Web开发优势的同时,还能极大程度地利用Native的能力。因此无论是BAT等大厂,还是开源社区都拥有成熟的Hybrid解决方案。而整个Hybrid方案最底层、最核心的技术就是JS和Native的互相通信,本文就主要来扒一扒这其中的原理。
Bridge
JS和Native毕竟是两种完全不同的语言,他们是不能直接互相调用的。我们把JS和Native之间调用转换的组件叫做Bridge。Bridge架起了JS和Native调用的桥梁,其核心功能是:
- 在己端注册功能,供对端调用,也就是Native侧注册供JS侧调用的函数,JS侧注册供Native调用的函数
- 从己端发起对对端已注册功能的调用,也就是在JS侧发起对Native已注册功能的调用,Native侧发起对JS已注册功能的调用
所以Bridge其实是由两部分组成的。
- JSBridge:负责Bridge在JS侧的逻辑
- NativeBridge:负责Bridge在Native侧的逻辑
示意如下:
WebViewJavascriptBridge是github上开源的JS和Native通信的框架。业界Hybrid解决方案或多或少都得到过该框架的启发,有的甚至直接将该框架作为解决方案的底层。我们接下来以WebViewJavascriptBridge
为基础,详细解读Hybrid通信机制。
JS -> Native通信(CallNative)
首先来看JS是如何调用Native的功能的。调用之前需要NativeBridge先注册功能,示例代码注册了名为testObjcCallback
的功能函数,然后JS端发起了对testObjcCallback
的调用。
// Native端,注册功能函数
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
// JS端,调用Native功能
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
Native功能函数是可以自定义,所以理论上来说,JS端可以调用任何的Native方法,想象空间巨大,当然安全隐患也很大。
NativeBridge注册功能
NativeBridge以handlerName
为key,将用户自定义的功能存放在名为messageHandlers
的字典,这是最为普通不过的注册了,没什么可多说的。
// base bridge定义
@interface WebViewJavascriptBridgeBase : NSObject
......
@property (strong, nonatomic) NSMutableDictionary* messageHandlers;
......
@end
// 注册handler函数
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
JSBridge调用功能
通过上面的Demo,我们知道JS是通过和Native约定好的handlerName
来调用相应的Native功能的。那么问题来了:
- 如何实现JS到Native的调用?
- 如何通过
handleName
找到对应的Native功能函数的? - 调用Native功能的参数是如何传递的?
- 如果有回调的话,Native是怎么回调JS函数的?
如何实现JS到Native的调用?
这个问题是挺核心的一个问题,应该也是整个通信机制里面最有技术含量的。
我们知道UIWebViewDelegate
是UIWebView的委托,页面内容加载会触发相应的回调。UIWebViewDelegate
里面有这样一个回调函数webView:shouldStartLoadWithRequest:navigationType:
,苹果对这个回调函数的说明是:
Sent before a web view begins loading a frame.
注:(对于IOS 8之后的WKWebView可以选用webView:decidePolicyForNavigationAction:decisionHandler:
,原理是一样的)
也就是说,该回调会在webview的frame加载之前被调用。
于是乎,
JSBridge创建了一个空的iframe,插入到正常的网页中,当需要调用Native的时候,就给iframe的src赋值,然后就会触发系统的webView:shouldStartLoadWithRequest:navigationType:
回调。注意,这里的src不是随便赋值的,而是特殊定义的一个URL,这个很重要。
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
// 定义用于消息传递的iframe
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
接下来我们也能够想到NativeBridge会在webView:shouldStartLoadWithRequest:navigationType:
判断URL是不是约定好的特殊的URL,如果是的话,说明此次页面加载并不是正常页面加载,而是一次JS对Native的调用。
#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage @"__wvjb_queue_message__"
- (BOOL)isQueueMessageURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage];
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
......
// 判断是不是约定好的用于消息调用的URL
if ([_base isQueueMessageURL:url]) {
// 获取所有的调用消息
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
// 根据调用消息发起相应的Native调用
[_base flushMessageQueue:messageQueueString];
......
// 不加载iframe的真实内容
return NO;
}
......
}
简单总结一下,JS并不能直接对Native发起调用,而是通过对iframe的src赋值,触发系统的webView:shouldStartLoadWithRequest:navigationType:
回调,让Native能有机会处理。
从上面的原理我们可以看出JS对Native的调用都是异步的。
剩余三个问题
好,啃完硬骨头后面就比较轻松了,剩余的三个问题一块秒掉。
首先我们来看下JSBridge的调用逻辑。
// JS调用Native的API
function callHandler(handlerName, data, responseCallback) {
......
// 构造发往Native的消息`{ handlerName:handlerName, data:data }`
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
// 往Native发送调用消息
function _doSend(message, responseCallback) {
if (responseCallback) {
// 构造回调id,并将回调保存在responseCallbacks中等待被调用
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
// 通过特殊的url发起对Native的调用
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
JS在调用Native之前构造消息,消息的格式如下:
- handlerName: 解决 如何通过
handleName
找到对应的Native功能函数的? - data:解决 调用Native功能的参数是如何传递的?
- callbackId:解决 如果有回调的话,Native是怎么回调JS函数的?
但Native怎么收到这个消息呢?
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
UIWebView是提供了stringByEvaluatingJavaScriptFromString:
来执行JS代码的,Native通过这个函数获得了JS构造的消息列表。这个函数在后面Native -> JS通信中也起到的关键作用。
注:iOS 8.0之后的WKWebView使用的是evaluateJavaScript:completionHandler:
。
拿到消息数据,后面一切就好办了。
剩下还值得关注的是回调的实现。Native根据JS告知的callbackId
,构造了传递给JS的WVJBMessage
,然后在适当的时机CallJS,实现回调,JSBridge会根据callbackId
,调用正确的回调函数,有兴趣的童鞋可以自己再挖一挖代码。
- (void)flushMessageQueue:(NSString *)messageQueueString{
......
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
......
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
// 构造回调函数,作为native功能函数
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
// callbackId能让JS在responseCallbacks中找到相应的回调执行
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
// 调用JS回调
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
// 根据名字找到相应的功能函数。
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
......
// 调用功能
handler(message[@"data"], responseCallback);
}
}
Native > JS 通信(CallJS)
同样的,Native调用JS的功能,也需要JS先注册好功能函数。不多说,上示例代码。
// JS端,注册功能函数
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha!' }
log('JS responding with', responseData)
responseCallback(responseData)
})
// Native端,调用功能函数
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];
首先向JSBridge
中注册testJavascriptHandler
JS功能函数,然后Native端发起对testJavascriptHandler
的调用。
JS功能函数是可以自定义的,所以理论上来说,Native端可以调用任何的JS方法,其实Native端是可以执行任意的JS的。
JSBridge注册功能
var messageHandlers = {};
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
这简直就跟NativeBridge注册功能是一毛一样啊。
NativeBridge调用功能
Okay,相同的,再来四个问题。
- 如何实现Native到JS的调用?
- 如何通过
handleName
找到对应的JS功能函数的? - 调用JS功能的参数是如何传递的?
- 如果有回调的话,JS是怎么回调Native函数的?
如何实现Native到JS的调用?
Native调用JS要比JS调用Native方便的多得多,苹果的已经给我们提供了现成的解决方案,之前我们也提及过,就是:
UIWebView的stringByEvaluatingJavaScriptFromString:
注:WKWebView对应的是evaluateJavaScript:completionHandler:
所以,Native调用JS的核心逻辑如下:
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
}
剩余三个问题
分析CallNative的时候,解决这三个问题靠的是消息传递,这么好用的招数,肯定也被移植到callJS上来了。
是不是又是一毛一样呢?
- handlerName: 解决 如何通过
handleName
找到对应的JS功能函数的? - data:解决 调用JS功能的参数是如何传递的?
- callbackId: 解决 如果有回调的话,JS是怎么回调Native函数的?
// Native调用JS的API
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
// 构造传递给JS的消息体
NSMutableDictionary* message = [NSMutableDictionary dictionary];
message[@"data"] = data;
message[@"handlerName"] = handlerName;
if (responseCallback) {
// 构造callbackId,并将回调的实现放在`responseCallbacks`中,等待被调用
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
[self _queueMessage:message];
}
当JSBridge收到Native的消息之后,就根据消息做类似的处理。
function _dispatchMessageFromObjC(messageJSON) {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
// 调用Native回调
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
// 根据handler名字找到相应的处理函数
var handler = messageHandlers[message.handlerName];
......
// 执行处理函数
handler(message.data, responseCallback);
......
}
总分总的结尾
从上述分析我们可以看到,通信的本质无非就是消息的传递,Hybrid的通信也是离不开这个本质的。无论是callNative,还是callJS的实现,都是通过将构造好的消息传递给对端,对端根据消息做出相应的动作。
其实难就难在两点:
- 如何实现消息的传递
- callNative曲线救国用了iframe
- callJS则是得益于苹果的API
- 如何通过消息来传递多种多样的信息
- 普通参数可以直接传
- 功能函数需要通过名字来传递
- 回调函数则需要通过回调id来传递