Android 混合開發之JsBridge
- 2019 年 11 月 11 日
- 筆記
電商或者內容類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
僅供參考,歡迎指正

