電商或者內容類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會阻塞等待較長時間,如下圖
方案二:通過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中可以驗證。
上圖中,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
僅供參考,歡迎指正