OC和JS交互(UIWebView)中級篇3

上一篇博文重點講了下我們項目中最常用的JS調(diào)用OC, 花開兩朵各表一枝, 本文將重點講下OC調(diào)用JS.

OC調(diào)用JS的入口在VC, 下面是代碼

[self.bridge callHandler:@"getUserInfo" data:@{@"userId":@"DX001"} responseCallback:^(id responseData) {
        NSString *userInfo = [NSString stringWithFormat:@"%@,姓名:%@,年齡:%@", responseData[@"userID"], responseData[@"userName"], responseData[@"age"]];
        UIAlertController *vc = [UIAlertController alertControllerWithTitle:@"從網(wǎng)頁端獲取的用戶信息" message:userInfo preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
        UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleDefault handler:nil];
        [vc addAction:cancelAction];
        [vc addAction:okAction];
        [self presentViewController:vc animated:YES completion:nil];
    }];
WebViewJavascriptBridge.m
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
WebViewJavascriptBridgeBase.m
- (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];
}

最終來到了WebViewJavascriptBridgeBasesendData方法里面, 這里創(chuàng)建一個NSMutableDictionary對象message, 并把VC傳遞進(jìn)來的參數(shù)data = @{@"userId":@"DX001"}, handlerName = @"getUserInfo"還有responseCallback保存起來, 和之前JS保存responseCallback方法相似, 這里也是生成一個callbackId, 并把responseCallback保存在以callbackId為key的字典self.responseCallbacks中, 最后執(zhí)行[self _queueMessage:message];

WebViewJavascriptBridgeBase.h
@interface WebViewJavascriptBridgeBase : NSObject

@property (strong, nonatomic) NSMutableArray* startupMessageQueue;

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

我們來回憶下上文是怎么使用_dispatchMessage

responseCallback = ^(id responseData) {
  if (responseData == nil) {
      responseData = [NSNull null];
  }
                    
  WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
  [self _queueMessage:msg];
};

這里是在OC的block中執(zhí)行了_queueMessage, 實際也是OC調(diào)用JS. 只是在上文中, OC調(diào)用JS不是重點. 好了這里也順便分析下我們之前遺留下來的問題:startupMessageQueue是干什么的?

WebViewJavascriptBridgeBase.m
-(id)init {
    self = [super init];
    self.messageHandlers = [NSMutableDictionary dictionary];
    self.startupMessageQueue = [NSMutableArray array];
    self.responseCallbacks = [NSMutableDictionary dictionary];
    _uniqueId = 0;
    return(self);
}

- (void)dealloc {
    self.startupMessageQueue = nil;
    self.responseCallbacks = nil;
    self.messageHandlers = nil;
}

- (void)reset {
    self.startupMessageQueue = [NSMutableArray array];
    self.responseCallbacks = [NSMutableDictionary dictionary];
    _uniqueId = 0;
}

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

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

startupMessageQueueWebViewJavascriptBridgeBase中的一個數(shù)組, 這個數(shù)組在WebViewJavascriptBridgeBase初始化的時候被創(chuàng)建, 但是只是一個空的數(shù)組, 并且在初始化注入的時候就被取出來并置空了, 所以后面正常情況是不存在_queueMessage走進(jìn)if分支的, 只有一種情況, 就是在injectJavascriptFile還沒執(zhí)行的時候, 先進(jìn)行了OC對JS的調(diào)用, 這種情況在我們把

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 = 'wvjbscheme://__BRIDGE_LOADED__';
            document.documentElement.appendChild(WVJBIframe);
            setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
        }

這段代碼寫進(jìn)html的時候應(yīng)該是不存在的, 因為在網(wǎng)頁加載的時候OC就完成了注入, 但是如果上面的這段代碼如果不在html中, 那還是有可能的, 而且在實際開發(fā)中, 難道我們還要要求前端的同事每個網(wǎng)頁都加上面的一段代碼, 也是不現(xiàn)實的. 所以作者應(yīng)該是通過綜合的考慮才加入了startupMessageQueue的, 好了, 在本例中, startupMessageQueue還是沒有實際作用, 代碼最終回到上文的后半部分, OC回調(diào)JS, 這里要注意一下的問題就是, 運行JS腳本可能會存在線程安全的問題, 所以, 一定要在主線程執(zhí)行JS

if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

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

經(jīng)過一些列調(diào)用_queueMessage->_dispatchMessage->_evaluateJavascript->_handleMessageFromObjC->_dispatchMessageFromObjC->_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);
        }
    }
}

我們在Safari中下斷點, 發(fā)現(xiàn)


image.png

, 這里有個小的tips, 因為這段腳本不在html的頁面里面不能直接打斷點, 要先在

bridge.registerHandler('getUserInfo', function(data, responseCallback) {
    console.log("OC中傳遞過來的參數(shù):", data);
    // 把處理好的結(jié)果返回給OC
    responseCallback({"userID":"DX001", "userName":"旋之華", "age":"18", "otherName":"旋之華"})
});

responseCallback這里打斷點, 然后調(diào)用JS接口, Safari左側(cè)會出現(xiàn)調(diào)用堆棧, 里面有我們注入的代碼, 這時候就可以在_dispatchMessageFromObjC里面打斷點了, 好了, 繼續(xù)分析_dispatchMessageFromObjC里面的代碼.
由于調(diào)用的時候并沒有給responseId賦值, 所以, 代碼走到

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

這里是和之前JS調(diào)用OC時候, OC回調(diào)JS不同的, 這里message.responseId是沒有值的, message.callbackId是有值的, 所以會在這里創(chuàng)建一個JS的responseCallback, 后面取出handler并調(diào)用handler(message.data, responseCallback);, 下面來看下messageHandlers吧, 實際和OC注冊很相似

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

還沒完, 這里OC調(diào)用JS的時候也傳遞了一個block, 這個block最終傳遞到了JS

bridge.registerHandler('getUserInfo', function(data, responseCallback) {
    console.log("OC中傳遞過來的參數(shù):", data);
    // 把處理好的結(jié)果返回給OC
    responseCallback({"userID":"DX001", "userName":"旋之華", "age":"18", "otherName":"旋之華"})
});       

把函數(shù)名作為key, 回調(diào)方法作為value, 建立messageHandlers字典, 所以最終執(zhí)行handler(message.data, responseCallback);實際是調(diào)用了

bridge.registerHandler('getUserInfo', function(data, responseCallback) {
    console.log("OC中傳遞過來的參數(shù):", data);
    // 把處理好的結(jié)果返回給OC
    responseCallback({"userID":"DX001", "userName":"旋之華", "age":"18", "otherName":"旋之華"})
});      

中函數(shù)體里的代碼, 如果沒有OC傳遞的block, OC調(diào)用JS就到此結(jié)束了, 輸出console.log("OC中傳遞過來的參數(shù):", data);完成調(diào)用, 但是OC傳遞了block, 所以還要繼續(xù)分析, responseCallback是在剛才通過JS代碼創(chuàng)建的回調(diào), 只有OC傳遞了block才會創(chuàng)建.

responseCallback = function(responseData) {
    _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};

在JS中調(diào)用responseCallback({"userID":"DX001", "userName":"旋之華", "age":"18", "otherName":"旋之華"})實際會來到_doSend, responseData正是JS中傳遞來的參數(shù)

image.png
, 而handlerNameresponseId都是OC調(diào)用的時候傳遞的參數(shù).

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

進(jìn)入_doSend, 由于沒傳遞responseCallback, 所以if走不到, 這里還是把OC傳遞過來的message保存在sendMessageQueue中, 然后改變src觸發(fā)OC執(zhí)行, 來到OC的

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

由于responseId有值, 而從_responseCallbacks取出來的responseCallback正是OC之前傳入的block, 所以下面的代碼

WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];

執(zhí)行responseCallback, 回調(diào)OC之前傳遞的block, 至此, OC調(diào)用JS, 并把JS端數(shù)據(jù)取回完美結(jié)束.

總結(jié)

在實際中, 我們應(yīng)該很少會用到JS提供接口給OC調(diào)用, 通常是OC提供穩(wěn)定通用接口給JS調(diào)用, 所以本文不是我們實踐的重點, 但是作為講解框架的完整性, 我們應(yīng)該把OC調(diào)用JS和JS調(diào)用OC都進(jìn)行詳細(xì)的分析, 這樣能更好的理解作者設(shè)計的意圖和架構(gòu)的巧妙之處.

遺留問題

1 難道每個Web頁面都要加入setupWebViewJavascriptBridge這段代碼, 這應(yīng)該是所有開發(fā)者都不能接受的.
2 對于WKWebView怎么處理? 也能攔截嗎?
3 如何設(shè)計一個通用的WebView或者WebViewController?

下面是本文用到的代碼的github, 不是小編原著.
WebViewJSBridgeDemo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末影所,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子尾膊,更是在濱河造成了極大的恐慌厢塘,老刑警劉巖丐一,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阿弃,死亡現(xiàn)場離奇詭異支救,居然都是意外死亡循诉,警方通過查閱死者的電腦和手機臭增,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門懂酱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人速址,你說我怎么就攤上這事玩焰。” “怎么了芍锚?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵昔园,是天一觀的道長蔓榄。 經(jīng)常有香客問我,道長默刚,這世上最難降的妖魔是什么甥郑? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮荤西,結(jié)果婚禮上澜搅,老公的妹妹穿的比我還像新娘。我一直安慰自己邪锌,他們只是感情好勉躺,可當(dāng)我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著觅丰,像睡著了一般饵溅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妇萄,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天蜕企,我揣著相機與錄音,去河邊找鬼冠句。 笑死轻掩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的懦底。 我是一名探鬼主播唇牧,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼聚唐!你這毒婦竟也來了奋构?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤拱层,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后宴咧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體根灯,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年掺栅,在試婚紗的時候發(fā)現(xiàn)自己被綠了烙肺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡氧卧,死狀恐怖桃笙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情沙绝,我是刑警寧澤搏明,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布鼠锈,位于F島的核電站,受9級特大地震影響星著,放射性物質(zhì)發(fā)生泄漏购笆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一虚循、第九天 我趴在偏房一處隱蔽的房頂上張望同欠。 院中可真熱鬧,春花似錦横缔、人聲如沸铺遂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽襟锐。三九已至,卻和暖如春斗蒋,著一層夾襖步出監(jiān)牢的瞬間捌斧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工泉沾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捞蚂,地道東北人。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓跷究,卻偏偏與公主長得像姓迅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子俊马,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,870評論 2 361

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