javascript
JSBridge的思考
前言
最近在做一個web與原生交互的需求,需求背景是這樣子的,提供一個SDK里面包含一個webview用于加載業務h5,原生這邊賦予webview選擇相片、相機、刷臉、關閉原生界面的能力。雖然這個功能邏輯都是“熟悉的配方”,但還是有不少坑。
webview執行JS阻塞
項目一開始使用的橋接框架是以前項目用的橋接框架,但這個項目里面有一功能點跟舊項目不一樣,舊項目只涉及到單圖片的選擇和上傳而新項目需要支持多圖片選擇和上傳,因為以前單圖片選擇上傳整個過程響應較快,所以沒關注執行JS時卡住了主線程,但這次項目是多圖片選擇上傳而且h5多了ocr識別,導致整個處理相對耗時,原生這邊執行JS一個回調將多張圖片數據回傳給h5處理,實例代碼如下
[UIWebView stringByEvaluatingJavaScriptFromString:jsstring] 復制代碼這個方法是一個同步方法,他會阻塞到JS方法執行結束才會返回,這時整個UI就會卡住。一開始的解決方案是通過原生這邊異步派發隊列解決同步的問題,但這又是一個坑,會致webview出現偶現的crash,這個稍后再詳講。原生這邊不通,那就從JavaScript這一邊著手,熟悉JavaScript的同學都知道,setTimeout方法能夠實現異步,如果代碼中設定了一個 setTimeout,那么瀏覽器便會在合適的時間,將代碼插入任務隊列,如果這個時間設為 0,就代表立即插入隊列,但不是立即執行,仍然要等待前面代碼執行完畢,所以 setTimeout 并不能保證執行的時間,是否及時執行取決于 JavaScript 線程是擁擠還是空閑,但它能夠解決我們執行JS代碼導致的同步問題,在我們原生調用JS回調之前用setTimeout做一層包裝,相當于調用setTimeout方法,一調用就即刻返回,不阻塞線程,實例代碼如下:
function asyncallback(callback,params) {if(typeof callback == 'function'){setTimeout(function () {callback(params);},0);}} 復制代碼Why no WebViewJavascriptBridge
當給出第一版SDK給h5同事聯調的時候,h5同事反饋了幾個意見:
1、橋接依賴于協議定制和iframe,數據傳輸透明,存在安全隱患;
2、調用方式過于硬編碼,調用時需要匹對填入方法名和參數,希望我這邊設計出類似微信web api;
3、webview出現偶現的crash;
4、希望支持命名空間;
有人會問為什么不用業界更加成熟橋接框架WebViewJavascriptBridge,我們通過讀源碼可知WebViewJavascriptBridge底層還是依賴于協議定制和iframe,并不支持命名空間,而且crash還是會出現(網友反饋)。 綜合上次的意見,我們需要重新設計我們的橋接框架,原框架的兩端交互依賴iframe發請求、攔截請求來進行交互,iOS還有另外一個方案來實現兩端交互:JavaScriptCore,想深入了解JavaScriptCore可以看這篇文章,而且通過JavaScriptCore設計的js api的代碼風格可以做到微信web api的效果。JavaScriptCore框架是一個蘋果在iOS7引入的框架,該框架讓 Objective-C 和 JavaScript 代碼直接的交互變得更加的簡單方便,而JavaScriptCore是蘋果Safari瀏覽器的JavaScript引擎。通過JavaScriptCore,我們可以以寫原生代碼的方式寫JavaScript,最終JavaScriptCore都會將我們的原生代碼順滑、安全轉化為JavaScript層的實現。我們以這個JavaScriptCore框架為基礎設計我們的橋接組件XDMicroJSBridge。
XDMicroJSBridge簡概
關鍵類
JSContext: JSContext是JavaScript的執行環境;
JSValue: JSValue代表一個JavaScript實體,一個JSValue可以表示很多JavaScript原始類型例如boolean、 integers、doubles甚至包括對象和函數;
實現原理
先在原生注冊對應的暴露給h5使用js API函數名,通過[JSContext currentArguments]捕獲方法的參數,參數的類型是JSValue,JSValue提供一系列方法將值轉換成合適的Objective-C值或對象,方便這邊原生處理,通過block包裝原生調用方法(相機、相冊等),將block注入JSContext當中,命名空間的實現是往JSContext注入一個空實現的類,需要賦予命名空間的方法則將對應包裝的block注入到這個空實現的類中。想了解具體實現點擊github.com/caixindong/…。實例代碼如下:
- (void)registerAction:(NSString *)action handler:(XDMCJSBHandle)handler {if (action && handler) {__weak typeof(self) weakSelf = self;_context[_nameSpace][action] = ^{NSLog(@"action is %@",action);__strong typeof(weakSelf) strongSelf = weakSelf;strongSelf.webThread = [NSThread currentThread];NSLog(@"webThread is %@",[NSThread currentThread]);NSArray *args = [JSContext currentArguments];JSValue *last = (JSValue *)[args lastObject];XDMCJSBCallback ncallback = nil;NSMutableArray *trueArgs = [NSMutableArray arrayWithArray:args];if ([last isObject] && [[last toDictionary] isEqualToDictionary:@{}]) {[trueArgs removeLastObject];ncallback = ^(NSDictionary *params){[strongSelf performSelector:@selector(_callJSMethodWithArgs:) onThread:strongSelf.webThread withObject:@[last, params] waitUntilDone:NO];};}NSMutableArray *trueOCArgs = [NSMutableArray array];for (JSValue *value in trueArgs) {if ([value isObject]) {[trueOCArgs addObject:[value toDictionary]];} else if ([value isString]) {[trueOCArgs addObject:[value toString]];} else if ([value isNull]) {[trueOCArgs addObject:[NSNull null]];} else if ([value isBoolean]) {[trueOCArgs addObject:[NSNumber numberWithBool:[value toBool]]];}}handler([trueOCArgs copy], ncallback);};} } 復制代碼實現難點
JSValue提供了JavaScript原始類型boolean、integers、doubles、對象轉化方法,但沒有提供函數的轉化方法,因為JS函數參數一般都會包含回調,回調是function對象,所以這一塊轉化是很有必要的,由代碼可見我這邊是通過一個oc的block保存了函數回調的信息。
webthread crash
對于crash問題,經過我多次調試發現,在web與原生交互多次后再觸發下一次交互會發現野指針crash,頻次不定,crash棧定位到webview的webthread。兩種實現方案都會出現這個問題。總所周知,JavaScript是以單線程的方式運行的,所以webview底層會維護一個線程用于處理JavaScript的交互,網上很多例子和教程在webview執行js代碼的時候都會派發到主線程,可是webthread有時候并不在主線程,這是有隱患的,如果是頻次低的交互可能不會觸發這個bug,當頻次高時,就例如我這個項目,h5內有很多表單需要上傳選擇圖片這種跨端操作,就可能會觸發webthread crash。網上資料和官方文檔并沒有對這個crash做具體的解釋,我猜測可能是底層線程通信派發出現問題,所以正確的做法應該是webview內JavaScript的執行和回調應始終在一個線程,以防止線程切換導致偶現crash。那怎么獲取webthread,獲取webthread的時機應該是JavaScript的執行環境初始化完成之后,所以可以在包裝原生調用方法的block捕獲這個webthread,因為h5觸發原生封裝的js api后會跑進封裝原生方法block,這時候上下文已經初始化完成,而且也是在webview維護的webthread內。實例代碼如下:
- (void)registerAction:(NSString *)action handler:(XDMCJSBHandle)handler {if (action && handler) {__weak typeof(self) weakSelf = self;_context[_nameSpace][action] = ^{NSLog(@"action is %@",action);__strong typeof(weakSelf) strongSelf = weakSelf;strongSelf.webThread = [NSThread currentThread];NSLog(@"webThread is %@",[NSThread currentThread]); } 復制代碼然后在這個線程執行js相關邏輯代碼,這樣修改后,crash沒再出現,實例代碼如下:
[self performSelector:@selector(_callJSMethodWithArgs:) onThread:strongSelf.webThread withObject:@[callback, params] waitUntilDone:NO]; 復制代碼最終框架實現效果
相比其他橋接框架,XDMicroJSBridge更加輕量(代碼量不到100行),支持命名空間,原生專注原生代碼,web專注JavaScript,維護一致的web thread。
初始化Bridge
#import "XDMicroJSBridge.h" @property (nonatomic, strong) UIWebView *webview; @property (nonatomic, strong) XDMicroJSBridge *bridge; @property (nonatomic, copy) XDMCJSBCallback callback; self.bridge = [XDMicroJSBridge bridgeForWebView:_webview]; 復制代碼注冊JS方法
__weak typeof(self) weakself = self; [_bridge registerAction:@"camerapicker" handler:^(NSArray *params, XDMCJSBCallback callback) {dispatch_async(dispatch_get_main_queue(), ^{//if your javaScript method has callback, you should register this call like this.if (callback) {weakself.callback = callback;}UIImagePickerController *cameraVC = [[UIImagePickerController alloc] init];cameraVC.delegate = weakself;cameraVC.sourceType = UIImagePickerControllerSourceTypeCamera;[weakself presentViewController:cameraVC animated:YES completion:nil];});}]; 復制代碼h5調用原生注冊的JS方法
<script>function clickcamera() {XDMCBridge.camerapicker(function (response) {var photos = response['photos'];var insert = document.getElementById('insert');for(var i = 0; i < photos.length; i++) {var img = new Image(100,100);img.src = photos[i];insert.appendChild(img);}});} </script> 復制代碼想了解更多iOS終端相關知識可以前往終端雜談。
總結
以上是生活随笔為你收集整理的JSBridge的思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mongodb在使用过程中有什么问题
- 下一篇: Python基本数据类型之整型