Close to New Year’s day in the past two days, things a little less, some of the time, just write something, from the best mix of iOS development write up, due to the iOS development experience less than four years, also make the front end of a year and a half, the lack of some knowledge is likely to be accumulated, ability is insufficient, the level is limited, may have the false hope readers find a timely and correct, thank you very much.
As for the introduction of WebviewJavascriptBridge, no longer worded here, now that you can see this article, compared to the three-party library is more or less familiar with it. I would like to make it clear that the demo involved in this article is directly taken from the WebviewJavascriptBridge, without any modification, and directly used for research
Look at the popular WebviewJavascriptBridge this tripartite library source, found a lot of js and oc part of the core code is almost symmetric, so I think it is best to read js and oc code together, so that it is easier to understand, can also find its symmetrical beauty…
The javascript code loaded with oc is very hard to find (at least I spent some time to find it). Don’t panic, if you can’t find it, search for WebviewJavascriptBridge in the search bar. Then set breakpoints on the corresponding code output, which is ready to study
For those of you who don’t know how to open Safari debug mode, please go to the portal
WebViewJavascriptBridge VS WKWebViewJavascriptBridge
This framework is still a little bit 666, it supports both iOS and MAC OS but since we don’t use MAC OS much, we’ll just go to the iOS section
The red line is the core code of the WebviewJavascriptBridge framework
Core WebViewJavascriptBridge_JS = = > js code section, responsible for the js end message assembly, forwarding WebViewJavascriptBridgeBase = = > oc core code section, responsible for oc end assembly, Forwarding WebViewJavascriptBridge ==> encapsulates UIWebView, Is based on the WebViewJavascriptBridgeBase WKWebViewJavascriptBridge = = > for WKWebView encapsulation, is based on the WebViewJavascriptBridgeBase
As for the first two core classes will be explained in detail in the next section, this summary will only do the analysis of the latter two classes directly above
Contrast WebViewJavascriptBridge and WKWebViewJavascriptBridge two class header file, see WKWebViewJavascriptBridge one more reset method, other methods of two classes almost a MAO, We continue to look at. M implementation file also confirmed that, the difference only lies in the realization of the webview, it also confirms the core of this framework is WebViewJavascriptBridgeBase, its core is through to js “loadUrl (” this is my own habit so to speak, easy to understand, The webView then intercepts the specified URL in the proxy method and processes the message.
The following is a proxy method intercept of WKWebView
- (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]) {/ / url is WebViewJavascriptBridge agreedif([_base isBridgeLoadedURL:url]) {// Is not the initialization instruction __bridge_loaded__ // inject core JAVASCRIPT code [_base injectJavascriptFile]; }else if([_base isQueueMessageURL: url]) {/ / whether the message instruction __wvjb_queue_message__ WebViewJavascriptBridgeBase / / call API to distribute news [the self WKFlushMessageQueue]; }else{// Unknown url [_baselogUnkownMessage:url]; } / / cancel jump decisionHandler (WKNavigationActionPolicyCancel);return; } // this is not the url specified by WebViewJavascriptBridgeif (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
} else{ decisionHandler(WKNavigationActionPolicyAllow); }}Copy the code
Here are the methods of UIWebView proxy
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if(webView ! = _webView) {returnYES; } NSURL *url = [request url]; __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;if([_base isWebViewJavascriptBridgeURL: url]) {/ / url is WebViewJavascriptBridge agreedif([_base isBridgeLoadedURL:url]) {// Is not the initialization instruction __bridge_loaded__ // inject core JAVASCRIPT code [_base injectJavascriptFile]; }else if([_base isQueueMessageURL:url]) {// Is not a message directive __wvjb_queue_message__ NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; / / call WebViewJavascriptBridgeBase API to distribute news [_base flushMessageQueue: messageQueueString]; }else{// Unknown url [_baselogUnkownMessage:url]; } // Cancel the jumpreturn NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
returnYES; }}Copy the code
Js calls 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");
}];
Copy the code
In UIWebView, you also see the MAC OS processing, which is essentially the same thing, so if you’re interested, you can do your own research.
Because of the UIWebView and WKWeb to WebViewJavascriptBridgeBase what principle is basically consistent, I here to WKWebView carefully to explain
The realization of the WebViewJavascriptBridgeBase analysis
As mentioned above, there are many places in the framework where OC and JS are relative, and there are many similar implementations, so let’s start with a few references for comparison
This is the method that registers the 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);
}];
Copy the code
When JS calls OC and OC calls JS, they maintain a queue of message pairs, callbacks respectively
var messageHandlers = {}; Var responseCallbacks = {}; @property (strong, nonatomic) NSMutableDictionary* responseCallbacks; @property (strong, nonatomic) NSMutableDictionary* messageHandlers;Copy the code
The contents of messages that interact with each other
{callbackId = {callbackId =}"cb_1_1514520891115";
data = {
foo = bar;
};
handlerName = testObjcCallback; } // This is what oc sends to js {callbackId ="objc_cb_1";
data = {
greetingFromObjC = "Hi there, JS!";
};
handlerName = testJavascriptHandler;
}
Copy the code
Send methods are also like twins
//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];
}
Copy the code
Let’s take a look at the implementation of our big Objective-C JavaScript call
Void callHandler:(NSString *)handlerName data:(id)data ResponseCallback :(WVJBResponseCallback)responseCallback sends out the message
- (void)callHandler:(id)sender {
id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];
}
Copy the code
2, call the WebViewJavascriptBridgeBase method – (void) sendData publishes the event: (id) data responseCallback: (responseCallback WVJBResponseCallback) HandlerName :(NSString*)handlerName assembles a wave of data
- (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];
}
Copy the code
The send method assembles the data sent from OC to JS into a specific JSON format, as shown below:
{
callbackId = "objc_cb_1";
data = {
greetingFromObjC = "Hi there, JS!";
};
handlerName = testJavascriptHandler;
}
Copy the code
– (void)_queueMessage:(WVJBMessage*)message
- (void) _queueMessage message: (WVJBMessage *) {/ / self. StartupMessageQueue this is to initialize the message queue, generally there is no custom initialization message queue this is nil, go directly to theelseIt went to theif (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else{ [self _dispatchMessage:message]; }}Copy the code
– (void)_dispatchMessage:(WVJBMessage*)message {\”callbackId\”:\”objc_cb_1\”,\”data\”:{\”greetingFromObjC\”:\”Hi there, JS! \”},\”handlerName\”:\”testJavascriptHandler\”} will then call the _evaluateJavascript method, This place is in essence through a proxy to invoke a different method of the implementation of the webview js UIwebview calls – (nullable nsstrings *) stringByEvaluatingJavaScriptFromString: (nsstrings *)script;
EvaluateJavaScript (NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
WebViewJavascriptBridge._handleMessageFromObjC()
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjCParse () {// Convert json string to JSON object (can be interpreted as oc dictionary object) 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{// Direct interaction calls go to this methodif (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); }}}}Copy the code
The execution method _doDispatchMessageFromObjC will go else this step, if the oc this method needs the callback, the message. The callbackId for undefined, js will call _doSend method callback oc, Call the callback function when done
Send (); send (); 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;
}
Copy the code
ResponseCallback = responseCallback (); / / return (); / / return (); SRC = CUSTOM_PROTOCOL_SCHEME + ‘://’ + QUEUE_HAS_MESSAGE to call the native iframe. Similar to loading a special URL https://__wvjb_queue_message__/
7 agents, WKWebview method – (void) webView: (WKWebview *) webView decidePolicyForNavigationAction: (WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler will intercept this url
- (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]) {/ / url is WebViewJavascriptBridge agreedif([_base isBridgeLoadedURL:url]) {// Is not the initialization instruction __bridge_loaded__ // inject core JAVASCRIPT code [_base injectJavascriptFile]; }else if([_base isQueueMessageURL: url]) {/ / whether the message instruction __wvjb_queue_message__ WebViewJavascriptBridgeBase / / call API to distribute news [the self WKFlushMessageQueue]; }else{// Unknown url [_baselogUnkownMessage:url]; } / / cancel jump decisionHandler (WKNavigationActionPolicyCancel);return; } // this is not the url specified by WebViewJavascriptBridgeif (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
} else{ decisionHandler(WKNavigationActionPolicyAllow); }}Copy the code
The truncated URL is __wvjb_queue_message__ and the method – (void)WKFlushMessageQueue is called
8. Distribute the message
- (void) WKFlushMessageQueue {/ / this method will first call webViewJavascriptFetchQueyCommand method, This method is invoked in js _fetchQueue () the method used to retrieve messages in the 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];
}];
}
Copy the code
SendMessageQueue is a message queue maintained in JS. It’s an array. SendMessageQueue takes it to oc and cleans it up. [{“handlerName”:”testJavascriptHandler”,” objC_CB_4 “,”responseData”:{“Javascript Says”:”Right back” Atcha!”}}] 9, find js maintenance of the message pair, only the match can be called
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
Copy the code
The oc calls the method – (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) {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); }}}Copy the code
If the responseId is present, the responseId is returned by the responseId method, and then the _responseCallbacks callback block is found in oc and the corresponding method is called back
Oc tells JS I’m going to send an interactive message ==> JS gets the notification and “loads” __wvjb_queue_message__ to tell OC to pass the message
Oc knows that JS already knows to deliver the message, and actively calls the method webviewjavascriptBridge. _handleMessageFromObjC() in JS and passes the message as a string in this method. ==> JS gets the message content and pars it. Matches in the message name saved in the JS context, gets the js calling method, and invokes the method
JavaScript calls objective-C methods
After looking at the whole process of OC calling JS, then look at the process of JS calling OC is very clear, and now make the following explanation:
1. The js button triggers the onClick event and then calls the Bridge method callHandler.
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
Copy the code
2. The callHandler calls the core _doSend method after doing simple parameter processing
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 _doSend method is responsible for assembling the parameters, saving them to the context, and then “loadUrl”
3, next is the WKWebView proxy shine time, – (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction DecisionHandler :(void (^)(WKNavigationActionPolicy))decisionHandler intercepts the specified url https://__wvjb_queue_message__/
4. It is time to call a wave of native methods – (void)WKFlushMessageQueue to get the message queue
(void)flushMessageQueue:(NSString *)messageQueueString :(void)flushMessageQueue:(NSString *)messageQueueString :(void)flushMessageQueue:(NSString *)messageQueueString Is called to the registry’s callback method
Completes the corresponding processing and calls back its callback
7. Process the message, convert the dictionary into A JSON string, and call the WebViewJavascriptBridge._handleMessageFromObjCjs method to pass oc data to JS
9. The next step is to find the registered callback method and the related callback function
This is how js calls oc and gets the callback. Oc ==> js==> oc ==> js==> oc ==> js==> oc ==> js This is also the symmetrical beauty mentioned at the beginning!
WebViewJavascriptBridge initialization process
HTML code inside but necessary ha, familiar with the use of WebViewJavascriptBridge framework should be more familiar with the lament
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
Once the HTML is loaded, it will load it, it will “loadUrl” https://__bridge_loaded__/load after this,
At this point, we injectJavascriptFile, inject js from WebViewjavascriptBridge_js. m into the context of the Web run, and then check startupMessageQueue, See if there’s any method that needs to be called when you initialize it, but by default this is nil, so it’s not going to do anything like this, right
We see is a wave of initialization, and then there are the registration method _disableJavascriptAlertBoxSafetyTimeout this stuff, temporarily have not used
This is my reading of WebViewJavascriptBridge framework source notes, big god see do not spray ah. In the future, THERE will be my experience about the development of WKWebView in the company project, and I will summarize a wave in the near future. Thank you for your patience in reading ah, where there is a problem can be private message to me ~