1

一、我们先看看如何使用OC调用JS中的方法。

注:我们以wkwebview为例。下面的代码都是针对于wkwebview的。

1.先创建一个按钮和WKWebViewJavascriptBridge对象

UIButton *callbackButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [callbackButton setTitle:@"原生调用JS" forState:UIControlStateNormal];
    [callbackButton addTarget:self action:@selector(callJSMethod:) forControlEvents:UIControlEventTouchUpInside];
    ...
    _bridge = [WKWebViewJavascriptBridge bridgeForWebView:webView];

2.按钮的消息处理函数

- (void)callJSMethod:(id)sender {
    id data = @{ @"原生调用JS参数1": @"参数1" };
    [_bridge callHandler:@"JSMethod1" data:data responseCallback:^(id response) {
        NSLog(@"testJavascriptHandler responded: %@", response);
    }];
}

3.调用WKWebViewJavascriptBridge中的callHandler方法

- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

4.WKWebViewJavascriptBridge中包含一个WebViewJavascriptBridgeBase对象_base。继续调用栈的跟踪。

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

这个方法写的很清晰,把要调用的js的函数名handlerName,参数data,和回调方法的Id(callbackId)打包到一个字典对象message中。callbackId,每个回调一个,唯一。为什么用callbackId,因为block本身是一个对象这个对象JS识别不了。其实传过去意义也不是很大,只要把这个block放在WebViewJavascriptBridgeBase对象中的responseCallbacks字典中就行,key就是刚才生成的callbackId。然后继续调用下面的方法。

5.把消息放入队列中。

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

这里其实没有放入队列,而是直接分发了消息,稍后会说为什么这里self.startupMessageQueue为nil。

6.把消息发送给Web环境

- (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];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

首先把WVJBMessage对象message串行化为JSON字符串,然后转义字符串里的字符;生成JS的命令字符串;在主线程中执行js命令。为什么要在主线程中执行,苹果文档中有这么一句话:The WebKit framework is not thread-safe. If you call functions or methods in this framework, you must do so exclusively on the main program thread。

7.最终把命令传给WebView来执行

//WebViewJavascriptBridgeBase.m
- (void) _evaluateJavascript:(NSString *)javascriptCommand {
    [self.delegate _evaluateJavascript:javascriptCommand];
}
//WKWebViewJavascriptBridge.ms
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    return NULL;
}

8.OC调用JS方法的本质

OC调用js的方法,都会把callback方法,方法名,参数打包到messageJSON中,然后调用下面这个终极方法。WebViewJavascriptBridge._handleMessageFromObjC(messageJSON);这个方法存在于WebViewJavascriptBridge_js.m文件中,是页面加载的时候注入的。下面部分就讲这个过程。

二、方法如何在JS中执行

1.加载页面的时候做了什么?

我们还是以官方的例子为例。加载一个本地的html页面ExampleApp.html。加载之后如下方法会被执行

- (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]) {
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

这个方法是加载网页第一个执行的方法,因为它要确定是否允许或者取消加载这个导航(就是是不是允许加载这个页面)。首次加载的时候url不是特殊的jsBridge的URL,直接允许加载这个页面。下面看看页面的源代码。

2.被加载的页面的内容(ExampleApp.html)

<!doctype html>
<html><head>
    <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <style type='text/css'>
        html { font-family:Helvetica; color:#222; }
        h1 { color:steelblue; font-size:24px; margin-top:24px; }
        button { margin:0 3px 10px; font-size:12px; }
        .logLine { border-bottom:1px solid #ccc; padding:4px 2px; font-family:courier; font-size:11px; }
    </style>
</head><body>
    <h1>WebViewJavascriptBridge Demo</h1>
    <script>
    window.onerror = function(err) {
        log('window.onerror: ' + err)
    }

    function setupWebViewJavascriptBridge(callback) {
        //第一次调用这个方法的时候,为false
        if (window.WebViewJavascriptBridge) {
            var result = callback(WebViewJavascriptBridge);
            return result;
        }
        //第一次调用的时候,也是false
        if (window.WVJBCallbacks) {
            var result = window.WVJBCallbacks.push(callback);
            return result;
        }
        //把callback对象赋值给对象。
        window.WVJBCallbacks = [callback];
        //这段代码的意思就是执行加载WebViewJavascriptBridge_JS.js中代码的作用
        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 callback(bridge) {
        var uniqueId = 1
        //把操作记录写入webview中
        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)
            }
        }
        //把WEB中要注册的方法注册到bridge里面
        bridge.registerHandler('JSMethod1', function(data, responseCallback) {
            log('OC调用JS方法成功', data)
            var responseData = { 'JS给OC调用的回调':'回调值!' }
            log('OC调用JS的返回值', responseData)
            responseCallback(responseData)
        })
        //获取web中的button,然后添加点击事件。
        document.body.appendChild(document.createElement('br'))
        document.getElementById('buttons').onclick = function(e) {
            e.preventDefault()
            var params =  {'JS调用OC参数': '参数值'};
            log('JS马上调用OC方法',params)
            bridge.callHandler('OC提供方法给JS调用',params, function(response) {
                log('JS调用OC的返回值', response)
            })
        }
    };
    //驱动所有hander的初始化
    setupWebViewJavascriptBridge(callback);
    </script>
    <input type='button' id='buttons' class='button' value='点击开始JS调用OC'></input>
    <div id='log'></div>
</body></html>

定义了2个方法,并且以第二个方法为参数,调用了第一个方法。方法一第一次调用的时候只是为window对象添加了一个数组WVJBCallbacks。并把第二个函数放进去。然后创建一个不可见的iframe元素,设置其url为一个特殊的url:https://__bridge_loaded__。这样也面又会发起一个请求,第一步的方法webView: decidePolicyForNavigationAction: decisionHandler再次被调用。这次会执行到这个分支[_base injectJavascriptFile]。

3.这一次要把WebViewJavascriptBridge_js里面的代码注入到js中

- (void)injectJavascriptFile {
    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];
        }
    }
}

WebViewJavascriptBridge_js只包含一个方法,生成一个字符。这个字符串就是要注入的js代码(也就是要执行的的代码)。

NSString * WebViewJavascriptBridge_js() {
    #define __wvjb_js_func__(x) #x
    
    // BEGIN preprocessorJSCode
    static NSString * preprocessorJSCode = @__wvjb_js_func__(
;(function() {
    if (window.WebViewJavascriptBridge) {
        return;
    }

    if (!window.onerror) {
        window.onerror = function(msg, url, line) {
            console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
        }
    }
    window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };

    var messagingIframe;
    var sendMessageQueue = [];
    var messageHandlers = {};
    
    var CUSTOM_PROTOCOL_SCHEME = 'https';
    var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
    
    var responseCallbacks = {};
    var uniqueId = 1;
    var dispatchMessagesWithTimeoutSafety = true;

    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    
    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;
    }
    
    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;
    }

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

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

    registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
    
    setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }
})();
    ); // END preprocessorJSCode

    #undef __wvjb_js_func__
    return preprocessorJSCode;
};

这个js代码的功能都是啥?创建了window.WebViewJavascriptBridge对象,这个是整个OC和原生交互的核心。这个对象里面包含方法_handleMessageFromObjC。就是我们第一部分8中调用的。这个方法会调用_doDispatchMessageFromObjC()方法。然后就是定义了各种对象,和函数。在最后我们看到一个通过setTimeout的函数调用。

    setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }

还记得window.WVJBCallbacks吗?他是在ExampleApp.html中定义的一个存储回调的数组。我们定义的callback就放在里面。调用这个数组里的所有回调函数并以WebViewJavascriptBridge对象作为参数。于是在ExampleApp.html中定义的第二个方法得到执行(这个方法里包含用户页面要执行的js代码,所以要放到页面里,不能放到框架jsbridge中)。
看看callback方法里的这段代码。

        //把WEB中要注册的方法注册到bridge里面
        bridge.registerHandler('JSMethod1', function(data, responseCallback) {
            log('OC调用JS方法成功', data)
            var responseData = { 'JS给OC调用的回调':'回调值!' }
            log('OC调用JS的返回值', responseData)
            responseCallback(responseData)
        })

调用了bridge对象的registerHandler注册了一个方法名和对应的函数,OC就是通过这个方法名JSMethod1来调用了JS的方法。我们回到WebViewJavascriptBridge_js中,看registerHandler它是如何实现的。

    function registerHandler(handlerName, handler) {
        alert(handlerName+'01');
        messageHandlers[handlerName] = handler;
    }

就是把函数存到了对象messageHandlers里。到此OC要调用JS方法已经放到字典里,等待被调用。

4.OC调用JS方法

第一部分第8步里说过,OC调用JS方法最后都会变成执行WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)这个在js代码中定义的方法。下面还是看源码吧

function _handleMessageFromObjC(messageJSON) {
    _dispatchMessageFromObjC(messageJSON);
}
//继续看_dispatchMessageFromObjC
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 {
                
                //alert(message.callbackId);        //objc_cb_1 objc_cb_2 objc_cb_3....
                
                //如果OC调用JS的时候设置了回调用的block,callbackId就不为空。这里生成了一个responseCallback函数。在后面调用JS方法的时候用。
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    
                    //这里定义的这个函数,被传递给OC将要调用的JS方法。在方法里会用要传给OC的数据responseData做参数调用。如果没有定义这个回调函数,OC调用JS方法也能成功,但是调用的时候传入的block不会被执行。
                    responseCallback = function(responseData) {
                        //这里只传递了一个参数。
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                        
                    };
                }
                
                //messageHandlers字典中存着我们要调用的JS的方法。
                var handler = messageHandlers[message.handlerName];
                alert(message.handlerName);
                
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    //调用JS的方法,至此OC最终调用了JS的方法。传入的responseCallback方法在JS中被调用,参数是要返回给OC的数据  yuxg
                    handler(message.data, responseCallback);
                }
            }
        }
    }

5.JS通过调用OC回调block,来给OC传回值。

这把message对象放到数组里,然后更改iframe的url,刷新页面。
function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
        responseCallbacks[callbackId] = responseCallback;
        message['callbackId'] = callbackId;
    }
    //message对象里包含,OC调用的JS的方法名,回调的Block的Id,和block的参数,也就是传回的数据。
    //把这个对象
    sendMessageQueue.push(message);
    //把iframe的地址修改为:https://__wvjb_queue_message__
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

JS给OC传递消息,都是通过修改iframed的src(也就是url)来实现的。这样WKWebViewJavascriptBridge里面webView:decidePolicyForNavigationAction: decisionHandler方法就能拦截这个消息。在里面会调用
[self WKFlushMessageQueue],我们继续看代码

//WKWebViewJavascriptBridge.m
- (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];
    }];
}
//WebViewJavascriptBridgeBase.m
 (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;
    }
    //JSON字符串反序列化为数组,数组里的元素是字典类型。
    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];
            //这里调用了OC调用JS方法是传入的block。
             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);
        }
    }
}

大鱼
4 声望0 粉丝