Summary of a.
It is inevitable for client development to deal with WebView, especially for Hybrid App. Under the background that H5 accounts for an increasing proportion, a good SET of API for WebView and native interaction is particularly important. Of course, there are mature three-party libraries at both ends to support it. For example, JsBridge for Android and WebViewJavascriptBridge for iOS, but the author has a little knowledge of their internal principles. As a result, sometimes when faced with problems, he is at a loss to start. Finally, he decides to analyze the internal implementation principles of WebViewJavascriptBridge. One is to improve their own source reading level, and then also hope to help in the future work.
Ii. Basic principles
downloadWebViewJavascriptBridge
After the source code can see its files are not much, respectively on a few files to do a simple introduction, after detailed analysis of its source code
WebViewJavascriptBridge_JS
: JS bridge file, through it to achieve the initialization of the JS environment, which is a C function, return JS methods. Native JS methods and their corresponding method callbacks need to be registered here.WKWebViewJavascriptBridge
与WebViewJavascriptBridge
:WKWebView
withUIWebView
Corresponding bridge file. Native methods called by JS and their corresponding method callbacks need to be registered here first.WebViewJavascriptBridgeBase
: Bridge base file. It implements initialization of the native environment, as well as initialization of the method storage container, and of course, of theWebViewJavascriptBridge_JS
Inside the JS method call.
Three. Source code analysis
With a general understanding of the functions of the above classes, we analyze the internal implementation logic through the source code. We’ll WebViewJavascriptBridgeDemo, for example.
1. JS calls the OC method
(1) OC environment initialization and method registration
How to implement JS to call OC method, first need to initialize the current OC environment
// ExampleWKWebViewController _bridge = [WebViewJavascriptBridge bridgeForWebView:webView]; [_bridge setWebViewDelegate:self]; [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) { NSLog(@"testObjcCallback called: %@", data); responseCallback(@"Response from testObjcCallback"); }]; . // WKWebViewJavascriptBridge + (instancetype)bridgeForWebView:(WKWebView*)webView { WKWebViewJavascriptBridge* bridge = [[self alloc] init]; [bridge _setupInstance:webView]; [bridge reset]; return bridge; }... // WKWebViewJavascriptBridge - (void) _setupInstance:(WKWebView*)webView { _webView = webView; _webView.navigationDelegate = self; _base = [[WebViewJavascriptBridgeBase alloc] init]; _base.delegate = self; }Copy the code
-
[WebViewJavascriptBridge bridgeForWebView:webView]; : look at the call stack, this method can clearly see their role is to initialize the WKWebViewJavascriptBridge, and instantiate the corresponding WebViewJavascriptBridgeBase, and bind their respective agents, ultimately achieve the goal of initialization OC call environment.
-
– (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; If you want to achieve the purpose of JS calling the native method, then you must register the native method, and this is the corresponding registration method. Let’s see what he does inside:
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
Copy the code
It’s very simple, but the current Block is stored in the messageHandlers dictionary so that when the JS side calls, the method name finds the corresponding implementation.
(2) JS environment initialization and method triggering
From – (void)loadExamplePage:(WKWebView*)webView method load web pages from the current webView. Take a look at the core method in exampleApp.html:
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)
}
setupWebViewJavascriptBridge(function(bridge) {
var uniqueId = 1
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) }
}
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)
})
document.body.appendChild(document.createElement('br'))
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
e.preventDefault()
log('JS calling handler "testObjcCallback"')
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}
})
Copy the code
setupWebViewJavascriptBridge(callback)
Is the core method that is called first when the webView loads the HTML. This method takes one parametercallback
Phi is also a function. Let’s look at this method:
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)
}
Copy the code
Page loading for the first time window. WebViewJavascriptBridge and window. WVJBCallbacks are false, the window. The WVJBCallbacks assignment for array contains the callback, The callback is a function(bridge)…. Next create WVJBIframe, which you can think of as a blank page, to set SRC = ‘https://__bridge_loaded__’; .
Note that the SRC attribute is critical. When we set the SRC attribute for a web page, the link will be captured by our webView on the OC side. Agents to invoke the webView approach – (void) webView: (WKWebView *) webView decidePolicyForNavigationAction: (WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler,
Load the current blank page in order to trigger OC’s proxy method and remove it immediately.
- So let’s go
WKWebViewJavascriptBridge
see- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
What the proxy method does after intercepting the request.
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;
}
Copy the code
First determines whether the current URL is __wvjb_queue_message__ or __bridge_loaded__, just trigger URL is __bridge_loaded__ invokes WebViewJavascriptBridgeBase – (void) injectJavascriptFile method.
- (void)injectJavascriptFile {
// 获取JS字符串
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];
}
}
}
....
- (void) _evaluateJavascript:(NSString *)javascriptCommand {
[self.delegate _evaluateJavascript:javascriptCommand];
}
....
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
return NULL;
}
Copy the code
As you can see from the above method call, the last one is the WebViewJavascriptBridge_js(); JS method string, through method [_webView evaluateJavaScript: javascriptCommand completionHandler: nil] into the webView and execution. To initialize the Brige of the javascript environment.
- WebViewJavascriptBridge_js() method parsing
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
var messagingIframe;
// List of messages to be sent to native
var sendMessageQueue = [];
// Store the JS methods registered in the bridge
var messageHandlers = {};
// The URL to jump to
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
// the JS method callback
var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;
// OC calls JS methods that need it to register
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
//JS calls the method entry of OC
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;
}
// To send a message to the native
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
// Trigger the webView proxy to parse the JS message
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ': / /' + QUEUE_HAS_MESSAGE;
}
// Convert the message to a JSON string
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
function _dispatchMessageFromObjC(messageJSON) {... }// It is called by the native, and JS uses it for message distribution
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);
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) { callbacks[i](WebViewJavascriptBridge); }}Copy the code
First, the entire WebViewJavascriptBridge_js is the execution of a JS method. First, the WebViewJavascriptBridge on the JS side is created and assigned to the window.
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
Copy the code
registerHandler
: directly corresponds to the followingregisterHandler(handlerName, handler)
Method, through it we can be OC call JS method registration, see its implementation is relatively simple
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
Copy the code
We store the JS method implementation in messageHandlers as handleName.
callHandler
: Corresponds to the followingcallHandler(handlerName, data, responseCallback)
Method, through which we can directly initiate the call to OC method, the specific call logic is analyzed in the following.disableJavscriptAlertBoxSafetyTimeout
: Timeout switch for callback. The default value is false_fetchQueue
: Serializes the javascript environment’s methods into JSON strings and returns them to the OC side_handleMessageFromObjC
: handle the OC method sent to the javascript environment,_dispatchMessageFromObjC(messageJSON)
The argument of this method is the message message of OC calling JS. This method parses messageJSON and calls the corresponding JS method.
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ': / /' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
Copy the code
SRC is https://wvjb_queue_message, and this code means to send the javascript message to the OC immediately.
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) { callbacks[i](WebViewJavascriptBridge); }}Copy the code
At the end of WebViewJavascriptBridge_js is the code above, which calls the callBack method in exampleApp.html, which is this
setupWebViewJavascriptBridge(function(bridge) {... })Copy the code
Initialize the JS environment and load exampleApp.html.
(3) PROCESS of JS calling OC method
- Click the JS button to trigger the method below
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
Copy the code
Function (response) {log(‘JS got response’, response)}),
- call
WebViewJavascriptBridge_js
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;
}
Copy the code
The core method is _doSend(), the input parameter message is the name of the OC method and the parameter responseCallback is the OC callback method, and the responseCallback is the responseCallbacks method. Add a callbackId to the message object message, and set the SRC property of messagingIframe. Agents to be ebView method – (void) webView: (WKWebView *) webView decidePolicyForNavigationAction: (WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
- In the proxy method above, the intercepted URL is
__wvjb_queue_message__
, so call the method:
- (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]; }]; }... - (NSString *)webViewJavascriptFetchQueyCommand { return @"WebViewJavascriptBridge._fetchQueue();" ; }Copy the code
WebView triggers JS webViewjavascriptBridge._fetchQueue (),
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
Copy the code
This method changes sendMessageQueue to a JSON string and returns it to the OC environment.
- (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; NSLog(@"messageQueueString===%@",messageQueueString); 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; ResponseCallback (message[@"data"], responseCallback); }}}Copy the code
This method is the core method of OC side processing JS, deserialize messageQueueString, get the message array
( { callbackId = "cb_1_1639553291614"; data = { foo = bar; }; handlerName = testObjcCallback; })Copy the code
ResponseCallbackBlock, which packages the received parameter with the callbackId and sends it to the JS environment. And call the javascript environment WebViewJavascriptBridge._handleMessageFromObjC(messageJSON); Method to parse messageJSON. This is just an implementation of the Block, it’s not calling the Block, it’s calling it down here
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]]; if (! handler) { NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message); continue; ResponseCallback (message[@"data"], responseCallback);Copy the code
Handler (message[@”data”], responseCallback); Call responseCallback(@”Response from testObjcCallback”); Sends a callback to the JS environment and passes the parameters.
-
The JS environment gets the messageJSON from the _handleMessageFromObjC(messageJSON) method and parses it.
function _dispatchMessageFromObjC(messageJSON) { if (dispatchMessagesWithTimeoutSafety) { setTimeout(_doDispatchMessageFromObjC); } else { _doDispatchMessageFromObjC(); } function _doDispatchMessageFromObjC() { // Convert to object var message = JSON.parse(messageJSON); var messageHandler; var responseCallback; // This responseId is the responseId stored when JS calls OC 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); }}}}Copy the code
ResponseId (this responseId is the callbackId saved by JS calling OC method), find the corresponding method implementation, and call
function(response) { log('JS got response', response) } Copy the code
This completes the whole process of JS calling OC and OC calling back JS and passing arguments.
2. OC calls the JS method
Similar to the above, let’s look at the implementation of OC active call JS method
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }]; . - (void)callHandler:(NSString *)handlerName data:(id)data { [self callHandler:handlerName data:data responseCallback:nil]; } - (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]; }... - (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]; NSLog(@"javascriptCommand==%@",javascriptCommand); if ([[NSThread currentThread] isMainThread]) { [self _evaluateJavascript:javascriptCommand]; } else { dispatch_sync(dispatch_get_main_queue(), ^{ [self _evaluateJavascript:javascriptCommand]; }); }}Copy the code
If there is a callback function, store it in responseCallbacks. If there is a callback function, store it in responseCallbacks. The callbackId is generated and the entire message is packaged and sent to the WebViewJavascriptBridge._handleMessageFromObjC(messageJSON) of the JS environment. Analysis, the analysis process has been introduced above, here is no longer described.
4. To summarize
Through the above process analysis, the implementation principle of the whole WebViewJavascriptBridge is relatively clear.
- JS registers a method to the bridge of the JS environment. OC calls the core method of JS
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
After the JS environment receives the message, the method is passedWebViewJavascriptBridge._handleMessageFromObjC();
The message is parsed, invoking the methods registered in the Bridge. - OC registers the method to the BRIDGE of OC environment. The core logic of JS calling OC is to set the blank page
src
Property to be used by the webView’s proxy method- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
, OC through the core method- (void)flushMessageQueue:(NSString *)messageQueueString
The data will be passed for parsing. - In both cases, the method is implemented by method name, and the callback function is found by ID.
Read more source code is good on the one hand can improve their reading level of source code, on the other hand can learn some good design ideas of the author.
Reference:
Github.com/marcuswesti…
Github.com/lzyzsd/JsBr…
Juejin. Cn/post / 684490…