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

downloadWebViewJavascriptBridgeAfter 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.
  • WKWebViewJavascriptBridgeWebViewJavascriptBridge: WKWebViewwithUIWebViewCorresponding 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_JSInside 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 parametercallbackPhi 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 goWKWebViewJavascriptBridgesee- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandlerWhat 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)}),

  • callWebViewJavascriptBridge_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 pagesrcProperty 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 *)messageQueueStringThe 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…