JSBridge原理解析——以WebviewJavascriptBridge实现方式为例

6,496 阅读5分钟

一、什么是JSBridge?

JSBridge是一种webview侧和native侧进行通信的手段,webview可以通过jsb调用native的能力,native也可以通过jsb在webview上执行一些逻辑。

二、JSB的实现方式

在比较流行的JSBridge中,主要是通过拦截URL请求来达到native端和webview端相互通信的效果的。

这里我们以比较火的WebviewJavascriptBridge为例,来解析一下它的实现方式。

源码地址:github.com/marcuswesti…

2-1、在native端和webview端注册Bridge

注册的时候,需要在webview侧和native侧分别注册bridge,其实就是用一个对象把所有函数储存起来。

function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}

2-2、在webview里面注入初始化代码

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

这段代码主要做了以下几件事:

(1)创建一个名为WVJBCallbacks的数组,将传入的callback参数放到数组内

(2)创建一个iframe,设置不可见,设置src为 https://__bridge_loaded__

(3)设置定时器移除这个iframe

2-3、在native端监听URL请求

iOS中有两种webview,一种是UIWebview,另一种是WKWebview,这里以WKWebview为例:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    if (webView != _webView) { return; }

    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationResponse:decisionHandler:)]) {
        [strongDelegate webView:webView decidePolicyForNavigationResponse:navigationResponse decisionHandler:decisionHandler];
    }
    else {
        decisionHandler(WKNavigationResponsePolicyAllow);
    }
}

这段代码主要做了以下几件事:

(1)拦截了所有的URL请求并拿到url

(2)首先判断isWebViewJavascriptBridgeURL,判断这个url是不是webview的iframe触发的,具体可以通过host去判断。

(3)继续判断,如果是isBridgeLoadedURL,那么会执行injectJavascriptFile方法,会向webview中再次注入一些逻辑,其中最重要的逻辑就是,在window对象上挂载一些全局变量和WebViewJavascriptBridge属性,具体值如下:

window.WebViewJavascriptBridge = {
	registerHandler: registerHandler,
	callHandler: callHandler,
	disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
	_fetchQueue: _fetchQueue,
	_handleMessageFromObjC: _handleMessageFromObjC
};

var sendMessageQueue = [];
var messageHandlers = {};

var responseCallbacks = {};
var uniqueId = 1;

(4)继续判断,如果是isQueueMessageURL,那么这就是个处理消息的回调,需要执行一些消息处理的方法(第四步会详细讲)

2-4、webview调用native能力

当native和webview都注册好了Bridge之后,双方就可以互相调用了,这里先介绍webview调用native能力的过程。

2-4-1、webview侧callHandler

当webview调用native时,会调用callHandler方法,这个方法具体逻辑如下:

bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
	console.log("JS received response:", responseData)
})

function callHandler(handlerName, data, responseCallback) {
	if (arguments.length == 2 && typeof data == 'function') {
		responseCallback = data;
		data = null;
	}
	_doSend({ handlerName: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_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

实际上就是先生成一个message,然后push到sendMessageQueue里,然后更改iframe的src。

2-4-2、native侧flushMessageQueue

然后,当native端检测到iframe src的变化时,会走到isQueueMessageURL的判断逻辑,然后执行WKFlushMessageQueue函数,获取到JS侧的sendMessageQueue中的所有message。

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

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

当一个message结构存在responseId的时候说明这个message是执行bridge后传回的。取不到responseId说明是第一次调用bridge传过来的,这个时候会生成一个返回给调用方的message,其reponseId是传过来的message的callbackId,当native执行responseCallback时,会触发_dispatchMessage方法执行webview环境的的js逻辑,将生成的包含responseId的message返回给webview。

2-4-3、webview侧handleMessageFromObjC

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

如果从native获取到的message中有responseId,说明这个message是JS调Native之后回调接收的message,所以从一开始sendData中添加的responseCallbacks中根据responseId(一开始存的时候是用的callbackId,两个值是相同的)取出这个回调函数并执行,这样就完成了一次JS调用Native的流程。

2-4-4、过程总结

过程如下图:

截屏2021-06-14 23.28.32.png

1、native端注册jsb

2、webview侧创建iframe,设置src为__bridge_load__

3、native端捕获请求,注入jsb初始化代码,在window上挂载相关对象和方法

4、webview侧调用callHandler方法,并在responseCallback上添加callbackId: responseCallback,并修改iframe的src,触发捕获

5、native收到message,生成一个responseCallback,并执行native侧注册好的方法

6、native执行完毕后,通过webview执行_handleMessageFromObjC方法,取出callback函数,并执行

2-5、native调用webview能力

native调用webview注册的jsb的逻辑是相似的,不过就不是通过触发iframe的src触发执行的了,因为Native可以自己主动调用JS侧的方法。其具体过程如下图:

截屏2021-06-14 23.28.26.png

1、native侧调用callHandler方法,并在responseCallback上添加callbackId: responseCallback

2、native侧主动调用_handleMessageFromObjC方法,在webview中执行对应的逻辑

3、webview侧执行结束后,生成带有responseId的message,添加到sendMessageQueue中,并修改iframe的src为__wvjb_queue_message__

4、native端拦截到url变化,调用webview的逻辑获取到message,拿到responseId,并执行对应的callback函数

作者:赵越