前言
Hybrid App(混合模式移動應用)是指介於web-app、native-app這兩者之間的app,兼具“Native App良好用戶交互體驗的優勢”和“Web App跨平台開發的優勢”。談到Hybrid App,JS與Native code的交互就是一個繞不開的話題,這時就需要“一座橋”來連接兩端。
JSBridge
架起了一座連接JavaScript
與Native Code
的橋梁,讓兩端可以相互調用。

本文基於UIWebView
,將會分別介紹3種方案。通過Iframe
、Ajax
、JSCore
來實現JSBridge,涉及到的Demo地址,順手給個Star唄😏。
實現方案
Iframe
廢話不多說,直入主題,首先講的這種方案比較常見。WebViewJavascriptBridge
與Cordava
都是采用的該方案(推薦看看我之前的文章Cordova源碼解析)。
核心思路就是在UIWebView攔截Iframe的src,雙方提前約定好協議,例如https://__jsbridge__
就是一次調用開始。
可以學習Cordova
的策略,將並發的多次調用打包合並為一次處理,可以優化性能。
實現
1.JS暴露一個方法給Native,接收執行結果
function responseFromObjC(response) { if (!callback) { return; } callback(response); }
2.Native實現UIWebView
的代理,在webView:shouldStartLoadWithRequest:navigationType:
方法攔截請求,識別到特定URL,開始一次調用流程。
// 攔截JS調用原生核心方法 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { NSURL *url = request.URL; // 判斷url是否是JSBridge調用 if ([url.host isEqualToString:@"__jsbridge__"]) { // 處理JS調用Native return NO; } return YES; }
3.JS開啟一個Iframe,加載一個特定的URL,開始一次調用
var iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = 'https://__jsbridge__?action='+ action + '&data=' + data; document.documentElement.appendChild(iframe);
4.Native方法執行完成后,調用JS方法responseFromObjC
將結果回傳給JS。
...
// 獲取調用參數,demo的調用方式是:'https://__jsbridge__?action=action&data=' // 參數直接放在query里面的,更好的方案是js暴露一個方法給原生,原生調用方法獲取數據 NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; NSArray *queryItems = urlComponents.queryItems; NSMutableDictionary *params = [NSMutableDictionary dictionary]; for (NSURLQueryItem *queryItem in queryItems) { NSString *key = queryItem.name; NSString *value = queryItem.value; [params setObject:value forKey:key]; } NSString *action = params[@"action"]; NSString *data = params[@"data"]; if ([action isEqualToString:@"alertMessage"]) { // 調用原生方法,獲取數據 // js暴露方法`responseFromObjC`給原生,原生通過該方法回調 // 在實際項目中,為了實現實現js並發原生方法,最好帶一個callBackID,來區分不同的調用 [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"responseFromObjC('%@')", data]]; } else { [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"responseFromObjC('Unkown action'"]]; }
PS:demo代碼為了簡化,直接將參數放在URL的query里,如果只傳輸一些簡單數據是沒有問題的,更好的方案是JS先將參數存放起來,通過URL傳遞一個key給Native,再暴露一個通過key取數據的方法,Native主動調用這個方法取。
Ajax
第二種方案是JS使用XMLHttpRequest
發起請求,在Native攔截達到調用的目的。通過自定義NSURLProtocol
可以攔截到Ajax請求。Demo里有詳細的代碼和注釋,建議結合Demo一起看。
實現
1.新建類繼承自NSURLProtocol
,並注冊。
[NSURLProtocol registerClass:[CRURLProtocol class]];
2.實現自定義NSURLProtocol
,在startLoading
方法攔截Ajax請求
- (void)startLoading { NSURL *url = [[self request] URL]; // 攔截“http://__jsbridge__”請求 if ([url.host isEqualToString:@"__jsbridge__"]) { // 處理JS調用Native } }
3.JS發起Ajax請求,URL為提前約定的特殊值,例如:http://jsbridge。請求參數放在Request Body
里。
// 調用原生 function callNative(action, data) { var xhr = new window.XMLHttpRequest(), url = 'http://__jsbridge__'; xhr.open('POST', url, false); xhr.send(JSON.stringify({ action: action, data: data })); return xhr.responseText; }
4.Naive攔截到請求,獲取參數,執行Native方法,最后通過Ajax的Response
把結果返回給JS。
...
// 2. 從HTTPBody中取出調用參數 NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:self.request.HTTPBody options:NSJSONReadingAllowFragments error:nil]; NSString *action = dic[@"action"]; NSString *data = dic[@"data"]; NSData *responseData; // 3. 根據action轉發到不同方法處理,param攜帶參數 if ([action isEqualToString:@"alertMessage"]) { responseData = [data dataUsingEncoding:NSUTF8StringEncoding]; } else { responseData = [@"Unknown action" dataUsingEncoding:NSUTF8StringEncoding]; } // 4. 處理完成,將結果返回給js [self sendResponseWithResponseCode:200 data:responseData mimeType:@"text/html"]; ... - (void)sendResponseWithResponseCode:(NSInteger)statusCode data:(NSData*)data mimeType:(NSString*)mimeType { NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:[[self request] URL] statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:@{@"Content-Type" : mimeType}]; [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; if (data != nil) { [[self client] URLProtocol:self didLoadData:data]; } [[self client] URLProtocolDidFinishLoading:self]; }
JSCore
前兩種方案雖然實現方法不一致,但是思路都是類似的,由於JS不能直接調用Native方法,通過曲線救國的方式,找到一個載體來傳遞信息。
第三種方案就比較直接了,使用iOS7推出的黑科技JavaScriptCore
,將Native方法直接暴露給JS,打通兩端的數據通道。談到JavaScriptCore
不得不說的是bang590的JSPatch
,還有ReactNative、Weex等都是利用JavaScriptCore
來實現各種炫酷的功能。(強力推薦一本Lefe_x的書《一份走心的JS-Native交互電子書》,非常精彩)。
不過這種方案有個缺陷,UIWebView
沒有暴露JSContext
,雖然可以通過KVC拿到,但是畢竟不是一種完美的解決方案,不知道上架會不會有風險(求知道的同學指教一下)。
實現
實現流程就不細說了,流程比較簡單,Demo里面有。說說關鍵實現代碼
- (void)injectJSBridge { // 獲取JSContext JSContext *context = [_webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; // 給JS注入方法callNative context[@"callNative"] = ^(JSValue *action, JSValue *data) { NSString *actionStr = [action toString]; NSString *dataStr = [data toString]; if ([actionStr isEqualToString:@"alertMessage"]) { return dataStr; } else { return @"Unkown action"; } }; }
JS調用非常簡單,一句話搞定。
callNative("alertMessage", "Hello world!")
性能對比
為了驗證三種方案的性能,設計了個簡單的實驗,分別執行了100、1000、10000次調用,測試手機iPhone X,系統iOS 12,時間對比如下圖所示。
先說結論,JSCore的性能是最優的,JSCore>Ajax>Iframe
。在低並發的時候三種方案差距不大,執行次數10000次時Iframe效率就很低了,Ajax次之,JSCore性能很穩定。當然實際使用的時候不會出現調用10000次這種極限情況。
Cordova
對於並發有個優化策略,很值得參考,將並發的多次調用打包合並為一次處理。

轉自:https://www.jianshu.com/p/eff176e220e0