iOS WebViewJavascriptBridge源碼分析

前言

JSBridgeYes主要是為了替代WebViewJavascriptBridge弛车,同時(shí)也有兼容原來的方法齐媒,所以對WebViewJavascriptBridge的源碼以及原理做了比較深入的了解,以下主要是針對源碼的解讀

一帅韧、介紹

WebviewJavascriptBridge是一個(gè)第三方的支持webview和native進(jìn)行通信的庫里初,通過JSBridge,webview可以調(diào)用native的能力,native也可以webview上執(zhí)行一些邏輯忽舟。

二双妨、基本構(gòu)成

對于整個(gè)框架來說,一共有三部門組成


image.png
  • OC部分:包括oc處理暴露給js接口的類:WKWebViewJavascriptBridge(WebViewJavascriptBridge使用的是UIWebView叮阅,基本不用了)
  • js部分:包括js處理暴露給oc接口的文件: ExampleApp.html
  • bridge處理部分:這個(gè)部分對于oc和js都各有一個(gè)文件刁品,oc是類:WebViewJavascriptBridgeBase,js則是WebViewJavascriptBridge_JS
  • oc和js部分的作用就是聲明給對方調(diào)用的方法浩姥,以及提供供自身使用可以調(diào)用對方的一個(gè)接口挑随,但是具體如何調(diào)用的js、如果調(diào)用的oc或者說如何注冊給js勒叠、如何注冊給oc使用的方法兜挨,這些邏輯都放在兩端的bridge部分進(jìn)行處理。

三眯分、WVJB實(shí)現(xiàn)

image.png

1. 初始化

// iOS 初始化 
self.brige = [WebViewJavascriptBridge bridgeForWebView:_webView];
[self.brige registerHandler:@"imageSelectFunc" handler:^(id data, WVJBResponseCallback responseCallback) {
 
 }];

在native端和webview端注冊Bridge,本質(zhì)就是用一個(gè)對象把所有函數(shù)儲(chǔ)存起來

// js
function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}

// oc
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}

2.在webview里面注入初始化代碼


// Html
function setupWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        window.WVJBCallbacks = [callback]; // 創(chuàng)建一個(gè) WVJBCallbacks 全局屬性數(shù)組拌汇,并將 callback 插入到數(shù)組中。
        var WVJBIframe = document.createElement('iframe'); // 創(chuàng)建一個(gè) iframe 元素
        WVJBIframe.style.display = 'none'; // 不顯示
        WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'; // 設(shè)置 iframe 的 src 屬性
        document.documentElement.appendChild(WVJBIframe); // 把 iframe 添加到當(dāng)前文導(dǎo)航上弊决。
        setTimeout(function() {document.documentElement.removeChild(WVJBIframe) }, 0)
    }
    
 // 這里主要是注冊 OC 將要調(diào)用的 JS 方法
 setupWebViewJavascriptBridge(function(bridge){
       
 });

這段代碼主要做了以下幾件事:
(1)創(chuàng)建一個(gè)名為WVJBCallbacks的數(shù)組噪舀,將傳入的callback參數(shù)放到數(shù)組內(nèi)
(2)創(chuàng)建一個(gè)iframe魁淳,設(shè)置不可見,設(shè)置src為 https://bridge_loaded
(3)設(shè)置定時(shí)器移除這個(gè)iframe

3.在客戶端監(jiān)聽URL請求

// WKWebView為例
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    if (webView != _webView) { return; }

    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationResponse:decisionHandler:)]) {
        [strongDelegate webView:webView decidePolicyForNavigationResponse:navigationResponse decisionHandler:decisionHandler];
    }
    else {
        decisionHandler(WKNavigationResponsePolicyAllow);
    }
}

這段代碼主要做了以下幾件事:
(1)攔截了所有的URL請求并拿到url
(2)首先判斷isWebViewJavascriptBridgeURL与倡,判斷這個(gè)url是不是webview的iframe觸發(fā)的界逛,具體可以通過host去判斷。
(3)繼續(xù)判斷纺座,如果是isBridgeLoadedURL息拜,那么會(huì)執(zhí)行injectJavascriptFile方法,會(huì)向webview中再次注入一些邏輯比驻,其中最重要的邏輯就是该溯,在window對象上掛載一些全局變量和WebViewJavascriptBridge屬性
(4)繼續(xù)判斷,如果是isQueueMessageURL别惦,那么這就是個(gè)處理消息的回調(diào),需要執(zhí)行一些消息處理的方法

4. webview調(diào)用native

當(dāng)webview調(diào)用native時(shí)夫椭,會(huì)調(diào)用callHandler方法

// h5代碼有封裝 截取一部分 
function invoke (fun, params, cb) {
   setupWebViewJavascriptBridge(function (bridge) {
     bridge.callHandler(fun, params, function (res) {
       cb && cb(res)
       console.log('invoke', res)
       let logData = {
         JsBridge: 'invoke',
         BridgeName: fun,
         BridgeParam: params,
         Response: res
       }
       logger.info(logData)
     })
   })
 }

實(shí)際上就是生成一個(gè)message掸掸,然后push到sendMessageQueue里,然后更改iframe的src蹭秋。

5. native側(cè)接受消息 flushMessageQueue

當(dāng)native端檢測到iframe src的變化時(shí)扰付,會(huì)走到isQueueMessageURL的判斷邏輯,然后執(zhí)行WKFlushMessageQueue函數(shù)仁讨,獲取到JS側(cè)的sendMessageQueue中的所有message

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

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

當(dāng)一個(gè)message結(jié)構(gòu)存在responseId的時(shí)候說明這個(gè)message是執(zhí)行bridge后傳回的羽莺。
取不到responseId說明是第一次調(diào)用bridge傳過來的,這個(gè)時(shí)候會(huì)生成一個(gè)返回給調(diào)用方的message洞豁,其reponseId是傳過來的message的callbackId盐固,當(dāng)native執(zhí)行responseCallback時(shí),會(huì)觸發(fā)_dispatchMessage方法執(zhí)行webview環(huán)境的的js邏輯丈挟,將生成的包含responseId的message返回給webview刁卜。

image.png

6. native側(cè)發(fā)送消息 sendData

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

OC要調(diào)用javascript環(huán)境的方法,其實(shí)就是調(diào)用ExampleApp.html中的bridge.registerHandler注冊的方法曙咽。把所有信息存入一個(gè)名字為message的字典中蛔趴。里面拼裝好參數(shù)data、回調(diào)IDcallbackId例朱、消息名字handlerName,把OC消息序列化孝情、并且轉(zhuǎn)化為javascript環(huán)境的格式。然后在主線程中調(diào)用_evaluateJavascript洒嗤。

WebViewJavascriptBridge._handleMessageFromObjC('{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC調(diào)用JS方法\":\"OC調(diào)用JS方法的參數(shù)\"},\"handlerName\":\"OC調(diào)用JS提供的方法\"}');

image.png

7.總結(jié)

結(jié)合上面的邏輯圖箫荡,原理其實(shí)很簡單

  • 分別在OC環(huán)境和javascript環(huán)境都保存一個(gè)bridge對象,里面維持著requestId,callbackId,以及每個(gè)id對應(yīng)的具體實(shí)現(xiàn)烁竭。

  • OC通過javascript環(huán)境的window.WebViewJavascriptBridge對象來找到具體的方法菲茬,然后執(zhí)行。

  • javascript通過改變iframe的src來觸發(fā)webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler從而實(shí)現(xiàn)把javascript消息發(fā)送給OC這個(gè)功能。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末婉弹,一起剝皮案震驚了整個(gè)濱河市睬魂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镀赌,老刑警劉巖氯哮,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異商佛,居然都是意外死亡喉钢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門良姆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肠虽,“玉大人,你說我怎么就攤上這事玛追∷翱危” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵痊剖,是天一觀的道長韩玩。 經(jīng)常有香客問我,道長陆馁,這世上最難降的妖魔是什么找颓? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮叮贩,結(jié)果婚禮上击狮,老公的妹妹穿的比我還像新娘。我一直安慰自己妇汗,他們只是感情好帘不,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杨箭,像睡著了一般寞焙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上互婿,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天捣郊,我揣著相機(jī)與錄音,去河邊找鬼慈参。 笑死呛牲,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的驮配。 我是一名探鬼主播娘扩,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼着茸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了琐旁?” 一聲冷哼從身側(cè)響起涮阔,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎灰殴,沒想到半個(gè)月后敬特,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牺陶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年伟阔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掰伸。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡皱炉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出碱工,到底是詐尸還是另有隱情娃承,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布怕篷,位于F島的核電站,受9級(jí)特大地震影響酗昼,放射性物質(zhì)發(fā)生泄漏廊谓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一麻削、第九天 我趴在偏房一處隱蔽的房頂上張望蒸痹。 院中可真熱鬧,春花似錦呛哟、人聲如沸叠荠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽榛鼎。三九已至,卻和暖如春鳖孤,著一層夾襖步出監(jiān)牢的瞬間者娱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工苏揣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留黄鳍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓平匈,卻偏偏與公主長得像框沟,于是被迫代替她去往敵國和親藏古。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容