一. 概述
做客户端开发免不了要与WebView
打交道,特别是关于Hybrid App
,在H5
所占比重越来越大的布景下,一套好的WebView
与原生交互的API显得尤为重要,当然现在两头都有比较成熟的三方库进行支撑。比如Android端的JsBridge,iOS端的WebViewJavascriptBridge,可是关于其内部原理笔者一直一知半解,导致有时面对问题无从下手,最终决心剖析WebViewJavascriptBridge
的内部完成原理,一是提高自己的源码阅览水平,其次也希望对以后的作业有所协助。
二. 基本原理
下载WebViewJavascriptBridge
的源码后能够看到其文件并不多,分别对几个文件做简单的介绍,后边详细剖析其源码
-
WebViewJavascriptBridge_JS
: JS桥接文件,经过它完成JS环境的初始化,里边就一个C函数,回来的是JS办法。原生调用的JS办法与对应的办法回调都需求先在这儿边进行注册。 -
WKWebViewJavascriptBridge
与WebViewJavascriptBridge
:WKWebView
与UIWebView
对应的桥接文件。JS调用的原生办法与对应的办法回调都需求先在这儿边进行注册。 -
WebViewJavascriptBridgeBase
: 桥接基础文件。经过他完成对原生环境的初始化,以及对办法存储容器的初始化,当然还有对WebViewJavascriptBridge_JS
里边JS办法的调用。
三. 源码解析
大体了解了上面几个类的效果,咱们经过源码来剖析其内部的完成逻辑。咱们就以WebViewJavascriptBridge
Demo为例。
1. JS调用OC办法
(1) OC环境初始化与办法注册
如何完成JS调用OC办法呢,首要要对当时OC环境进行初始化
// ExampleWKWebViewController
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
....
// WKWebViewJavascriptBridge
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
....
// WKWebViewJavascriptBridge
- (void) _setupInstance:(WKWebView*)webView {
_webView = webView;
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
-
[WebViewJavascriptBridge bridgeForWebView:webView];
: 看这个办法的调用栈,能够明晰的看到其效果是初始化WKWebViewJavascriptBridge
,从而实例化其对应的WebViewJavascriptBridgeBase
,还有绑定各自的署理,最终完成初始化OC调用环境的意图。 -
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
: 假如要完成JS调用原生办法的意图,那么有必要对原生办法进行注册,这个便是对应的注册办法。咱们来看他内部做了什么:
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
很简单,只不过把当时的Block保存进了messageHandlers
这个字典中,以便等JS端调用时,经过办法称号来找到其对应的完成。
(2) JS环境初始化与办法触发
OC环境初始化与办法注册完成后,咱们来下JS环境的初始化
Demo中经过- (void)loadExamplePage:(WKWebView*)webView
办法加载网页到当时的webView,来看下ExampleApp.html
中的中心办法:
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
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) {
var uniqueId = 1
function log(message, data) {
var log = document.getElementById('log')
var el = document.createElement('div')
el.className = 'logLine'
el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
if (log.children.length) { log.insertBefore(el, log.children[0]) }
else { log.appendChild(el) }
}
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)
})
document.body.appendChild(document.createElement('br'))
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
e.preventDefault()
log('JS calling handler "testObjcCallback"')
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}
})
-
setupWebViewJavascriptBridge(callback)
是中心办法,webView加载html后会首要调用这个办法。这个办法需求一个参数callback
,也是一个函数。咱们来看这个办法:
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
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)
}
第一次加载网页时 window.WebViewJavascriptBridge
与 window.WVJBCallbacks
都是false,把window.WVJBCallbacks
赋值为包括callback的数组,此刻callback为一个函数,便是后边的function(bridge) ....
,接下来创立WVJBIframe
,你能够把它理解为一个空白页面,创立它的意图是设置src = 'https://__bridge_loaded__';
,
留意这个
src
特点很要害,当咱们设置一个网页的src
特点时,这个链接会被咱们OC端的webView所捕获,从而调用webView的署理办法- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
,
后边两句代码的的意思是加载当时空白页,以便触发OC的署理办法,然后立马移除。
- 接下来咱们去
WKWebViewJavascriptBridge
中看- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
这个署理办法阻拦到请求后做了什么。
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是否是__wvjb_queue_message__
或者__bridge_loaded__
,方才触发的URL是 __bridge_loaded__
会调用WebViewJavascriptBridgeBase
的- (void)injectJavascriptFile
办法。
- (void)injectJavascriptFile {
// 获取JS字符串
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
....
- (void) _evaluateJavascript:(NSString *)javascriptCommand {
[self.delegate _evaluateJavascript:javascriptCommand];
}
....
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
return NULL;
}
经过以上办法调用能够看到,最终是把WebViewJavascriptBridge_js();
JS办法字符串,经过办法 [_webView evaluateJavaScript:javascriptCommand completionHandler:nil]
注入到了webView中而且履行。从而达到初始化javascript环境的brige的效果。
- WebViewJavascriptBridge_js()办法解析
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
var messagingIframe;
// 要发送给原生的音讯列表
var sendMessageQueue = [];
// 存储注册在bridge的JS办法
var messageHandlers = {};
// 要跳转的URL
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
//JS办法回调
var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;
// OC调用的JS办法需求用它来进行注册
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
//JS调用OC的办法入口
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false;
}
// 要发送音讯给原生了
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
//触发webView 署理,解析JS 的message
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
// 把音讯转成json字符串
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
function _dispatchMessageFromObjC(messageJSON) {
....
}
// 原生会调用他,JS用它来达到音讯分发
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
我截选了一些要害代码,首要整个WebViewJavascriptBridge_js
是一个JS办法的履行,首要创立了JS端的WebViewJavascriptBridge
并赋值给了window
,咱们来看这个目标的构成:
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
-
registerHandler
:直接对应下面的registerHandler(handlerName, handler)
办法,经过它咱们把能被OC调用的JS办法进行注册,看它的完成也是比较简单的
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
把JS的办法完成以办法名handleName
保存在messageHandlers中。
-
callHandler
: 对应下面callHandler(handlerName, data, responseCallback)
办法,经过它咱们能够直接建议对OC办法的调用,具体调用逻辑咱们在下面进行剖析。 -
disableJavscriptAlertBoxSafetyTimeout
:回调是否超时开关,默以为false -
_fetchQueue
: 把javascript环境的办法序列化成JSON字符串,并回来给OC端 -
_handleMessageFromObjC
:处理OC发给javascript环境的办法,_dispatchMessageFromObjC(messageJSON)
这个办法的参数便是OC调用JS的message信息,这个办法对messageJSON进行解析处理,从而调用相应的JS办法。
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
这儿的src便是https://wvjb_queue_message,这段代码的意思是把javascript要发送给OC的音讯当即发送出去。
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
WebViewJavascriptBridge_js
的最终是上面的代码,它会调用ExampleApp.html
中的callBack办法,也便是它
setupWebViewJavascriptBridge(function(bridge) {
....
})
继而完成对这个JS环境的初始化与ExampleApp.html
的加载。
(3) JS调用OC办法流程
- 点击JS按钮触发下面的办法
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
传递办法名testObjcCallback
,音讯参数{'foo': 'bar'}
,以及OC回调JS的办法function(response) {log('JS got response', response)})
,
- 调用
WebViewJavascriptBridge_js
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
....
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()
,入参message是调用OC的办法名与参数,responseCallback是OC回调JS的办法,接下来将这个回调办法保存在responseCallbacks中,key值是callbackId
,音讯目标message也添加一个callbackId
,最终设置messagingIframe
的src特点,从而被ebView的署理办法- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
阻拦。
- 在上面的署理办法中,阻拦到的URL为
__wvjb_queue_message__
,所以调用办法:
- (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];
}];
}
....
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
WebView触发JS的WebViewJavascriptBridge._fetchQueue()
,
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
这个办法里边会将sendMessageQueue
换成json字符串,然后回来给OC环境,触发[_base flushMessageQueue:result];
- (void)flushMessageQueue:(NSString *)messageQueueString{
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}
// JS传递过来的json字符串,咱们进行反序列化 得到message数组
NSLog(@"messageQueueString===%@",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];
} 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;
}
//向下传递参数,而且触发block回调()
handler(message[@"data"], responseCallback);
}
}
}
这个办法便是OC端处理JS的中心办法了,将messageQueueString
反序列化,得到音讯数组
(
{
callbackId = "cb_1_1639553291614";
data = {
foo = bar;
};
handlerName = testObjcCallback;
}
)
有callbackId
,标明有音讯回调,生成responseCallback
Block,这个Block里边将接纳的参数与callbackId
打包同时发送给JS环境,并调用JS环境的WebViewJavascriptBridge._handleMessageFromObjC(messageJSON);
办法将messageJSON
进行解析。这儿仅仅Block的完成,并没调用这个Block,调用在下面
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
//向下传递参数,而且触发block回调()
handler(message[@"data"], responseCallback);
依据handlerName
找到在messageHandlers
保存的办法完成,handler(message[@"data"], responseCallback);
进行真正的调用,在OC的注册办法中,调用responseCallback(@"Response from testObjcCallback");
向JS环境发送回调并传递参数。
-
JS环境经过这个
_handleMessageFromObjC(messageJSON)
办法得到messageJSON
,并对其解析。function _dispatchMessageFromObjC(messageJSON) { if (dispatchMessagesWithTimeoutSafety) { setTimeout(_doDispatchMessageFromObjC); } else { _doDispatchMessageFromObjC(); } function _doDispatchMessageFromObjC() { //转换为目标 var message = JSON.parse(messageJSON); var messageHandler; var responseCallback; // 这个responseId便是JS调用OC办法保存的callbackId if (message.responseId) { responseCallback = responseCallbacks[message.responseId]; if (!responseCallback) { return; } responseCallback(message.responseData); delete responseCallbacks[message.responseId]; } else { 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); } } } }
先将字符串转化为JSON目标,依据responseId(这个responseId便是JS调用OC办法保存的callbackId),找到对应的办法完成,进行调用
function(response) { log('JS got response', response) }
到此就完成了JS调用OC,而且OC回调JS并传递参数的悉数过程。
2. OC调用JS办法
跟上面类似再来看下OC自动调用JS办法的完成
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
....
- (void)callHandler:(NSString *)handlerName data:(id)data {
[self callHandler:handlerName data:data responseCallback:nil];
}
- (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 {
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];
}
....
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
NSLog(@"javascriptCommand==%@",javascriptCommand);
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
能够看到,跟JS调用OC办法的原理类似,将OC调用JS的办法名与参数封装进message目标,假如有回调函数,将回调函数经过responseCallbacks
保存,并生成callbackId
,将整个message打包发送给JS环境的WebViewJavascriptBridge._handleMessageFromObjC(messageJSON);
进行解析,解析流程上面介绍过了,这儿不再赘述。
四. 总结
经过上面流程剖析,整个WebViewJavascriptBridge
内部的完成原理就比较明晰了。
- JS将办法注册到JS环境的bridge,OC调用JS的中心办法便是
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
,JS环境收到音讯后,经过办法WebViewJavascriptBridge._handleMessageFromObjC();
将音讯进行解析,调用注册在bridge的办法。 - OC将办法注册到OC环境的bridge,JS调用OC的中心逻辑是,设置空白网页的
src
特点,从而被webView的署理办法- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
,OC经过中心办法- (void)flushMessageQueue:(NSString *)messageQueueString
将传递数据进行解析。 - 两边都是经过办法名找到对应的办法完成,然后经过ID来查找回调函数。
多阅览源码是好的一方面能够提高自己源码阅览水平,另一方面能够学习作者的一些好的规划思路。
参阅:
github.com/marcuswesti…
github.com/lzyzsd/JsBr…
/post/684490…