ios 基础 面试

WebViewJavascriptBridge 原理解析

2017-12-21  本文已影响190人  廖丹_18be

WebViewJavascriptBridge应该是当前最流行最成功的OC与Web交互实现了。最近看了一下他的实现原理

我们可以在OC中调用javascript方法,但是反过来不能在javascript中调用OC方法。所以WebViewJavascriptBridge的实现过程就是在OC环境和javascript环境各自保存一个相互调用的信息。每一个调用之间都有id和callbackid来找到两个环境对应的处理。


image.png

WebViewJavascriptBridge_JS.m文件中是javascript环境的bridge初始化和处理,里面负责接收oc发给javascript的消息,并且把javascript环境的消息发送给oc。
WebViewJavascriptBridge.m主要负责OC环境的消息处理,并且把OC环境的消息发送给javascript环境。
WebViewJavascriptBridgeBase.m主要实现了OC环境的bridge初始化和处理。
ExampleApp.html主要用于模拟生产环境下的web端。

初始化过程

1、OC环境初始化

我们从OC环境的初始化开始。

//初始化一个OC环境的桥WKWebViewJavascriptBridge并且初始化。

//messageHandlers用于保存OC环境注册的方法,key是方法名,value是这个方法对应的回调block
//startupMessageQueue用于保存是实话过程中需要发送给javascirpt环境的消息。
//responseCallbacks用于保存OC于javascript环境相互调用的回调模块。通过_uniqueId加上时间戳来确定每个调用的回调。

2、OC环境注册方法

注册一个OC方法OC提供方法给JS调用给javascript调用,并且把他的回调实现保存在messageHandlers中。

[_bridge registerHandler:@"OC提供方法给JS调用" handler:^(id data, WVJBResponseCallback responseCallback) {
//NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"OC发给JS的返回值");
}];

加载Web环境的html,这里就是ExampleAPP.html文件,我删除了非关键部分。

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
//把WEB中要注册的方法注册到bridge里面
bridge.registerHandler('OC调用JS提供的方法', function(data, responseCallback) {
log('OC调用JS方法成功', data)
var responseData = { 'JS给OC调用的回调':'回调值!' }
log('OC调用JS的返回值', responseData)
responseCallback(responseData)
})
};
//驱动所有hander的初始化
setupWebViewJavascriptBridge(callback);
我们调用setupWebViewJavascriptBridge函数,并且这个函数传入的callback也是一个函数。callback函数中有我们在javascript环境中注册的OC调用JS提供的方法方法。setupWebViewJavascriptBridge的实现过程中我们可以发现,如果不是第一次初始化,会通过 window.WebViewJavascriptBridge或者window.WVJBCallbacks两个判断返回。

iframe可以理解为webview中的窗口,当我们改变iframe的src属性的时候,相当于我们浏览器实现了链接的跳转。比如从www.baidu.com跳转到www.google.com。下面这段代码的目的就是实现一个到https://bridge_loaded的跳转。从而达到初始化javascript环境的bridge的作用。

//这段代码的意思就是执行加载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);
我们知道只要webview有跳转,就会调用webview的代理方法。我们重点看下面这个代理方法。

define kOldProtocolScheme @"wvjbscheme"

define kNewProtocolScheme @"https"

define kQueueHasMessage @"wvjb_queue_message"

define kBridgeLoaded @"bridge_loaded"

//是否是WebViewJavascriptBridge框架相关的链接

接下来调用[_base injectJavascriptFile]方法,这个方法的作用就是把WebViewJavascriptBridge_JS.js中的方法注入到webview中并且执行,从而达到初始化javascript环境的brige的作用。

//初始化的是否注入WebViewJavascriptBridge_JS.js

上面我们讲到了注入javascript方法到webview中。具体的代码就是WebViewJavascriptBridge_JS.js这个文件中的方法。我们通过分析这个文件的代码可以知道javascript环境的bridge是如何初始化的。

;(function() {
//如果已经初始化了,则返回。
if (window.WebViewJavascriptBridge) {
return;
}
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
}
}
//初始化一些属性。
var messagingIframe;
//用于存储消息列表
var sendMessageQueue = [];
//用于存储消息
var messageHandlers = {};
//通过下面两个协议组合来确定是否是特定的消息,然后拦击。
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = 'wvjb_queue_message';
//oc调用js的回调
var responseCallbacks = {};
//消息对应的id
var uniqueId = 1;
//是否设置消息超时
var dispatchMessagesWithTimeoutSafety = true;
//web端注册一个消息方法
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
//web端调用一个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;
}
//把消息转换成JSON字符串返回
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
//OC调用JS的入口方法
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}

//初始化桥接对象,OC可以通过WebViewJavascriptBridge来调用JS里面的各种方法。
window.WebViewJavascriptBridge = {
    registerHandler: registerHandler,
    callHandler: callHandler,
    disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
    _fetchQueue: _fetchQueue,
    _handleMessageFromObjC: _handleMessageFromObjC
};


//处理从OC返回的消息。
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 });
                };
            }
            //获取JS注册的函数
            var handler = messageHandlers[message.handlerName];
            if (!handler) {
                console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
            } else {
                //调用JS中的对应函数处理
                handler(message.data, responseCallback);
            }
        }
    }
}
//把消息从JS发送到OC,执行具体的发送操作。
function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        //存储消息的回调ID
        responseCallbacks[callbackId] = responseCallback;
        //把消息对应的回调ID和消息一起发送,以供消息返回以后使用。
        message['callbackId'] = callbackId;
    }
    //把消息放入消息列表
    sendMessageQueue.push(message);
    //下面这句话会出发JS对OC的调用
    //让webview执行跳转操作,从而可以在
    //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中拦截到JS发给OC的消息
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}


messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
//messagingIframe.body.style.backgroundColor="#0000ff";
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);


//注册_disableJavascriptAlertBoxSafetyTimeout方法,让OC可以关闭回调超时,默认是开启的。
registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
//执行_callWVJBCallbacks方法
setTimeout(_callWVJBCallbacks, 0);

//初始化WEB中注册的方法。这个方法会把WEB中的hander注册到bridge中。
//下面的代码其实就是执行WEB中的callback函数。
function _callWVJBCallbacks() {
    var callbacks = window.WVJBCallbacks;
    delete window.WVJBCallbacks;
    for (var i = 0; i < callbacks.length; i++) {
        callbacks[i](WebViewJavascriptBridge);
    }
}

})();
其实我们发现整个js文件就是一个立即执行的javascript方法。

首先我们发现会初始化一个WebViewJavascriptBridge对象。并且这个对象是赋值给window对象,这里window对象可以理解为webview。所以说我们后面在OC环境中如果要调用js方法,就可以通过window.WebViewJavascriptBridge在加上具体方法来调用。
WebViewJavascriptBridge对象中有javascript环境注入的提供给OC调用的方法registerHandler,javascript调用OC环境方法的callHandler。
_fetchQueue这个方法的作用就是把javascript环境的方法序列化成JSON字符串,然后传入OC环境再转换。
_handleMessageFromObjC就是处理OC发给javascript环境的方法。
在这个文件中也初始化了一个iframe实现webview的url跳转功能,从而激发webview代理方法的调用。

messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
//messagingIframe.body.style.backgroundColor="#0000ff";
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);

上面的src就是https://wvjb_queue_message/。这个是javascript发送的OC的第一条消息,目的和上面OC环境的startupMessageQueue一样,就是在javascript环境初始化完成以后,把javascript要发送给OC的消息立即发送出去。

然后我们看文件的最后面有如下代码。这段代码的作用就是立即执行ExampleApp.html中的callback方法。callback中传入的bridge参数就是我们这里初始化的window.WebViewJavascriptBridge对象。

//执行_callWVJBCallbacks方法
setTimeout(_callWVJBCallbacks, 0);

//初始化WEB中注册的方法。这个方法会把WEB中的hander注册到bridge中。
//下面的代码其实就是执行WEB中的callback函数。
function _callWVJBCallbacks() {
    var callbacks = window.WVJBCallbacks;
    delete window.WVJBCallbacks;
    for (var i = 0; i < callbacks.length; i++) {
        callbacks[i](WebViewJavascriptBridge);
    }
}

直到这里,OC环境和javascript环境的bridege都建立完毕。OC和javascript环境都有一个bridge对象,这个对象都保存着注册的每个方法和回调,并且维护着各自的消息队列、回调id、requestId等一系列信息。

OC发消息给WEB

OC要调用javascript环境的方法,其实就是调用ExampleApp.html中的bridge.registerHandler注册的方法。

//点击按钮开始一个OC消息.ExampleWKWebViewController.m中一个方法开始。

//把消息发送给WEB环境

WebViewJavascriptBridge._handleMessageFromObjC('{"callbackId":"objc_cb_1","data":{"OC调用JS方法":"OC调用JS方法的参数"},"handlerName":"OC调用JS提供的方法"}');
其实就是通过javascript环境中的Bridge对象的_handleMessageFromObjC方法。下面我们去WebViewJavascriptBridege_JS.js中看_handleMessageFromObjC的处理过程。

//处理从OC返回的消息。
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 });
            };
        }
        //获取JS注册的函数
        var handler = messageHandlers[message.handlerName];
        if (!handler) {
            console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
        } else {
            //调用JS中的对应函数处理
            handler(message.data, responseCallback);
        }
    }
}

}

上面这段代码很容易理解,其实就是如果消息中有callbackId则表示是一个回调。直接调用_doSend方法把信息返回OC。否则就是Web环境主动调用OC的情况。此时把callbackID、handlerName、responseCallback封装进一个message对象中保存起来(其实你会发现和OC环境的bridge处理一样)。然后通过_doSend发消息发送到OC环境。下面我们看看_doSend的具体实现:

//把消息从JS发送到OC,执行具体的发送操作。
function doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb
' + (uniqueId++) + '_' + new Date().getTime();
//存储消息的回调ID
responseCallbacks[callbackId] = responseCallback;
//把消息对应的回调ID和消息一起发送,以供消息返回以后使用。
message['callbackId'] = callbackId;
}
//把消息放入消息列表
sendMessageQueue.push(message);
//下面这句话会出发JS对OC的调用
//让webview执行跳转操作,从而可以在
//webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中拦截到JS发给OC的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

其中最重要还是最后面的通过改变iframe的messagingIframe.src。从而触发webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler从而在OC中处理javascript环境触发过来的回调。具体如下:

if ([_base isWebViewJavascriptBridgeURL:url]) {
//第一次注入JS代码
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
//处理WEB发过来的消息
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
}
这里会走[self WKFlushMessageQueue];方法。然后通过调用WebViewJavascriptBridge._fetchQueue()来获取javascript给OC的回调信息。

//获取WEB消息的JSON字符串

获取到javascript给OC的回调消息以后,然后把javascript的bridge返回的信息加工处理成OC环境的bridge能识别的信息。从而找到具体的实现执行。

//把从WEB发送的消息返回。然后在这里处理

WEB发消息给OC

首先通过ExampleAPP.html中的bridge.callHandler方法,这里的bridge就是window.WebViewJavascriptBridge对象:

bridge.callHandler('OC提供方法给JS调用',params, function(response) {
log('JS调用OC的返回值', response)
})
接下来调用window.WebViewJavascriptBridge中的callHander方法

//web端调用一个OC注册的消息
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName: handlerName, data: data }, responseCallback);
}
然后调用WebViewJavascriptBridge_JS.js中的方法执行具体的操作。具体就和OC调用javascript过程一样了,就不解释了。

//把消息从JS发送到OC,执行具体的发送操作。
function doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb
' + (uniqueId++) + '_' + new Date().getTime();
//存储消息的回调ID
responseCallbacks[callbackId] = responseCallback;
//把消息对应的回调ID和消息一起发送,以供消息返回以后使用。
message['callbackId'] = callbackId;
}
//把消息放入消息列表
sendMessageQueue.push(message);
//下面这句话会出发JS对OC的调用
//让webview执行跳转操作,从而可以在
//webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中拦截到JS发给OC的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
总结

其实现在想想,原理很简单。

分别在OC环境和javascript环境都保存一个bridge对象,里面维持着requestId,callbackId,以及每个id对应的具体实现。
OC通过javascript环境的window.WebViewJavascriptBridge对象来找到具体的方法,然后执行。
javascript通过改变iframe的src来出发webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler从而实现把javascript消息发送给OC这个功能。
其实这里只是解析了webview与OC交互的桥接问题,其他比如webview中的请求拦截、添加进度条、运营商劫持、如何组织交互规则等问题这里还没有涉及。这些在我们项目中运用,具体就不抽出来了。

上一篇 下一篇

猜你喜欢

热点阅读