WVJB原理解析(JS調(diào)用OC)

之前有中間件項目用到了WebViewJavascriptBridge這個庫转质,當(dāng)時捉摸了一下原理串稀,現(xiàn)在拿出來給大家分享一下硅瞧,還是以官方給的demo為例,把復(fù)雜的代碼精簡到早簡單的一條調(diào)用線直觀的給大家展示調(diào)用過程
在交互過程中腿堤,外部只需要調(diào)用兩個方法就能完成這個交互的過程
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中的接收方法

    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];

就完成了一次調(diào)用過程

基本原理解析

大家都知道的通常的manual調(diào)用OC方法的方案是通過自定義協(xié)議和url,再通過webview的delegate截獲webview的url跳轉(zhuǎn)事件來實現(xiàn)JS調(diào)用OC方法的如暖,WVJB也很相似笆檀,但是多了幾步,了解了這個基本原理就來看看WVJB在完成交互調(diào)用前做了什么工作

WVJB前置工作

1.OC端

    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
    [_bridge setWebViewDelegate:self];

在OC端做的前置工作比較少但是后續(xù)的還是比較多的盒至,和交互有關(guān)的其實就這么兩句話酗洒,第一句是創(chuàng)建一個橋接初始化,因為要兼容WKWebview和UIWebview所以在這個方法中做了判斷枷遂,第二步是把要進(jìn)行交互的webview的代理掛載到這個橋上樱衷,如剛才講的基本原理一樣,因為它需要代替我們截獲url跳轉(zhuǎn)事件所以要拿到這個代理酒唉,另一個前置工作是在代理事件里面在這個Webview的請求開始前注入了一段JS代碼

2.JS端

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

setupWebViewJavascriptBridge(function(bridge) {...}

WVJB在調(diào)用前需要在網(wǎng)頁中寫如上的JS代碼矩桂,作用是創(chuàng)建了一個不可見的iframe進(jìn)行發(fā)出url跳轉(zhuǎn)事件用于讓OC截獲,之后把你想進(jìn)行交互的代碼放在一個匿名函數(shù)中當(dāng)參數(shù)傳給setupWebViewJavascriptBridge這個方法就完成了
這樣一來基本原理中所需要的兩個基本點就都具備了,一個發(fā)url跳轉(zhuǎn)的東西和一個截獲url跳轉(zhuǎn)的東西痪伦,接下來開始詳細(xì)解析一個JS代碼調(diào)用OC的流程

具體調(diào)用流程

首先在OC中注冊了這個方法用于接收J(rèn)S發(fā)的消息

    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];

registerHandler方法只做了一件事侄榴,把第一個參數(shù)當(dāng)做key,后面的回調(diào)函數(shù)當(dāng)做value存放到一個叫messageHandlers的字典中流妻,OC端就完成了牲蜀。

在JS中寫的

        var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
        callbackButton.innerHTML = 'Fire testObjcCallback'
        callbackButton.onclick = function(e) {
            e.preventDefault()
            log('JS calling handler "testObjcCallback"')
            bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
                log('JS got response', response)
            })
        }

實際上是調(diào)用了在webview載入前注入網(wǎng)頁中的一串實現(xiàn)準(zhǔn)備好的JS代碼中方法,這串JS詳見WebViewJavascriptBridge_JS.m文件,
bridge.callHandler這個方法需要三個參數(shù),第一個參數(shù)為調(diào)用方法名绅这,第二個參數(shù)為方法要傳的參數(shù)涣达,但三個參數(shù)為方法回調(diào)
這個方法是這樣的

    function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
    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;
    }

callHandler這個方法把方法名和要傳的參數(shù)合并成一個數(shù)組轉(zhuǎn)發(fā)給doSend方法,doSend方法做了以下幾件事:
1.把回調(diào)進(jìn)行格式化并存放到回調(diào)方法數(shù)組中
2.給傳進(jìn)來的回調(diào)方法添加一個獨一無二的ID并且根據(jù)ID存進(jìn)回調(diào)方法數(shù)組,然后把這個ID也合并到messsage里
3.把傳進(jìn)的方法名和參數(shù)合并數(shù)據(jù)添加到信息數(shù)組
4.修改iframe的url發(fā)送跳轉(zhuǎn)信息讓OC截獲证薇,而這個跳轉(zhuǎn)信息是定死的https://wvjb_queue_message度苔,就只用來通知OC有消息可以接收了,不傳送任何具體數(shù)據(jù)

之后OC的WVJB管理的webview delegate截獲到這條消息就判斷一下是不是這個wvjb_queue_message消息浑度,是的話就開始進(jìn)行方法調(diào)用寇窑,不是就讓它正常跳轉(zhuǎn),當(dāng)接收到這個wvjb_queue_message消息的時候OC會調(diào)用一個名為_fetchQueue的JS方法箩张,這個方法也是預(yù)先注入到網(wǎng)頁中的

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

可以看到這個方法把JS管理的消息數(shù)組中的消息取出來格式化成字符串返回給OC并把消息數(shù)組清空甩骏,之后OC把這個字符串發(fā)給flushMessageQueue方法進(jìn)行處理,

- (void)flushMessageQueue:(NSString *)messageQueueString{
id messages = [self _deserializeMessageJSON:messageQueueString];
...
WVJBResponseCallback responseCallback = NULL;
responseCallback = ^(id ignoreResponseData) {
                // Do nothing
            };
        
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
        
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
    };
}
handler(message[@"data"], responseCallback);

上面是精簡過flushMessageQueue方法我先慷,本來的這個方法還判斷了傳進(jìn)來的參數(shù)什么的饮笛,并且還有OC調(diào)用JS的邏輯,這里我全部精簡掉了只留下了JS調(diào)用OC所需的幾句代碼论熙,然后可以清晰的看出他做的幾件事
1.把這個字符串反解成Json
2.分別取出來參數(shù)和方法名
3.從之前OC注冊時添加到的self.messageHandlers字典中通過key取出對應(yīng)的匿名函數(shù)然后
4.調(diào)用這個匿名函數(shù)
5.查看是消息里面是否包含callbackId福青,如果有就通過_queueMessage處理一下
就這樣一次調(diào)用就完成了

附加

說完了一次完整的調(diào)用過程,?然后來講講JS的回調(diào)是怎么完成的,其實也和之前的流程差不多无午,?callbackId在flushMessageQueue中發(fā)現(xiàn)后發(fā)給_queueMessage以后_queueMessage又調(diào)用了_dispatchMessage方法媒役,這個方法就會處理處理這條消息之后返回給JS

- (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    [self _log:@"SEND" json:messageJSON];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
...
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

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

這個方法做的幾件事:
1.將message序列化
2.將序列化后的消息格式化轉(zhuǎn)義,斜杠啊什么的
3.?由于JS是單線程的所以要拿到一條線程JS同步執(zhí)行
4.調(diào)用JS的_handleMessageFromObjC方法

_handleMessageFromObjC這個方法也沒什么好說的了和之前很類似,當(dāng)這個方法拿到callbackID后會去回調(diào)數(shù)組通過這個key找到對應(yīng)的回調(diào)方法然后調(diào)用宪迟,就完成了JS的完成回調(diào)

總結(jié)

總的來說就是OC添加了一個以方法名為key,匿名函數(shù)為value的值到消息接收數(shù)組中酣衷,之后JS調(diào)用方法時把方法名和傳入?yún)?shù)合并作為一個數(shù)組放到消息數(shù)組中通知OC有消息可以接收,OC端截獲有消息的通知調(diào)用JS的fetchqueue方法獲取到消息數(shù)組中的內(nèi)容然后反解出來方法名和參數(shù)再從OC注冊的方法數(shù)組中通過方法名取出對應(yīng)的方法調(diào)用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末踩验,一起剝皮案震驚了整個濱河市鸥诽,隨后出現(xiàn)的幾起案子商玫,更是在濱河造成了極大的恐慌箕憾,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拳昌,死亡現(xiàn)場離奇詭異袭异,居然都是意外死亡,警方通過查閱死者的電腦和手機炬藤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門御铃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人沈矿,你說我怎么就攤上這事上真。” “怎么了羹膳?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵睡互,是天一觀的道長。 經(jīng)常有香客問我陵像,道長就珠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任醒颖,我火速辦了婚禮妻怎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘泞歉。我一直安慰自己逼侦,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布腰耙。 她就那樣靜靜地躺著榛丢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沟优。 梳的紋絲不亂的頭發(fā)上涕滋,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天,我揣著相機與錄音挠阁,去河邊找鬼宾肺。 笑死溯饵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的锨用。 我是一名探鬼主播丰刊,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼增拥!你這毒婦竟也來了啄巧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤掌栅,失蹤者是張志新(化名)和其女友劉穎秩仆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猾封,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡澄耍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了晌缘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片齐莲。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖磷箕,靈堂內(nèi)的尸體忽然破棺而出选酗,到底是詐尸還是另有隱情,我是刑警寧澤岳枷,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布芒填,位于F島的核電站,受9級特大地震影響嫩舟,放射性物質(zhì)發(fā)生泄漏氢烘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一家厌、第九天 我趴在偏房一處隱蔽的房頂上張望播玖。 院中可真熱鬧,春花似錦饭于、人聲如沸蜀踏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽果覆。三九已至,卻和暖如春殖熟,著一層夾襖步出監(jiān)牢的瞬間局待,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钳榨,地道東北人舰罚。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像薛耻,于是被迫代替她去往敵國和親营罢。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,455評論 2 359

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