電商或者內容類APP中仑荐,H5通常都會占據一席之地,Native跟H5通信會必不可少荚坞,比如某些場景H5通知native去分享,native通知H5局部刷新等蚁鳖,Android本身也提供這樣的接口届囚,比如addJavascriptInterface有梆、loadUrl("javascript:..."),而需要支持的能力也要是雙工的奖亚。
- 1:H5通知Native(可能需要處理回調),
- 2:Native通知H5(也可能需要處理回調)
實現這種機制的方式并不唯一析砸,但使用不當經常會引入很多問題昔字,比如:H5同Native需要一個中間js文件,實現簡單的通信協(xié)議首繁,這個js文件有的產品做法是讓前端自己加載作郭,有的做法是客戶端注入,也就是通過loadUrl("javascript:...")注入弦疮。采用客戶端注入這種方式就多少有問題夹攒,因為沒有一個很合適的時機既保證注入成功,又保證注入及時胁塞。如果在onPageStarted時注入咏尝,很多手機會注入失敗,如果onPageFinished時注入啸罢,又太遲编检,導致很多功能打折扣。再比如:有些人通過prompt方式實現H5通知Native扰才,而prompt是一個可能產生問題的同步方法允懂,一旦無法返回,整個js環(huán)境就會掛掉衩匣,導致所有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這個線程中執(zhí)行的鹦牛,雖然不提清楚它跟JS線程的關系,但JS會阻塞等待callNative函數執(zhí)行完畢再往下走勇吊,所以 @JavascriptInterface注解的方法里面最好也不要做耗時操作曼追,最好利用Handler封裝一下,讓每個任務自己處理汉规,耗時的話就開線程自己處理礼殊。
如果前端通知Native時需要回調怎么辦?可以抽離到一個中間的js针史,為每個任務設置一個ID晶伦,暫存回調函數,等到Native處理結束后啄枕,先走這個中間的js婚陪,找到對應的js回調函數執(zhí)行即可,
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執(zhí)行泌参,native那邊會收到js消息,同時里面包含著id常空,等到native執(zhí)行完畢后及舍,將執(zhí)行結果與消息id通知到這個中間層js,找到對應的回調函數執(zhí)行即可窟绷,如下:
jsRPC.onJsCallFinished = function(message) {
var response = message;
<!--找到回調函數-->
var success_cb = _callbacks[response.id].success_cb;
<!--刪除-->
delete _callbacks[response.id];
<!--執(zhí)行回調函數-->
success_cb(response.result);
};
這樣就完成H5通知Native锯玛,同時Native將結果回傳給H5,并完成回調這樣一條通路。Native通知H5,這條路怎么辦攘残?流程大概類似拙友,同樣可以基于一個消息ID完成回調,不過更加靈活歼郭,因為Native通知前端的接口不太好統(tǒng)一遗契,具體使用自己把握。
參考工程 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?對應方法,可以此為入口凛剥,作為消息傳遞通道侠仇,考慮到開發(fā)習慣,一般不會選擇alert跟confirm,?通常會選promopt作為入口逻炊,在App中就是onJsPrompt作為jsbridge的調用入口互亮。由于onJsPrompt是在UI線程執(zhí)行,所以盡量不要做耗時操作余素,可以借助Handler靈活處理豹休。對于回調的處理跟上面的addJavascriptInterface的方式一樣即可,采用消息ID方式做暫存區(qū)分桨吊,區(qū)別就是這里采用 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必須執(zhí)行完畢奕枝,prompt函數才會返回棺榔,否則js線程會一直阻塞在這里。實際使用中確實會發(fā)生這種情況隘道,尤其是APP中有很多線程的場景下症歇,懷疑是這么一種場景:
- 第一步:js線程在執(zhí)行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環(huán)境的問題
- 通過@JavascriptInterface的方法中不要同步處理耗時操作氧猬,需要返回值的方法需要阻塞調用(盡量減少)
- 如果非要用prompt,盡量不要自己destroy webview坏瘩,很容導致js環(huán)境掛了盅抚,所有webview打不開網頁
- 如論哪種實現,都不要直接處理耗時操作倔矾,會阻塞js線程妄均。
作者:看書的小蝸牛
原文鏈接:Android 混合開發(fā)之JsBridge
僅供參考,歡迎指正