原文:橘子不酸丶
转载:https://juejin.im/post/5e706726518825491b11e52a
前言
最近由于项目使用,在开发过程中再次研读了WebViewJavascriptBridge这个在移动开发中原生与h5交互的库。写下本文来记录源码的分析以及过程。
使用
首先先来看一下WebViewJavascriptBridge库的文件结构和大致用途。总共有四个类,逻辑也并不复杂,加起来代码不超过一千行。
- WebViewJavascriptBridge_JS (JS文件类,定义了要加载的JS代码,整体作为字符串)
- WebViewJavascriptBridgeBase (承载了与H5交互的核心类,主要的交互方法所在的基类)
- WebViewJavascriptBridge (封装的UIWebView和WebView(OS)交互类)
- WKWebViewJavascriptBridge (封装的WKWebView交互类,处理WKWebView的一些回调)
我们来看一下Bridge的使用过程,也就是Bridge的初始化使用。Bridge在初始化的时候会接管WebView的回调,需要重设Delegate来把回调转接回来。
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
//创建 WebViewJavascriptBridge
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
//设置完Bridge之后把delegate再设置回webview
[self.bridge setWebViewDelegate:self];
// 注册 handler
[self.handler registerHandlersForJSBridge:self.bridge];
这里使用一个handler类来实现Bridge方法的注册实现。主要用于桥接方法的分类处理和后续扩展。
NSString * const kMJYPBaseWebViewBridgeHandlerPre = @"__mjypExport__bridgeHandler_";
//公开接口, m方法名,n参数,c回调 (目前只是两个参数arg和callback)
#define BRIDGE_HANDLER_EXTERN_METHODX(m, n, c) \
- (void)__mjypExport__bridgeHandler_##m:(NSDictionary *)arg callback:(MJYPWebViewHandlerResponseBlock)callback
/// 注册 Handler
- (void)registerHandlersForJSBridge:(WebViewJavascriptBridge *)bridge;
在Handler中注册方法只需要宏定义就可以,使用runtime读取所有的注册方法。
NSString * const kMJYPBaseWebViewBridgeHandlerPre = @"__mjypExport__bridgeHandler_";
- (void)registerHandlersForJSBridge:(WebViewJavascriptBridge *)bridge {
NSArray *handlerMethods = [self bridgeHandlerMethods];
for (NSString *aHandlerName in handlerMethods) {
s_ws(weakself);
[bridge registerHandler:[self getHandlerNameWithBridgeMethod:aHandlerName] handler:^(id data, WVJBResponseCallback responseCallback) {
s_ss(strongself, weakself);
NSMutableDictionary *args = [NSMutableDictionary dictionary];
if ([data isKindOfClass:[NSDictionary class]]) {
[args addEntriesFromDictionary:data];
}
SEL selector = NSSelectorFromString(aHandlerName);
if ([strongself respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[strongself performSelector:selector withObject:args withObject:responseCallback];
#pragma clang diagnostic pop
}
}];
}
}
- (NSArray *)bridgeHandlerMethods {
if (!_methods) {
NSMutableArray *handlerMethods = [NSMutableArray new];
unsigned int methodCount;
Class cls = object_getClass(self);
while (cls && cls != [NSObject class] && cls != [NSProxy class]) {
Method *methods = class_copyMethodList(cls, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
NSString *selName = NSStringFromSelector(selector);
if ([selName hasPrefix:kMJYPBaseWebViewBridgeHandlerPre]) {
[handlerMethods addObject:selName];
}
}
free(methods);
cls = class_getSuperclass(cls);
}
_methods = [handlerMethods copy];
}
return _methods;
}
- (NSString *)getHandlerNameWithBridgeMethod:(NSString *)method {
if ([method isKindOfClass:[NSString class]] && [method hasPrefix:kMJYPBaseWebViewBridgeHandlerPre]) {
NSArray *components = [method componentsSeparatedByString:@":"];
if (components.count) {
NSString *handlerName = [components firstObject];
return [handlerName stringByReplacingOccurrencesOfString:kMJYPBaseWebViewBridgeHandlerPre withString:@""];
}
}
return method;
}
在外部初始化bridge之后,再来看一下WebViewJavascriptBridge内部的初始化逻辑。过程也很简单,初始化一个Bridge以及初始化一个BridgeBase对象来交互。
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
这里我们主要看WebViewJavascriptBridgeBase里就可以,初始化时的reset处理,startupMessageQueue数组主要作为存储h5页面加载Bridge的JS环境之前调用的message。responseCallbacks主要为存储原生调用H5方法时的回调。
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
再看一下H5端的初始化,我们发现WebViewJavascriptBridge会在Native和H5端都初始化一个Bridge对象。并且在H5初始化时调用'https://_ _ bridge_loaded__'来触发Native的方法并初始化JS环境挂载。
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)
}
而在native中通过WebView的代理方法来拦截并响应H5的初始化。如果url是bridge_loaded就加载Bridge的JS环境,如果是Message就响应消息处理。
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
交互分析
接下来我们来分析具体的原生与H5交互过程,
原生调用H5
WebViewJavaScriptBridge在原生和H5之间传递消息的方法很巧妙,都会封装成一个message(也就是一个Dictionary)来传递。原生调用H5的方法时,首先会把回调block存储在self.responseCallbacks中并生成callbackId通过callbackId来传递到h5并传回来再调用。WebViewJavaScriptBridge吧data(参数)handlerName(H5方法名)以及回调callbackId封装为一个message并通过WebViewJavascriptBridge._handleMessageFromObjC来传递到h5。
这里很明显,Bridge如果已经加载了JS环境就处理消息,如果还未加载JS环境则存储消息在startupMessageQueue中等待JS环境加载完成后处理。injectJavascriptFile之后JS环境加载完成则把startupMessageQueue置空。
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage: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];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
原生发送消息执行JS的_handleMessageFromObjC之后,H5接收到并转发执行JS中的_dispatchMessageFromObjC()方法(这里可以通过WebViewJavascriptBridge_JS查看)。
JS端接收到message之后,会将它解析成为JS对象,然后去使用data、callbackId和handlerName。然后根据handlerName去messageHandlers里面去对应的handler函数,然后去执行这个函数。
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId,responseData:responseData });
};
}
handler(message.data, responseCallback);
可以看到调用JS注册的handler时传入两个参数,第一个参数是传过来的data数据,第二个参数是responseCallback。responseCallback就是根据callbackId和JS方法返回值的function。然后handler里就可以处理接收到的回调了。handler通过responseId和回调数据来触发Native的调用CallBack方法,可以看到Native在收到message时如果包含responseId就视为block回调处理,则从responseCallbacks中取出之前存储的回调block并执行。这样从Native调H5并收到H5回调数据的流程就完成了。
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
}
H5调用原生方法
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;
}
从上边的H5调用原生的方法可以看到,逻辑和原生调用H5原理类似,JS中会存储callBack并生成callbackId,然后通过封装的message传递到Native,url为'https://_ _ wvjb_queue_message_ _'。
客户端在webview的代理方法中识别到message类型并处理,客户端会把message解析成WVJBMessage(NSDictionary)并处理
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);
可以看到如果存在callbackId则认为有回调,如果没有则无需回调(此时给了一个空的回调block),识别到callbackId之后客户端会执行对应的HandlerName注册方法,并可以在注册方法里回调给H5方法。回调时则是把callbackId作为responseId和responseData一起回调给H5,H5在识别到responseId之后则识别为回调并从之前存储的callback中取出来执行对应回调。
结束
至此整个交互过程就结束了,整体来说不论是从H5调用Native还是Native调用H5,思想都是一致的,从存储回调block,然后拿blockId、data、handlerName来调用对方,并通过对方返回的responseId来识别是否block回调。