1 前言 WebViewJavascriptBridge 是iOS/OSX平台上支撑Obj-C和UIWebViews/WebViews JavaScript互发消息的库。目前主流App几乎都是某种程度的Hybrid App,该库因而得到广泛应用。
2 基础知识 在学习该库之前我们必须了解一些基础知识。主要包含前端和Native两大部分。
2.1 前端部分——HTML Keypoint:
<script>
标签包裹的是JavaScript代码
window、iframe
setTimeout(0)
2.2 前端部分——JavaScript Keypoint:
资料:
2.3 Native部分-关于UIWebView 1 2 3 4 5 6 7 8 9 __TVOS_PROHIBITED @protocol UIWebViewDelegate <NSObject > @optional - (BOOL )webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType )navigationType; - (void )webViewDidStartLoad:(UIWebView *)webView; - (void )webViewDidFinishLoad:(UIWebView *)webView; - (void )webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error; @end
UIWebView的代理UIWebViewDelegate,会在UIWebView各个事件节点收到回调消息。其中最重要的是- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
当UIWebView加载URL page或者iframe设置src的时候,UIWebViewDelegate都会执行该回调。
3 WebViewJavascriptBridge设计 在分析WebViewJavascriptBridge源码之前,我们先聊一下WebViewJavascriptBridge设计。
3.1 整体框架
WebViewJavascriptBridge整体框架如上图所示。
包含4部分:
前端业务逻辑
前端js bridge基础设施
Native js bridge基础设施
Native业务逻辑
3.2 js bridge基础设施 总的来说js bridge基础设施主要由3部分组成:
消息流 : FE和Native之间的消息传递过程;
消息体(message) :message即Native和前端消息流中的消息体,主要有4个部分:函数名、参数、回调ID、响应ID;
消息队列(FE message queue) :前端消息队列用来暂存前端到Native的消息体。
3.2.1 消息流 消息流如下图所示。
从图中我们可以看出消息流有两个参与者,即调用方 和被调方 。调用方发起请求,收到对方的回调消息。被调方收到请求,执行请求,发送回调消息。
Native和FE都可能是调用方和被调用方,所以Native和FE都至少包含两部分功能:
send(发送自己的调用请求到对端)
receive(收到了来自对端的调用请求)
3.2.2 消息体 消息体有四个成员:
函数名
参数
callbackID
responseID
其中函数名和参数都很好理解。这里我们主要说一下callbackID和responseID。
调用方在发起调用的同时设置回调块
,该回调块
在被调方执行完任务后再执行。具体的实现手段是,调用方在拼接消息体的时候,把回调块
管理起来,并设置一个唯一的ID, 放到消息体的callbackID上面。 此时被调方收到的消息包含callbackID,在执行完成对应函数后,会生成一个应答消息,告知对方自己已经执行完成,这个应答消息也是一个消息体,该消息体的responseID设置为其所应答消息的callbackID,表示对该消息的应答。这时,调用方收到应答消息,检查responseID,匹配后找到之前对应的回调块
并执行。
样例如图所示:
4 WebViewJavascriptBridge实现 4.1 消息流和消息队列实现 消息流(前端到Native)
前端到Native的消息流由隐藏的iframe发起。每次调用js bridge函数时设置iframe的src,然后,Native的UIWebViewDelegate
收到- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
回调,在回调上加一些额外的逻辑区分,Native就知道前端发起了js bridge函数调用。
消息流(Native到前端)
Native到前端的消息流比较简单。它是由UIWebView本身完成。UIWebView的- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script
可以直接执行js命令。Native要调用前端的方法时,可以把方法转化为js命令直接调用。
消息队列(FE message queue)
前端消息队列用来暂存前端到Native的消息体。 相关的点如下:
前端设置iframe的src之前会先把消息存到消息队列;
Native收到回调后,调用相关js命令从前端获取消息队列,得到消息队列后,按照消息队列的每条消息执行相应操作——函数调用。
4.2 前端js bridge源码 4.2.1 send 参考前端到Native消息流。send通过iframe设置src和messageQueue缓存消息体实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function callHandler (handlerName, data, responseCallback ) { if (arguments .length == 2 && typeof data == 'function' ) { responseCallback = data; data = null ; } var message = { handlerName :handlerName, data :data }; if (responseCallback) { var callbackId = 'cb_' +(uniqueId++)+'_' +new Date ().getTime(); responseCallbacks[callbackId] = responseCallback; message['callbackId' ] = callbackId; } sendMessageQueue.push(message); messagingIframe.src = 'wvjbscheme://__WVJB_QUEUE_MESSAGE__' ; } function _fetchQueue ( ) { var messageQueueString = JSON .stringify(sendMessageQueue); sendMessageQueue = []; return messageQueueString; }
4.2.2 receive 参考Native到前端的消息流。receive通过registerHandler注册js bridge函数,通过_handleMessageFromObjC方法执行messageHandlers里面的函数体。
1 2 3 4 5 var messageHandlers = {};function registerHandler (handlerName, handler ) { messageHandlers[handlerName] = handler; }
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 function _handleMessageFromObjC (messageJSON ) { var message = JSON .parse(messageJSON); var messageHandler; var responseCallback; 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 ) { var message = { handlerName :message.handlerName, responseId :callbackResponseId, responseData :responseData }; sendMessageQueue.push(message); messagingIframe.src = 'wvjbscheme://__WVJB_QUEUE_MESSAGE__' ; }; } var handler = messageHandlers[message.handlerName]; if (!handler) { console .log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:" , message); } else { handler(message.data, responseCallback); } } }
4.3 Native js bridge源码 4.3.1 send 参考Native到前端的消息流。Native的send是先拼接出js命令,再直接执行stringByEvaluatingJavaScriptFromString
。
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]; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 - (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]; }); } }
1 2 3 4 5 - (NSString *) _evaluateJavascript:(NSString *)javascriptCommand { return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand]; }
4.3.2 receive 参考FE到Native消息流。
1 2 3 4 - (void )registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler { _base.messageHandlers[handlerName] = [handler copy ]; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - (BOOL )webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType )navigationType { NSURL *url = [request URL]; __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate; if ([_base isCorrectProcotocolScheme:url]) { if ([_base isQueueMessageURL:url]) { NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; [_base flushMessageQueue:messageQueueString]; } return NO ; } }
1 2 3 -(NSString *)webViewJavascriptFetchQueyCommand { return @"WebViewJavascriptBridge._fetchQueue();" ; }
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 49 50 - (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 ; } 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) { }; } WVJBHandler handler = self .messageHandlers[message[@"handlerName" ]]; if (!handler) { NSLog (@"WVJBNoHandlerException, No handler for message from JS: %@" , message); continue ; } handler(message[@"data" ], responseCallback); } } }
5总结 本文从JS bridge的基础知识讲到WebViewJavascriptBridge的源码实现。涉及的点有消息流,消息体,消息队列等。其中比较有意思的是回调实现原理。算是对自己阅读代码的一个记录。