Android 混合開發之JsBridge

電商或者內容類APP中,H5一般都會佔據一席之地,Native跟H5通訊會必不可少,好比某些場景H5通知native去分享,native通知H5局部刷新等,Android自己也提供這樣的接口,好比addJavascriptInterface、loadUrl("javascript:..."),而須要支持的能力也要是雙工的。javascript

  • 1:H5通知Native(可能須要處理回調),
  • 2:Native通知H5(也可能須要處理回調

實現這種機制的方式並不惟一,但使用不當常常會引入不少問題,好比:H5同Native須要一箇中間js文件,實現簡單的通訊協議,這個js文件有的產品作法是讓前端本身加載,有的作法是客戶端注入,也就是經過loadUrl("javascript:...")注入。採用客戶端注入這種方式就多少有問題,由於沒有一個很合適的時機既保證注入成功,又保證注入及時。若是在onPageStarted時注入,不少手機會注入失敗,若是onPageFinished時注入,又太遲,致使不少功能打折扣。再好比:有些人經過prompt方式實現H5通知Native,而prompt是一個可能產生問題的同步方法,一旦沒法返回,整個js環境就會掛掉,致使全部H5頁面都沒法打開,下面簡單說下兩種實現,一是經過addJavascriptInterface,另外一種就 是經過prompt。前端

方案一:藉助WebView.addJavascriptInterface實現H5與Native通訊

WebView的addJavascriptInterface方法容許Natvive向Web頁面注入Java對象,以後,在js中即可以直接訪問該對象,使用@JavascriptInterface註解的方法。好比經過以下代碼向前端注入一個名字爲mJsMethodApi的java對象java

class JsMethodApi {
     
    /**
     * js調用native,可能須要回調
     */
    @JavascriptInterface
    public void callNative(String jsonString) {
        ...
    }
}

webView.addJavascriptInterface(new JsMethodApi(), "mJsMethodApi");
複製代碼

在前端的js代碼中,是能夠直接經過mJsMethodApi.callNative(jsonString)通知Native的,並且經過addJavascriptInterface注入的對象在H5的任何地方均可以調用,不存在注入時機跟注入失敗的問題,在H5的head裏調用都沒問題。git

<head>
    <script type="text/javascript"  >
       JsMethodApi.callNative('頭部就能夠回調');
    </script>
</head>
複製代碼

經測試,實際上是能夠通知到Native的,不過有一點須要注意callNative是這JavaBridge這個線程中執行的,雖然不提清楚它跟JS線程的關係,但JS會阻塞等待callNative函數執行完畢再往下走,因此 @JavascriptInterface註解的方法裏面最好也不要作耗時操做,最好利用Handler封裝一下,讓每一個任務本身處理,耗時的話就開線程本身處理。github

若是前端通知Native時須要回調怎麼辦?能夠抽離到一箇中間的js,爲每一個任務設置一個ID,暫存回調函數,等到Native處理結束後,先走這個中間的js,找到對應的js回調函數執行便可,web

var _callbacks = {};
 
 function callNative(method, params, success_cb, error_cb) {

     var request = {
         version: jsRPCVer,
         method: method,
         params: params,
         id: _current_id++
     };
  <!--暫存回調函數-->
     if (typeof success_cb !== 'undefined') {
         _callbacks[request.id] = {
             success_cb: success_cb,
             error_cb: error_cb
         };
     }
     <!--利用JsMethodApi通知Native-->
    JsMethodApi.callNative(JSON.stringify(request));
 };
複製代碼

以上js代碼完成回調的暫存、通知native執行,native那邊會收到js消息,同時裏面包含着id,等到native執行完畢後,將執行結果與消息id通知到這個中間層js,找到對應的回調函數執行便可,以下:chrome

jsRPC.onJsCallFinished = function(message) {
        var response = message;
             <!--找到回調函數-->
             var success_cb = _callbacks[response.id].success_cb;
             <!--刪除-->
             delete _callbacks[response.id];
             <!--執行回調函數-->
             success_cb(response.result);
 };
複製代碼

這樣就完成H5通知Native,同時Native將結果回傳給H5,並完成回調這樣一條通路。Native通知H5,這條路怎麼辦?流程大概相似,一樣能夠基於一個消息ID完成回調,不過更加靈活,由於Native通知前端的接口不太好統一,具體使用本身把握。json

參考工程 https://github.com/happylishang/CMJsBridge 安全

注意不要混淆多線程

若是混淆了,@JavascriptInterface註解的方法可能就沒了,結果是,JS就沒辦法知己調用對應的方法,致使通訊失敗。

關於漏洞問題

4.2之後,WebView會禁止JS調用沒有添加@JavascriptInterface方法, 解決了安全漏洞,並且不多APP兼容到4.2之前,安全問題能夠忽略。

關於阻塞問題

JavascriptInterface注入的方法被js調用時,能夠看作是一個同步調用,雖然二者位於不一樣線程,可是應該存在一個等待通知的機制來保證,因此Native中被回調的方法裏儘可能不要處理耗時操做,不然js會阻塞等待較長時間,以下圖

801573097289_.pic.jpg

方案二:經過prompt實現H5與Native的通訊

平常使用Webview的時候通常都會設置WebChromeClient,用來處理一些進度、title之類的事件,除此以外,WebChromeClient還提供了幾個js回調的入口,如onJsPrompt,onJsAlert等,在前端調用​window.alert​,​window.confirm​,​window.prompt​時,

public boolean onJsAlert(WebView view, String url, String message,
            JsResult result) {
        return false;
    }
 
    public boolean onJsConfirm(WebView view, String url, String message,
            JsResult result) {
        return false;
    }

 
    public boolean onJsPrompt(WebView view, String url, String message,
            String defaultValue, JsPromptResult result) {
        return false;
    }
複製代碼

在js調用​window.alert​,​window.confirm​,​window.prompt​時,​會調用WebChromeClient​對應方法,能夠此爲入口,做爲消息傳遞通道,考慮到開發習慣,通常不會選擇alert跟confirm,​一般會選promopt做爲入口,在App中就是onJsPrompt做爲jsbridge的調用入口。因爲onJsPrompt是在UI線程執行,因此儘可能不要作耗時操做,能夠藉助Handler靈活處理。對於回調的處理跟上面的addJavascriptInterface的方式同樣便可,採用消息ID方式作暫存區分,區別就是這裏採用 prompt(JSON.stringify(request));通知native,以下:

function callNative(method, params, success_cb, error_cb) {

     var request = {
         version: jsRPCVer,
         method: method,
         params: params,
         id: _current_id++
     };

     if (typeof success_cb !== 'undefined') {
         _callbacks[request.id] = {
             success_cb: success_cb,
             error_cb: error_cb
         };
     }
    prompt(JSON.stringify(request));
 };
複製代碼

同以前JavaBridge線程相似,這裏prompt的js線程必需要等待UI線程中onJsPrompt返回纔會喚醒,能夠認爲是個同步阻塞調用(應該是經過線程等待來作的)。

public class JsWebChromeClient extends WebChromeClient {

    JsBridgeApi mJsBridgeApi;

    public JsWebChromeClient(JsBridgeApi jsBridgeApi) {
        mJsBridgeApi = jsBridgeApi;
    }

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
	        try {
            if (mJsBridgeApi.handleJsCall(message)) {
            <!--若是睡眠10s js就會等待10s-->
				//    Thread.sleep(10000);
                result.confirm("sdf");
                return true;
            }
        } catch (Exception e) {
            return true;
        }
        //   未處理走默認邏輯
        return super.onJsPrompt(view, url, message, defaultValue, result);
    }
}
複製代碼

若是在onJsPrompt睡眠10s,js的prompt函數必定會阻塞等待10s才返回,這個設計就要求咱們不能在onJsPrompt中作耗時操做,systrace中能夠驗證。

image.png

上圖中,chrome_iothread看作js線程。

prompt的一個坑致使js掛掉

從表現上來看,onJsPrompt必須執行完畢,prompt函數纔會返回,不然js線程會一直阻塞在這裏。實際使用中確實會發生這種狀況,尤爲是APP中有不少線程的場景下,懷疑是這麼一種場景:

  • 第一步:js線程在執行prompt時被掛起,
  • 第二部 :UI線程被調度,剛好銷燬了Webview,調用了 (webview的detroy),detroy以後,致使 onJsPrompt不會被回調,prompt一直等着,js線程就一直阻塞,致使全部webview打不開,一旦出現可能須要殺進程才能解決。

若是不主動destroy webview,能夠很大程度避免這個問題,具體Chrome的實現如何,還沒分析過,這裏只是根據現象推測如此。而WebView.addJavascriptInterface並不會有這個問題,不管是否主動destroy Webview,都不會上述問題,可能chrome對addJavascriptInterface這種方式作了額外處理,在本身銷燬的時候,主動喚起JS線程,可是onJsPrompt所在的UI線程顯然沒處理這種場景。

參考工程 https://github.com/happylishang/CMJsBridge

總結

  • 最好經過前端注入,這樣就能夠避免注入失敗與注入時機很差把握的問題
  • 建議採用WebView.addJavascriptInterface實現,能夠避免prompt掛掉js環境的問題
  • 經過@JavascriptInterface的方法中不要同步處理耗時操做,須要返回值的方法須要阻塞調用(儘可能減小)
  • 若是非要用prompt,儘可能不要本身destroy webview,很容致使js環境掛了,全部webview打不開網頁
  • 如論哪一種實現,都不要直接處理耗時操做,會阻塞js線程。

做者:看書的小蝸牛 原文連接:Android 混合開發之JsBridge

僅供參考,歡迎指正

相關文章
相關標籤/搜索