WebViewJavascriptBridge 源码解析
框架简介
marcuswestin/WebViewJavascriptBridge 是用于在 WKWebViews, UIWebViews & WebViews 中,JS 与 Obj-C 相互发送消息的桥接库。
通常实现 iOS 与 JS 桥接的方式有两种:
- 通过 Webview 拦截请求的方式。
- 通过 iOS7 之后的 JavaScriptCore 框架。
marcuswestin/WebViewJavascriptBridge 使用的正是第一种方式。
该桥接库在 Github 上拥有 1.2 w+ 的 star 数,可见该库受欢迎的程度。
功能解析
我们从该框架的几个重要功能点入手,来逐步了解其对应的实现细节。
主要功能分为 6 点:
- Obj-C Bridge 对象的初始化。
- JS Bridge 对象的初始化。
- Obj-C 注册函数。
- JS 调用 Obj-C 注册的函数,并支持回调 JS。
- JS 注册函数。
- Obj-C 调用 JS 的函数,并支持回调 Obj-C。
相关功能对应的使用方法如下:
// Obj-C bridge 对象的初始化。
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
// JS bridge 对象的初始化。
// JS 中引入的代码
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = dObj-Cument.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
dObj-Cument.dObj-CumentElement.appendChild(WVJBIframe);
setTimeout(function() { dObj-Cument.dObj-CumentElement.removeChild(WVJBIframe) }, 0)
}
// Obj-C 桥接库中注入的 JS 代码
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
// Obj-C 注册函数供 JS 调用
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
responseCallback(@"Response from testObjcCallback");
}];
// JS 调用 Obj-C 注册的方法,传参,并支持回调 JS
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
// JS 注册函数供 Obj-C 调用
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
var responseData = { 'Javascript Says':'Right back atcha!' }
responseCallback(responseData)
})
// Obj-C 调用 JS 注册的方法,传参,支持回调 Obj-C
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" } responseCallback:^(id responseData) {
NSLog(@"responseCallback called: %@", responseData);
}];
源码解析
源码解析从作者 ExampleApp-iOS 这个 demo 入手,我们看下 ExampleUIWebViewController.m
中的 demo 代码。
Obj-C 初始化 Bridge
// Obj-C 初始化 bridge 对象
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
// ViewController 实现 WebView 的代理方法
[_bridge setWebViewDelegate:self];
bridgeForWebView:
方法中创建 WebViewJavascriptBridge
对象 bridge,并设置 webView 的代理为 bridge,这里主要是需要代理方法 shouldStartLoadWithRequest:
与 JS 通信。创建 WebViewJavascriptBridgeBase
对象 _base,并设置 _base 代理为 bridge,这里是需要 WebView 对象提供 evaluateJavaScript:
方法与 JS 通信。
而 WebViewJavascriptBridgeBase
的初始化方法中,会创建 messageHandlers
,startupMessageQueue
,responseCallbacks
对象,messageHandlers
用于存储 Obj-C 注册的函数,startupMessageQueue
存储 webview
载入页面前就调用的 JS 函数,responseCallbacks
存储 Obj-C 调用 JS 函数后要回调 Obj-C 的回调方法。
这几个 Array 和 Dictionary 如何作用,后面会有介绍。
JS 初始化 bridge
因为 JS 与 Obj-C 之间会相互调用,不容易理清两者的调用关系,所以我画了一个泳道图辅助理解。
WebViewJavascriptBridge 源码解析 - JS Bridge 初始化.pngJS 初始化 bridge 的过程要比 Obj-C 复杂。
首先,WebView 要载入 html 页面,里面有初始化相关的 JS 代码。
- (void)loadExamplePage:(UIWebView*)webView {
NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"ExampleApp" ofType:@"html"];
NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
[webView loadHTMLString:appHtml baseURL:baseURL];
}
载入 html 页面会加载 JS 代码,JS 相关代码如下:
function setupWebViewJavascriptBridge(callback) {
// 如果有 bridge 对象则直接调用 callback 并传入 bridge对象。
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
// 如果有 WVJBCallbacks 则将回调函数 push 到数组里,后面初始化 bridge 时会统一遍历调用 callback,并传入 bridge。
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
// 首次载入,创建 WVJBCallbacks 并放入 callback,用 iframe 加载 url https://__bridge_loaded__'
window.WVJBCallbacks = [callback];
var WVJBIframe = dObj-Cument.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
dObj-Cument.dObj-CumentElement.appendChild(WVJBIframe);
setTimeout(function() { dObj-Cument.dObj-CumentElement.removeChild(WVJBIframe) }, 0)
}
// 用 bridge 注册函数和调用 Obj-C 函数
setupWebViewJavascriptBridge(function(bridge) {
var uniqueId = 1
// 注册 JS 函数
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
var responseData = { 'Javascript Says':'Right back atcha!' }
responseCallback(responseData)
})
dObj-Cument.body.appendChild(dObj-Cument.createElement('br'))
var callbackButton = dObj-Cument.getElementById('buttons').appendChild(dObj-Cument.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
// 调动 Obj-C 的函数
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}
})
iframe 加载 url https://__bridge_loaded__
后,会调用 webview 的回调方法 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
,
然后判断 url 的 host 是 bridge_loaded 后调用如下代码:
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
}
- (void)injectJavascriptFile {
// 注入初始化 JS bridge 的 JS 代码。
NSString *JS = WebViewJavascriptBridge_JS();
[self _evaluateJavascript:JS];
// 如果有在 JS bridge 没有初始化好就调用的 JS 函数(这些函数调用保存到 startupMessageQueue 中),则在 JS bridge 初始化好后,统一调用。
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
WebViewJavascriptBridge_JS
中是一段 JS 代码,需要注入到 JS 环境中,这段代码就是初始化 JS bridge 的代码。
精简后的代码如下:
NSString * WebViewJavascriptBridge_JS() {
#define __wvjb_JS_func__(x) #x
// BEGIN preprObj-CessorJSCode
static NSString * preprObj-CessorJSCode = @__wvjb_JS_func__(
;(function() {
if (window.WebViewJavascriptBridge) {
return;
}
// 初始化 bridge
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
var sendMessageQueue = [];
var messageHandlers = {};
// 略一部分代码
var CUSTOM_PROTObj-COL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
// 调用 https://__wvjb_queue_message__ 通知 Obj-C,统一调用 JS 启动时 JS 调用 Obj-C 的所有方法
messagingIframe = dObj-Cument.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTObj-COL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
dObj-Cument.dObj-CumentElement.appendChild(messagingIframe);
// 将 JS bridge 传给 callback
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
})();
); // END preprObj-CessorJSCode
#undef __wvjb_JS_func__
return preprObj-CessorJSCode;
};
调用 _callWVJBCallbacks
函数,将 window.WVJBCallbacks
数组中的所有 callback 调用一遍,并传入初始化好的 bridge 对象。
此处正好和前面的 setupWebViewJavascriptBridge
的代码呼应上。到此 JS bridge 的初始化结束。
Obj-C 注册函数
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
handlerName 作为 Key,handlerBlock作为 Value 存入 _base 的 messageHandlers 字典中。
JS 调用 Obj-C 函数
管道图辅助理解 JS 与 Obj-C 两者调用关系。
WebViewJavascriptBridge 源码解析 - JS 调用 Obj-C 函数.pngJS 调用 Obj-C 代码如下:
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
// WebViewJavascriptBridge_JS.m 中的 JS 代码
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ d: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_PROTObj-COL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
将 JS 调用 Obj-C 函数的一些信息封装成 message 放入 sendMessageQueue
队列中,并通知 Obj-C 去获取 JS 的队列消息,并逐一调用 Obj-C 的注册函数。
message['handlerName'] = handlerName;
message['data'] = data;
message['callbackId'] = callbackId;
webview 代理方法 shouldStartLoadWithRequest:
,接受到 iframe 的 url 请求,判断是 QUEUE_HAS_MESSAGE
后,
if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
}
获取 JS bridge 的消息队列,调用 flushMessageQueue
方法来消费这些消息。
代码如下:
- (void)flushMessageQueue:(NSString *)messageQueueString{
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
// 略一些不影响逻辑的代码
NSString* responseId = message[@"responseId"];
if (responseId) {
// 处理 Obj-C 调用 JS 函数时,Obj-C 需要回调的情况,后面还会提到。
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
// 处理 JS 调用 Obj-C 函数时,JS 需要回调的情况。
// 构建一个 blObj-Ck,传给 handler,需要时回调 JS。
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
// 开始回调 JS,构建回调 message,并发送给 JS bridge 处理。
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
// 获取之前注册的 handler 函数,并调用。
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
handler(message[@"data"], responseCallback);
}
}
}
这里在说明一下,如何在 JS 调用 Obj-C 函数时,再回调 JS。
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
function(response) {}
这个函数就是 JS 需要的回调函数,那具体是怎么回调的呢?
回过头看下 Obj-C bridge 的 - (void)flushMessageQueue:(NSString *)messageQueueString
函数。
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
}
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
handler(message[@"data"], responseCallback);
判断有 callbackId 时说明,JS 需要回调,然后构建一个回调的 message,responseId 就是传来的 callbackId,responseData 是要回传的数据,之所以用 responseId 这个 key 是因为做消息的类型区分。[self _queueMessage:msg]
就是将消息发回给 JS bridge。JS bridge 通过 responseId 找到之前保存的函数,调用即可。
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
// 处理 JS 的回调函数,找到 responseId 对应的函数并调用。
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
// 略...
}
}
JS 注册函数
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
var responseData = { 'Javascript Says':'Right back atcha!' }
responseCallback(responseData)
})
// WebViewJavascriptBridge_JS.m 中的 JS 代码
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
Obj-C 调用 JS 函数
管道图辅助理解两者调用关系。
WebViewJavascriptBridge源码解析 - Obj-C 调用 JS 函数.png[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
最终调用的是 WebViewJavascriptBridgeBase
中的 - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName
方法,根据参数拼装 message 字典。
message[@"data"] = data; // 数据参数
message[@"callbackId"] = callbackId; // 回调方法
message[@"handlerName"] = handlerName; // 函数名
然后调用方法 - (void)_queueMessage:(WVJBMessage*)message
将拼装好的 message 传入,我们看具体的 _queueMessage 方法实现。
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
如果有 self.startupMessageQueue
对象,将消息放入其中,否则发送消息给 JS,之所以要存入 startupMessageQueue
中,是因为在 viewWillAppear
中的 callHandler 不能立马调用到 JS 中,因为 html 没有载入,JS bridge 的环境也没有准备完成。先存入 startupMessageQueue
中,待 JS bridge 准备完成后,在统一调用 startupMessageQueue
中的消息到 JS,并将 startupMessageQueue
置为空。
接下来我们看如何调用 JS 的函数。
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
// 略
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
将之前拼装好的 message 传给 JS,用 JS bridge 的 _handleMessageFromObjC
函数处理 Obj-C 的调用请求。
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
// 在 JS 调用 Obj-C 函数,且要 JS 回调的情况下的处理方式
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
// 这部分是 Obj-C 调用 JS 函数的处理
if (message.callbackId) {
// 构建一个新的 message 用来回调 Obj-C,responseCallback 函数用于 JS 回调 Obj-C。
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
// 通过 handlerName 获取到对应的函数,并调用。
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}
JS 通过传来的 message,获取到 handlerName,从注册 JS 函数时,保存 handler 处理函数的 messageHandlers 对象中取出要调用的 handler 函数,并调用。
如何处理 Obj-C 的回调函数。
在Obj-C 调用 JS 的函数时,有时候 Obj-C 需要 JS 调用 Obj-C 的回调函数返回一些数据,如:
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" } responseCallback:^(id responseData) {
NSLog(@"responseCallback called: %@", responseData);
}];
这个 responseCallback
就是 JS 要回调 Obj-C 的回调函数。实现方式是在 JS 处理 Obj-C 消息的 _doDispatchMessageFromObjC
方法中,如果 message 有 callbackId 意味着 Obj-C 需要这个回调,然后构建一个 JS 函数 responseCallback
,传给 JS 的 handler 函数,供注册的 JS 函数回调时调用,调用 responseCallback
之后,responseCallback
会创建一个回调的 message 信息 responseId 就是 callbackId,调用 _doSend
函数发送给 Obj-C。
_doSend
中主要是将消息添加到 sendMessageQueue
队列中,供 Obj-C 获取后,通过 responseId 找到 Obj-C 保存的回调函数并调用,实现请看 - (void)flushMessageQueue:(NSString *)messageQueueString
方法中 if (responseId) { ... }
这一段。
// - (void)flushMessageQueue:(NSString *)messageQueueString 中
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
}