JSBridge的思考

前言

最近在作一個web與原生交互的需求,需求背景是這樣子的,提供一個SDK裏面包含一個webview用於加載業務h5,原生這邊賦予webview選擇相片、相機、刷臉、關閉原生界面的能力。雖然這個功能邏輯都是「熟悉的配方」,但仍是有很多坑。html

webview執行JS阻塞

項目一開始使用的橋接框架是之前項目用的橋接框架,但這個項目裏面有一功能點跟舊項目不同,舊項目只涉及到單圖片的選擇和上傳而新項目須要支持多圖片選擇和上傳,由於之前單圖片選擇上傳整個過程響應較快,因此沒關注執行JS時卡住了主線程,但此次項目是多圖片選擇上傳並且h5多了ocr識別,致使整個處理相對耗時,原生這邊執行JS一個回調將多張圖片數據回傳給h5處理,實例代碼以下java

[UIWebView stringByEvaluatingJavaScriptFromString:jsstring]
複製代碼

這個方法是一個同步方法,他會阻塞到JS方法執行結束纔會返回,這時整個UI就會卡住。一開始的解決方案是經過原生這邊異步派發隊列解決同步的問題,但這又是一個坑,會致webview出現偶現的crash,這個稍後再詳講。原生這邊不通,那就從JavaScript這一邊着手,熟悉JavaScript的同窗都知道,setTimeout方法可以實現異步,若是代碼中設定了一個 setTimeout,那麼瀏覽器便會在合適的時間,將代碼插入任務隊列,若是這個時間設爲 0,就表明當即插入隊列,但不是當即執行,仍然要等待前面代碼執行完畢,因此 setTimeout 並不能保證執行的時間,是否及時執行取決於 JavaScript 線程是擁擠仍是空閒,但它可以解決咱們執行JS代碼致使的同步問題,在咱們原生調用JS回調以前用setTimeout作一層包裝,至關於調用setTimeout方法,一調用就即刻返回,不阻塞線程,實例代碼以下:ios

function asyncallback(callback,params) {if(typeof callback == 'function'){setTimeout(function () {callback(params);},0);}}
複製代碼

Why no WebViewJavascriptBridge

當給出初版SDK給h5同事聯調的時候,h5同事反饋了幾個意見:
一、橋接依賴於協議定製和iframe,數據傳輸透明,存在安全隱患;
二、調用方式過於硬編碼,調用時須要匹對填入方法名和參數,但願我這邊設計出相似微信web api;
三、webview出現偶現的crash;
四、但願支持命名空間;
有人會問爲何不用業界更加成熟橋接框架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。git

XDMicroJSBridge簡概

關鍵類

JSContext: JSContext是JavaScript的執行環境;
JSValue: JSValue表明一個JavaScript實體,一個JSValue能夠表示不少JavaScript原始類型例如boolean、 integers、doubles甚至包括對象和函數;github

實現原理

先在原生註冊對應的暴露給h5使用js API函數名,經過[JSContext currentArguments]捕獲方法的參數,參數的類型是JSValue,JSValue提供一系列方法將值轉換成合適的Objective-C值或對象,方便這邊原生處理,經過block包裝原生調用方法(相機、相冊等),將block注入JSContext當中,命名空間的實現是往JSContext注入一個空實現的類,須要賦予命名空間的方法則將對應包裝的block注入到這個空實現的類中。想了解具體實現點擊github.com/caixindong/…。實例代碼以下:web

- (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保存了函數回調的信息。api

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。bash

初始化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終端相關知識能夠前往終端雜談

相關文章
相關標籤/搜索