注:咱们以wkwebview为例。下面的代码都是针对于wkwebview的。javascript
UIButton *callbackButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; [callbackButton setTitle:@"原生调用JS" forState:UIControlStateNormal]; [callbackButton addTarget:self action:@selector(callJSMethod:) forControlEvents:UIControlEventTouchUpInside]; ... _bridge = [WKWebViewJavascriptBridge bridgeForWebView:webView];
- (void)callJSMethod:(id)sender { id data = @{ @"原生调用JS参数1": @"参数1" }; [_bridge callHandler:@"JSMethod1" data:data responseCallback:^(id response) { NSLog(@"testJavascriptHandler responded: %@", response); }]; }
- (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]; }
这个方法写的很清晰,把要调用的js的函数名handlerName,参数data,和回调方法的Id(callbackId)打包到一个字典对象message中。callbackId,每一个回调一个,惟一。为何用callbackId,由于block自己是一个对象这个对象JS识别不了。其实传过去意义也不是很大,只要把这个block放在WebViewJavascriptBridgeBase对象中的responseCallbacks字典中就行,key就是刚才生成的callbackId。而后继续调用下面的方法。css
- (void)_queueMessage:(WVJBMessage*)message { if (self.startupMessageQueue) { [self.startupMessageQueue addObject:message]; } else { [self _dispatchMessage:message]; } }
这里其实没有放入队列,而是直接分发了消息,稍后会说为何这里self.startupMessageQueue为nil。html
- (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]; }); } }
首先把WVJBMessage对象message串行化为JSON字符串,而后转义字符串里的字符;生成JS的命令字符串;在主线程中执行js命令。为何要在主线程中执行,苹果文档中有这么一句话:The WebKit framework is not thread-safe. If you call functions or methods in this framework, you must do so exclusively on the main program thread。java
//WebViewJavascriptBridgeBase.m - (void) _evaluateJavascript:(NSString *)javascriptCommand { [self.delegate _evaluateJavascript:javascriptCommand]; } //WKWebViewJavascriptBridge.ms - (NSString*) _evaluateJavascript:(NSString*)javascriptCommand { [_webView evaluateJavaScript:javascriptCommand completionHandler:nil]; return NULL; }
OC调用js的方法,都会把callback方法,方法名,参数打包到messageJSON中,而后调用下面这个终极方法。WebViewJavascriptBridge._handleMessageFromObjC(messageJSON);这个方法存在于WebViewJavascriptBridge_js.m文件中,是页面加载的时候注入的。下面部分就讲这个过程。web
咱们仍是以官方的例子为例。加载一个本地的html页面ExampleApp.html。加载以后以下方法会被执行json
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { if (webView != _webView) { return; } 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; } if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) { [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler]; } else { decisionHandler(WKNavigationActionPolicyAllow); } }
这个方法是加载网页第一个执行的方法,由于它要肯定是否容许或者取消加载这个导航(就是是否是容许加载这个页面)。首次加载的时候url不是特殊的jsBridge的URL,直接容许加载这个页面。下面看看页面的源代码。数组
<!doctype html> <html><head> <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0"> <style type='text/css'> html { font-family:Helvetica; color:#222; } h1 { color:steelblue; font-size:24px; margin-top:24px; } button { margin:0 3px 10px; font-size:12px; } .logLine { border-bottom:1px solid #ccc; padding:4px 2px; font-family:courier; font-size:11px; } </style> </head><body> <h1>WebViewJavascriptBridge Demo</h1> <script> window.onerror = function(err) { log('window.onerror: ' + err) } function setupWebViewJavascriptBridge(callback) { //第一次调用这个方法的时候,为false if (window.WebViewJavascriptBridge) { var result = callback(WebViewJavascriptBridge); return result; } //第一次调用的时候,也是false if (window.WVJBCallbacks) { var result = window.WVJBCallbacks.push(callback); return result; } //把callback对象赋值给对象。 window.WVJBCallbacks = [callback]; //这段代码的意思就是执行加载WebViewJavascriptBridge_JS.js中代码的做用 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 callback(bridge) { var uniqueId = 1 //把操做记录写入webview中 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) } } //把WEB中要注册的方法注册到bridge里面 bridge.registerHandler('JSMethod1', function(data, responseCallback) { log('OC调用JS方法成功', data) var responseData = { 'JS给OC调用的回调':'回调值!' } log('OC调用JS的返回值', responseData) responseCallback(responseData) }) //获取web中的button,而后添加点击事件。 document.body.appendChild(document.createElement('br')) document.getElementById('buttons').onclick = function(e) { e.preventDefault() var params = {'JS调用OC参数': '参数值'}; log('JS立刻调用OC方法',params) bridge.callHandler('OC提供方法给JS调用',params, function(response) { log('JS调用OC的返回值', response) }) } }; //驱动全部hander的初始化 setupWebViewJavascriptBridge(callback); </script> <input type='button' id='buttons' class='button' value='点击开始JS调用OC'></input> <div id='log'></div> </body></html>
定义了2个方法,而且以第二个方法为参数,调用了第一个方法。方法一第一次调用的时候只是为window对象添加了一个数组WVJBCallbacks。并把第二个函数放进去。而后建立一个不可见的iframe元素,设置其url为一个特殊的url:https://__bridge_loaded__。这样也面又会发起一个请求,第一步的方法webView: decidePolicyForNavigationAction: decisionHandler再次被调用。此次会执行到这个分支[_base injectJavascriptFile]。app
- (void)injectJavascriptFile { 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]; } } }
WebViewJavascriptBridge_js只包含一个方法,生成一个字符。这个字符串就是要注入的js代码(也就是要执行的的代码)。框架
NSString * WebViewJavascriptBridge_js() { #define __wvjb_js_func__(x) #x // BEGIN preprocessorJSCode static NSString * preprocessorJSCode = @__wvjb_js_func__( ;(function() { if (window.WebViewJavascriptBridge) { return; } if (!window.onerror) { window.onerror = function(msg, url, line) { console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line); } } window.WebViewJavascriptBridge = { registerHandler: registerHandler, callHandler: callHandler, disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, _fetchQueue: _fetchQueue, _handleMessageFromObjC: _handleMessageFromObjC }; var messagingIframe; var sendMessageQueue = []; var messageHandlers = {}; var CUSTOM_PROTOCOL_SCHEME = 'https'; var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__'; var responseCallbacks = {}; var uniqueId = 1; var dispatchMessagesWithTimeoutSafety = true; function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; } 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); messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; } function _fetchQueue() { var messageQueueString = JSON.stringify(sendMessageQueue); sendMessageQueue = []; return messageQueueString; } function _dispatchMessageFromObjC(messageJSON) { if (dispatchMessagesWithTimeoutSafety) { setTimeout(_doDispatchMessageFromObjC); } else { _doDispatchMessageFromObjC(); } function _doDispatchMessageFromObjC() { 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) { _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); } } } } 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); registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout); setTimeout(_callWVJBCallbacks, 0); function _callWVJBCallbacks() { var callbacks = window.WVJBCallbacks; delete window.WVJBCallbacks; for (var i=0; i<callbacks.length; i++) { callbacks[i](WebViewJavascriptBridge); } } })(); ); // END preprocessorJSCode #undef __wvjb_js_func__ return preprocessorJSCode; };
这个js代码的功能都是啥?建立了window.WebViewJavascriptBridge对象,这个是整个OC和原生交互的核心。这个对象里面包含方法_handleMessageFromObjC。就是咱们第一部分8中调用的。这个方法会调用_doDispatchMessageFromObjC()方法。而后就是定义了各类对象,和函数。在最后咱们看到一个经过setTimeout的函数调用。ide
setTimeout(_callWVJBCallbacks, 0); function _callWVJBCallbacks() { var callbacks = window.WVJBCallbacks; delete window.WVJBCallbacks; for (var i=0; i<callbacks.length; i++) { callbacks[i](WebViewJavascriptBridge); } }
还记得window.WVJBCallbacks吗?他是在ExampleApp.html中定义的一个存储回调的数组。咱们定义的callback就放在里面。调用这个数组里的全部回调函数并以WebViewJavascriptBridge对象做为参数。因而在ExampleApp.html中定义的第二个方法获得执行(这个方法里包含用户页面要执行的js代码,因此要放到页面里,不能放到框架jsbridge中)。
看看callback方法里的这段代码。
//把WEB中要注册的方法注册到bridge里面 bridge.registerHandler('JSMethod1', function(data, responseCallback) { log('OC调用JS方法成功', data) var responseData = { 'JS给OC调用的回调':'回调值!' } log('OC调用JS的返回值', responseData) responseCallback(responseData) })
调用了bridge对象的registerHandler注册了一个方法名和对应的函数,OC就是经过这个方法名JSMethod1来调用了JS的方法。咱们回到WebViewJavascriptBridge_js中,看registerHandler它是如何实现的。
function registerHandler(handlerName, handler) { alert(handlerName+'01'); messageHandlers[handlerName] = handler; }
就是把函数存到了对象messageHandlers里。到此OC要调用JS方法已经放到字典里,等待被调用。
第一部分第8步里说过,OC调用JS方法最后都会变成执行WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)这个在js代码中定义的方法。下面仍是看源码吧
function _handleMessageFromObjC(messageJSON) { _dispatchMessageFromObjC(messageJSON); } //继续看_dispatchMessageFromObjC function _dispatchMessageFromObjC(messageJSON) { if (dispatchMessagesWithTimeoutSafety) { setTimeout(_doDispatchMessageFromObjC); } else { _doDispatchMessageFromObjC(); } function _doDispatchMessageFromObjC() { 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 { //alert(message.callbackId); //objc_cb_1 objc_cb_2 objc_cb_3.... //若是OC调用JS的时候设置了回调用的block,callbackId就不为空。这里生成了一个responseCallback函数。在后面调用JS方法的时候用。 if (message.callbackId) { var callbackResponseId = message.callbackId; //这里定义的这个函数,被传递给OC将要调用的JS方法。在方法里会用要传给OC的数据responseData作参数调用。若是没有定义这个回调函数,OC调用JS方法也能成功,可是调用的时候传入的block不会被执行。 responseCallback = function(responseData) { //这里只传递了一个参数。 _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData }); }; } //messageHandlers字典中存着咱们要调用的JS的方法。 var handler = messageHandlers[message.handlerName]; alert(message.handlerName); if (!handler) { console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message); } else { //调用JS的方法,至此OC最终调用了JS的方法。传入的responseCallback方法在JS中被调用,参数是要返回给OC的数据 yuxg handler(message.data, responseCallback); } } } }
这把message对象放到数组里,而后更改iframe的url,刷新页面。 function _doSend(message, responseCallback) { if (responseCallback) { var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); responseCallbacks[callbackId] = responseCallback; message['callbackId'] = callbackId; } //message对象里包含,OC调用的JS的方法名,回调的Block的Id,和block的参数,也就是传回的数据。 //把这个对象 sendMessageQueue.push(message); //把iframe的地址修改成:https://__wvjb_queue_message__ messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
JS给OC传递消息,都是经过修改iframed的src(也就是url)来实现的。这样WKWebViewJavascriptBridge里面webView:decidePolicyForNavigationAction: decisionHandler方法就能拦截这个消息。在里面会调用
[self WKFlushMessageQueue],咱们继续看代码
//WKWebViewJavascriptBridge.m - (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]; }]; } //WebViewJavascriptBridgeBase.m (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; } //JSON字符串反序列化为数组,数组里的元素是字典类型。 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]; //这里调用了OC调用JS方法是传入的block。 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; } handler(message[@"data"], responseCallback); } } }