原生與JS交互 iOS

 

前言

Hybrid App(混合模式移動應用)是指介於web-app、native-app這二者之間的app,兼具「Native App良好用戶交互體驗的優點」和「Web App跨平臺開發的優點」。談到Hybrid App,JS與Native code的交互就是一個繞不開的話題,這時就須要「一座橋」來鏈接兩端。
JSBridge架起了一座鏈接JavaScriptNative Code的橋樑,讓兩端能夠相互調用。javascript

 
JSBridge.png

本文基於UIWebView,將會分別介紹3種方案。經過IframeAjaxJSCore來實現JSBridge,涉及到的Demo地址,順手給個Star唄😏。html

實現方案

Iframe

廢話很少說,直入主題,首先講的這種方案比較常見。WebViewJavascriptBridgeCordava都是採用的該方案(推薦看看我以前的文章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主動調用這個方法取。併發

Ajax

第二種方案是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]; } 

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對於併發有個優化策略,很值得參考,將併發的屢次調用打包合併爲一次處理。

 
15401284999756.jpg

 

 

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

相關文章
相關標籤/搜索