WebViewJavascriptBridge源码探秘上

45次阅读

共计 15754 个字符,预计需要花费 40 分钟才能阅读完成。

一、我们先看看如何使用 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);
        }
    }
}

正文完
 0