Hybrid App(混合模式移動應用)是指介於web-app、native-app這二者之間的app,兼具「Native App良好用戶交互體驗的優點」和「Web App跨平臺開發的優點」。談到Hybrid App,JS與Native code的交互就是一個繞不開的話題,這時就須要「一座橋」來鏈接兩端。
JSBridge
架起了一座鏈接JavaScript
與Native Code
的橋樑,讓兩端能夠相互調用。javascript
本文基於UIWebView
,將會分別介紹3種方案。經過Iframe
、Ajax
、JSCore
來實現JSBridge,涉及到的Demo地址,順手給個Star唄😏。html
廢話很少說,直入主題,首先講的這種方案比較常見。WebViewJavascriptBridge
與Cordava
都是採用的該方案(推薦看看我以前的文章Cordova源碼解析)。
核心思路就是在UIWebView攔截Iframe的src,雙方提早約定好協議,例如https://__jsbridge__
就是一次調用開始。
能夠學習Cordova
的策略,將併發的屢次調用打包合併爲一次處理,能夠優化性能。java
1.JS暴露一個方法給Native,接收執行結果git
function responseFromObjC(response) { if (!callback) { return; } callback(response); }
2.Native實現UIWebView
的代理,在webView:shouldStartLoadWithRequest:navigationType:
方法攔截請求,識別到特定URL,開始一次調用流程。github
// 攔截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,開始一次調用web
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。bash
...
// 獲取調用參數,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主動調用這個方法取。併發
第二種方案是JS使用XMLHttpRequest
發起請求,在Native攔截達到調用的目的。經過自定義NSURLProtocol
能夠攔截到Ajax請求。Demo裏有詳細的代碼和註釋,建議結合Demo一塊兒看。app
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]; }
前兩種方案雖然實現方法不一致,可是思路都是相似的,因爲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