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

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

OC調用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:@"從網頁端獲取的用戶信息" 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方法里面, 這里創建一個NSMutableDictionary對象message, 并把VC傳遞進來的參數data = @{@"userId":@"DX001"}, handlerName = @"getUserInfo"還有responseCallback保存起來, 和之前JS保存responseCallback方法相似, 這里也是生成一個callbackId, 并把responseCallback保存在以callbackId為key的字典self.responseCallbacks中, 最后執行[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中執行了_queueMessage, 實際也是OC調用JS. 只是在上文中, OC調用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中的一個數組, 這個數組在WebViewJavascriptBridgeBase初始化的時候被創建, 但是只是一個空的數組, 并且在初始化注入的時候就被取出來并置空了, 所以后面正常情況是不存在_queueMessage走進if分支的, 只有一種情況, 就是在injectJavascriptFile還沒執行的時候, 先進行了OC對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 = 'wvjbscheme://__BRIDGE_LOADED__';
            document.documentElement.appendChild(WVJBIframe);
            setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
        }

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

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

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

經過一些列調用_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中下斷點, 發現


image.png

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

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

responseCallback這里打斷點, 然后調用JS接口, Safari左側會出現調用堆棧, 里面有我們注入的代碼, 這時候就可以在_dispatchMessageFromObjC里面打斷點了, 好了, 繼續分析_dispatchMessageFromObjC里面的代碼.
由于調用的時候并沒有給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調用OC時候, OC回調JS不同的, 這里message.responseId是沒有值的, message.callbackId是有值的, 所以會在這里創建一個JS的responseCallback, 后面取出handler并調用handler(message.data, responseCallback);, 下面來看下messageHandlers吧, 實際和OC注冊很相似

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

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

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

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

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

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

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

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

image.png
, 而handlerNameresponseId都是OC調用的時候傳遞的參數.

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

進入_doSend, 由于沒傳遞responseCallback, 所以if走不到, 這里還是把OC傳遞過來的message保存在sendMessageQueue中, 然后改變src觸發OC執行, 來到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];

執行responseCallback, 回調OC之前傳遞的block, 至此, OC調用JS, 并把JS端數據取回完美結束.

總結

在實際中, 我們應該很少會用到JS提供接口給OC調用, 通常是OC提供穩定通用接口給JS調用, 所以本文不是我們實踐的重點, 但是作為講解框架的完整性, 我們應該把OC調用JS和JS調用OC都進行詳細的分析, 這樣能更好的理解作者設計的意圖和架構的巧妙之處.

遺留問題

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

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容