iOS WebviewJavascriptBridge 源码研读笔记

2,099 阅读12分钟

这两天接近元旦,事情稍微少些,有些时间,索性写点什么,就从最擅长的iOS混合开发写起了,由于iOS开发经验不到四年吧,期间还搞了一年半的前端,有些知识可能还是积累的不足,能力不足,水平有限,可能有谬误希望各位读者发现的话及时指正,感激不尽。

至于WebviewJavascriptBridge的介绍,此处不再啰嗦了,既然能看到本文,相比对该三方库或多或少还是有所了解的吧。我在申明一点,本文中涉及的demo是直接拿的WebviewJavascriptBridge的,并未做任何修改,直接拿来研究

看了看比较流行的WebviewJavascriptBridge这个三方库的源码,发现好多js和oc部分的核心代码几乎是对称的,所以觉得最好是js和oc代码一起读,这样才更容易理解,也能发现其对称美。。。

要搞明白其调用逻辑,最好是用Safari连上调试一把哈,在网页检查其中我们用oc载入的js代码好难找啊(至少我是花了一番功夫才找到了),莫慌,是在找不到的话在搜索栏里面搜一下WebviewJavascriptBridge,然后在对应的代码出都打上断点,这下就可以研究了

至于有些同学不知道如何打开Safari的调试模式的,请移步至传送门这个方法mac 的Safari也同样受用哈

WebViewJavascriptBridge VS WKWebViewJavascriptBridge

这个框架还是有点666啊,既支持iOS又支持mac OS 但鉴于我们mac OS 用的少,就直接看iOS部分了

红线框出来的部分也就是就是WebviewJavascriptBridge框架的核心代码部分

WebViewJavascriptBridge_JS ==> js核心代码部分,负责js端消息的组装,转发 WebViewJavascriptBridgeBase ==> oc核心代码部分,负责oc端消息组装,转发 WebViewJavascriptBridge ==> 对于UIWebView的进行的封装,是基于WebViewJavascriptBridgeBase的 WKWebViewJavascriptBridge ==> 对于WKWebView的进行的封装,是基于WebViewJavascriptBridgeBase的

至于前面两个核心类会在下一小节中做详细的阐述,本小结就只做后面两个类的分析阐述 直接上图了

对比WebViewJavascriptBridgeWKWebViewJavascriptBridge两个类的头文件,看处WKWebViewJavascriptBridge多了一个reset方法,其他的方法两个类几乎一毛一样,我们继续看.m实现文件也证实了这一点,差别仅在于webview的实现,这也印证了这个框架的核心只是WebViewJavascriptBridgeBase,其核心都是通过js中去“loadUrl”(这个是本人自己习惯这么说,方便理解啊,实际上和loadUrl有点差别,不过道理是一样的)然后webview在代理方法中去拦截特殊约定好的url,然后进行消息的处理。

以下是wkwebview的代理方法截取

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    //获取js “loadUrl”的url链接
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    if ([_base isWebViewJavascriptBridgeURL:url]) {//是不是WebViewJavascriptBridge约定的url
        if ([_base isBridgeLoadedURL:url]) {//是不是初始化指令__bridge_loaded__
            //注入核心js代码
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {//是不是消息指令__wvjb_queue_message__
            //调用WebViewJavascriptBridgeBase的API去分发消息
            [self WKFlushMessageQueue];
        } else {//未知的url
            [_base logUnkownMessage:url];
        }
        //取消跳转
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    //能走到这里证明已经不是WebViewJavascriptBridge约定的url了,做正常跳转
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

以下是UIWebView代理的方法

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if (webView != _webView) { return YES; }
    //获取js “loadUrl”的url链接
    NSURL *url = [request URL];
    __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
    if ([_base isWebViewJavascriptBridgeURL:url]) {//是不是WebViewJavascriptBridge约定的url
        if ([_base isBridgeLoadedURL:url]) {//是不是初始化指令__bridge_loaded__
            //注入核心js代码
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {//是不是消息指令__wvjb_queue_message__
            NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
            //调用WebViewJavascriptBridgeBase的API去分发消息
            [_base flushMessageQueue:messageQueueString];
        } else {//未知的url
            [_base logUnkownMessage:url];
        }
        //取消跳转
        return NO;
    } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
        return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    } else {
        return YES;
    }
}

js 调用oc

//js

bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
				log('JS got response', response)
			})

//oc
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];

UIWebView里面还看到了mac OS平台的处理,其实质跟这个也是一样的,有兴趣的同学可以自行研究啊。

由于UIWebView和WKWeb到WebViewJavascriptBridgeBase层的实现原理什么的基本上是一致的,我这里就以WKWebView精心给讲解了

WebViewJavascriptBridgeBase的实现分析

前文已经说过,该框架里面有好多地方oc和js是相对称的,有很多类似的实现,现在就先引用几个对比一下

这个是注册handler的方法

//js
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)
		})
		
		
//oc		
id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
        NSLog(@"testJavascriptHandler responded: %@", response);
    }];

js调用oc和oc调用js时候都各自维护了一套消息对列队列,回调

var messageHandlers = {}; //消息
var responseCallbacks = {}; //回调


@property (strong, nonatomic) NSMutableDictionary* responseCallbacks;
@property (strong, nonatomic) NSMutableDictionary* messageHandlers;

相互交互的消息内容

//这是js发给oc的
{
    callbackId = "cb_1_1514520891115";
    data =     {
        foo = bar;
    };
    handlerName = testObjcCallback;
}

//这是oc发给js的
{
    callbackId = "objc_cb_1";
    data =     {
        greetingFromObjC = "Hi there, JS!";
    };
    handlerName = testJavascriptHandler;
}

send方法也跟双胞胎似的,傻傻的不清楚啊

//js

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;
}
	
	
//oc
- (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];
}

下面来看一看我们大Objective-c 调用JavaScript部分的实现过程

1、原生按钮回调bridge的方法- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback将消息发出去

- (void)callHandler:(id)sender {
    id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
    [_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
        NSLog(@"testJavascriptHandler responded: %@", response);
    }];
}

2、调用WebViewJavascriptBridgeBase的方法- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)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];
}

send 方法将oc传个js的数据组装成特定的json格式,如下所示:

{
    callbackId = "objc_cb_1";
    data =     {
        greetingFromObjC = "Hi there, JS!";
    };
    handlerName = testJavascriptHandler;
}

3、将组好的数据向下传递调用方法- (void)_queueMessage:(WVJBMessage*)message

- (void)_queueMessage:(WVJBMessage*)message {
     //self.startupMessageQueue这个是初始化的消息队列,一般没有自定义初始化消息队列的话这个就是nil,直接就走到else里面去了
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

4、调用方法- (void)_dispatchMessage:(WVJBMessage*)message序列化消息,并在主线程中转发 序列化后的样板啊 {\"callbackId\":\"objc_cb_1\",\"data\":{\"greetingFromObjC\":\"Hi there, JS!\"},\"handlerName\":\"testJavascriptHandler\"} 然后会调用_evaluateJavascript方法,实质上这个地方是通过代理去调用不同的webview的执行js的方法 UIwebview会调用 - (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

WKWebView会调用 - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

5、此举可以用oc来调用js方法,此处就调到了js的方法WebViewJavascriptBridge._handleMessageFromObjC() 看看它的代码啊

function _dispatchMessageFromObjC(messageJSON) {
		if (dispatchMessagesWithTimeoutSafety) {
			setTimeout(_doDispatchMessageFromObjC);
		} else {
			 _doDispatchMessageFromObjC();
		}
		
		function _doDispatchMessageFromObjC() {
		  //将json字符串转换成json对象(可以理解为oc中的字典对象)
			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 });
					};
				}
				//查找js中注册过的方法,若没有js注册此方法则报错,反之取出储存的该方法,并调用之
				var handler = messageHandlers[message.handlerName];
				if (!handler) {
					console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
				} else {
					handler(message.data, responseCallback);
				}
			}
		}
	}

此时执行方法_doDispatchMessageFromObjC时会走到else这一步,如果oc调的这个方法需要回调,则message.callbackId不会为undefined,则js会调用_doSend方法回调oc,完成之后调用回调函数

6、上面方法完成最后一步是send方法了 先看看这个回调函数实现

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;
}

此处由于本身就是oc调用js的回调,没有再js调用oc后回调js,则responseCallback为undefined,直接将其加入消息队列中,并调用messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE来调用原生,这个调法感觉有些奇怪,但从现象和我的理解来看就是给iframe加了一个src,类似于load了一个特殊的url 即https://__wvjb_queue_message__/

7、WKWebview的代理方法- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler会拦截到这个url

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    //获取js “loadUrl”的url链接
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    if ([_base isWebViewJavascriptBridgeURL:url]) {//是不是WebViewJavascriptBridge约定的url
        if ([_base isBridgeLoadedURL:url]) {//是不是初始化指令__bridge_loaded__
            //注入核心js代码
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {//是不是消息指令__wvjb_queue_message__
            //调用WebViewJavascriptBridgeBase的API去分发消息
            [self WKFlushMessageQueue];
        } else {//未知的url
            [_base logUnkownMessage:url];
        }
        //取消跳转
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    //能走到这里证明已经不是WebViewJavascriptBridge约定的url了,做正常跳转
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

这里截取到的url是__wvjb_queue_message__则会调用方法- (void)WKFlushMessageQueue

8、分发消息

- (void)WKFlushMessageQueue {

//该方法首先会调用webViewJavascriptFetchQueyCommand 方法,这个方法是在js中 调用_fetchQueue()这个方法用来获取queue中消息sendMessageQueue
webViewJavascriptFetchQueyCommand
[_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];
    }];
}

sendMessageQueue是一个在js中维护的消息队列,是一个数组sendMessageQueue拿给oc然后将数据清空,在上面的这个oc函数evaluateJavaScript中result就是该js方法的返回值,即消息队列[{"handlerName":"testJavascriptHandler","responseId":"objc_cb_4","responseData":{"Javascript Says":"Right back atcha!"}}] 9、查找js中维护的消息对,只有匹配上了才能调用上

function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    return messageQueueString;
}

oc一旦取到了js给返回的值,就会调用方法- (void)flushMessageQueue:(NSString *)messageQueueString

- (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) {
                    // Do nothing
                };
            }
            //在这里匹配一波,要是取到了就搞起啊
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            
            handler(message[@"data"], responseCallback);
        }
    }
}

有responseId则证明是回调方法返回,然后就是在oc中的_responseCallbacks返回回调方法中找到该回调block,并回调相应的方法

这就是oc调用js的流程:大概总结如下 oc 告诉js我要发交互发消息了 ==> js 获取到通知,并主动去“load” __wvjb_queue_message__ 告诉oc把消息的内容传过来

oc 得知js已经知道要传递消息了,主动调用js中的方法WebViewJavascriptBridge._handleMessageFromObjC()并在这个方法里面将消息以字符串的形式传过去 ==> js拿到消息内容后进行解析,在js上下文中保存的消息名中进行匹配,得到js的调用方法,并调用该方法

JavaScript调用objective-c的方法

看完oc调用js的整个流程以后,再来看js调用oc的流程就明晰了很多,现在作如下讲解:

1、js中的按钮首先会触发器onclick事件,然后调用bridge的方法callHandler,

function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({
            handlerName: handlerName,
            data: data
        }, responseCallback);
    }

2、callHandler在做了简单的参数处理后转而调用核心函数_doSend方法

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方法负责组装参数,并保存到上下文中,然后就“loadUrl”了

3、接下来就是wkwebview代理方式发光的时候了,- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler拦截到约定好的url https://__wvjb_queue_message__/

4、是时候调用一波原生方法- (void)WKFlushMessageQueue来获取消息队列了

5、调用方法- (void)flushMessageQueue:(NSString *)messageQueueString该方法中处理序列化的字符串变成数组,遍历消息队列,查找到oc中已经注册好的对应的方法,匹配成功后调用该方法,则会调到注册处的回调方法

完成相应处理,并回调其callback

6、此时处理回调的msg,包装好后插入到oc需要处理的消息队列

7、处理消息,将字典转换成json字符串,调用方法WebViewJavascriptBridge._handleMessageFromObjCjs方法将oc的数据传递给js

8、紧接着就是js方法调用_handleMessageFromObjC() ==> _handleMessageFromObjC() ==> _doDispatchMessageFromObjC()

9、然后就是找到注册过的回调方法,回调相关的函数

这便是js调用oc并获取回调的流程。是不是觉得oc ==> js ==> oc 和 js==> oc ==> js 两个流程很相似,可以说是完美对称了?这也就是开头所说的对称美啊!

WebViewJavascriptBridge初始化过程

html代码里面可是必备的哈,熟悉使用WebViewJavascriptBridge框架的痛惜应该是比较熟悉的了

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)
}

html已加载后就会一把它,它会“loadUrl” https://__bridge_loaded__/load了这个后,

这时,我们会injectJavascriptFile,将WebViewJavascriptBridge_JS.m中的js注入到web运行的上下文中,然后检查startupMessageQueue,看有没有初始化时候需要调用什么方法(我理解应该是这样的,方便自定义一些什么初始化方法什么的),默认这个是nil,也就不会执行下面的内容

注入js后,紧接着就是执行js脚本了,来断点一波

我们看到也就是一波初始化了,然后就是注册方法_disableJavascriptAlertBoxSafetyTimeout这个东西,暂时木有用过啊

这就是我研读WebViewJavascriptBridge框架源码的笔记了,大神看了勿喷啊。以后还有我在公司项目中的关于wkwebview开发的一下心得,近期会总结一波,谢谢亲的耐心阅读啊,哪里有问题的可以私信我了~