Android 混合開發之JsBridge

電商或者內容類APP中,H5通常都會佔據一席之地,Native跟H5通信會必不可少,比如某些場景H5通知native去分享,native通知H5局部刷新等,Android本身也提供這樣的接口,比如addJavascriptInterface、loadUrl(“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對象

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

webView.addJavascriptInterface(new JsMethodApi(), "mJsMethodApi");

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

<head>
    <script type="text/javascript"  >
       JsMethodApi.callNative('頭部就可以回調');
    </script>
</head>

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

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

 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,找到對應的回調函數執行即可,如下:

 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通知前端的接口不太好統一,具體使用自己把握。

參考工程 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

僅供參考,歡迎指正

發佈了51 篇原創文章 · 獲贊 33 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章