轉自:https://imnerd.org/android-webview-and-js.html
WebView 是移動端應用中的一個控件萨咕,提供了類似瀏覽器可以在 App 中加載網頁的功能∏垂牛現(xiàn)在市面上很多應用都會使用這種方式內嵌一些 h5 頁面用來實現(xiàn)產品功能徐矩。使用這種方式帶來的好處就是支持快速迭代更新抛猫,并且頁面的功能是全網升級附较。當然目前 RN 和 Codorva 給我們帶來的熱更新方案也是可以的饵沧,只是目前 Apple 的態(tài)度很拒絕锨络,這里我們略過不表。在 WebView 中的網頁勢必存在和客戶端進行交互的動作狼牺,進行數(shù)據(jù)的共享羡儿。下面我們就來說說 Android WebView 中 JS 和 Native 的交互方式。
客戶端調用 JS
loadUrl()
我們明白 WebView 其實就是在加載網頁是钥,所以客戶端可以直接訪問?javascript:console.log('hello')?這樣的偽 URL 即可實現(xiàn)在頁面注入需要執(zhí)行的 JS 代碼掠归。調用方法如下:
WebView webview = (WebView) findViewById(R.id.webView);
webview.loadUrl("javascript:console.log('hello')");
這樣我們就實現(xiàn)了調用 JS 的目的了。loadUrl()?的方案從另外一個角度來看可以算是 hack 方案了悄泥,對客戶端來說虏冻,他們的 JS 交互本質上其實就是一個拼接 JS 字符串的過程。
evaluateJavascript()
剛才我們也說了?loadUrl()?不是 Android 的正經解決方法弹囚。好在官方也想到了這點厨相,在 Android 4.4+ 之后,官方給提供了原生的方法支持調用鸥鹉,那就是?evaluateJavascript()蛮穿。這個方法最大的好處就是能夠直接在一次執(zhí)行的時候獲取到 JS 返回的結果。如果是使用?loadUrl()?的方式的話毁渗,執(zhí)行完后對客戶端來說這句話就結束了践磅,如果想要拿到返回的結果的話另外需要 JS 調用客戶端的方法返回。
WebView webview = (WebView) findViewById(R.id.webView);
webview.evaluateJavascript("javascript:Date.now()", new ValueCallback<String>() {
? ? @Override
? ? public void onReceiveValue(String value) {
? ? ? ? System.out.println(value); //1515827651551
? ? }
});
可以看到調用方法和?loadUrl()?非常類似灸异,區(qū)別是增加了一個 callback 方法可以獲取到 JS 返回的值府适。該方法無疑比較優(yōu)秀羔飞,不過對兼容性有要求,目前市面上用還是使用前一種方法的比較多细溅。
JS 調用客戶端
相比較客戶端調用 JS 的方法褥傍,JS 調用客戶端的方法就比較多了,簡單歸類一下其實可以分為注入映射和方法劫持兩種喇聊。注入映射主要是使用官方提供的?addJavascriptInterface()?方法將 Java 對象和 JS 對象進行映射恍风。而方法劫持則是利用 JS 的一些系統(tǒng)方法調用會觸發(fā) Java 的事件回調,然后在回調中進行事件劫持誓篱,從而執(zhí)行客戶端方法朋贬。下面我們來具體看看。
addJavascriptInterface
addJavascriptInterface()?方法的使用非常簡單窜骄,定義好被調用的方法對象后直接配置映射關系即可锦募。
//定義好 Java 接口對象
public class SDK extends Object {
? ? @JavascriptInterface
? ? public void hello(String msg) {
? ? ? ? System.out.println("Hello World");
? ? }
}
//Webview 中調用
WebView webview = (WebView) findViewById(R.id.webview);
webview.addJavascriptInterface(new SDK(), 'sdk');
webview.loadUrl('http://imnerd.org'); //注入后加載頁面
這樣加載的頁面中就可以直接執(zhí)行?sdk.hello()?方法來執(zhí)行客戶端方法了。不過這種官方推薦的方法在 4.2- 的系統(tǒng)上存在遠程執(zhí)行安全漏洞邻遏,對 4.2 以下系統(tǒng)版本有要求的應用需要謹慎使用糠亩。目前來看 4.2 還是需要保持支持的。
URL劫持
URL劫持主要是使用?shouldOverrideUrlLoading()?進行 WebView URL 劫持准验。從方法名可以看出赎线,它是 WebView 攔截 URL 的一種回調,當 WebView 發(fā)生 URL 跳轉的時候會觸發(fā)該回調糊饱。在該回調中我們能夠獲取到前端提供的 URL 地址垂寥。我們通過構造約定協(xié)議的 URL 地址提供給客戶端識別,識別成功后執(zhí)行對應的方法即可另锋。
WebView webview = (WebView) findViewById(R.id.webview);
webview.loadUrl('http://imnerd.org');
webview.setWebViewClient(new WebViewClient() {
? @Override
? public boolean shouldOverrideUrlLoading(WebView view, String url) {
? ? if(url.equals('sdk:hello')) {
? ? ? System.out.println('hello world');
? ? ? return true;
? ? }
? ? return super.shouldOverrideUrlLoading(view, url);
? }
});
方法劫持
同 URL 劫持類似滞项,方法劫持主要是利用 JS 的一些方法執(zhí)行時會觸發(fā) Android 客戶端中的一些回調,通過對前端參數(shù)進行識別來執(zhí)行對應的客戶端代碼夭坪。目前前端主要有以下四種方法會觸發(fā)對應的回調方法文判,對應關系如下:
| JS方法 | 客戶端回調 |
|-------------|------------------|
| alert |?onJsAlert?|
| prompt |?onJsPrompt?|
| confirm |?onJsConfirm?|
| console.log |?onConsoleMessage?|
將這四個方法列在一塊是因為這幾個方法的本質上都是差不多,定義好對應的回調方法即可台舱÷筛埽客戶端具體的配置如下:
//定義好劫持回調類
private class hijackWebChromeClient extends WebChromeClient {?
? public boolean hijack(String text) {
? ? if(text.equals('sdk:hello')) {
? ? ? System.out.println('hello world');
? ? ? return true;
? ? }
? ? return false;
? }
? @Override
? public boolean onJsPrompt(WebView view, String message, String defaultValue, JSPromptResult result) {
? ? if(this.hijack(message)) {
? ? ? return true;
? ? }
? ? return super.onJsPrompt(view, url, message, defaultValue, result);
? }
? @Override
? public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
? ? if(this.hijack(message)) {
? ? ? return true;
? ? }
? ? return super.onJsAlert(view, url, message, result);
? }
? @Override
? public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
? ? if(this.hijack(message)) {
? ? ? return true;
? ? }
? ? return super.onJsConfirm(view, url, message, result);
? }
? @Override?
? public boolean onConsoleMessage(ConsoleMessage consoleMessage) {?
? ? String message = consoleMessage.message();
? ? if(this.hijack(message)) {
? ? ? return true;
? ? }
? ? return super.onConsoleMessage(consoleMessage);?
? }?
? @Override?
? public void onConsoleMessage(String message, int lineNumber, String sourceID) {
? ? if(this.hijack(message)) {
? ? ? return true;
? ? }
? ? super.onConsoleMessage(message, lineNumber, sourceID);?
? }?
}
//注入劫持回調類
WebView webview = (WebView) findViewById(R.id.webview);
webview.loadUrl('http://imnerd.org');
webview.setWebChromeClient(new hijackChromeClient);
這里為了方便展示,將所有回調的方法都寫全了竞惋,實際上在實際的使用過程中一般都是約定好一種調用方式即可。另外?console.log?對應的回調寫了兩種灰嫉,三參數(shù)的是老版本方法拆宛,在新API中已經被廢棄,推薦使用?ConsoleMessage?對象傳參方式讼撒。
總結
以上講述了 JS 調用客戶端的方法浑厚,以及客戶端調用前端的方法股耽。除了這兩種單向調用的方式之外,往往比較多的是 JS 調用客戶端方法钳幅,客戶端再調用 JS 返回結果的雙向調用物蝙。在 JS 調用的時候需要傳入一個回調方法名,然后客戶端直接執(zhí)行回調方法敢艰。這樣就完成了一個完成的信息交流的過程诬乞。
window.hello = function(text) {
? console.log(text);
};
console.log('$hello:{"callback": "hello"}');
webview.loadUrl('javascript:hello("hello world")');
這些調用方法有兩點需要注意:
不管是前端調用還是客戶端調用,所有的調用的結果返回都是異步的钠导≌鸺担客戶端?loadUrl()?需要另外通過 JS 異步回調客戶端方法告訴結果,evaluateJavascript()?也需要傳如一個異步回調方法牡属。前端調用中?addJavascriptInterface()?是無返回值的票堵,而方法劫持中,需要等待客戶端回調我們的 JS 方法才能異步獲取到數(shù)據(jù)逮栅。所以我們需要對異步通信進行妥善處理悴势。
由于 JS 和客戶端無法實現(xiàn)內存共享,所以所有的數(shù)據(jù)必須字符串化措伐,只能通過字符串進行交流特纤。例如兩邊的復雜對象數(shù)據(jù),需要使用類似 JSON 的格式進行字符串化废士,而文件/圖片等二進制數(shù)據(jù)最好使用 base64 字符串化叫潦。
后記
基本上交互的基本方式就是以上幾種,不過有人將通信機制進行了封裝官硝,形成一套完善的 WebviewJSBridge 方案矗蕊,提供了客戶端調前端,前端調用客戶端的系統(tǒng)解決方案氢架。例如?lzyzsd/JsBridge?項目傻咖,我們從代碼中可以看到,其實它在底層是使用了 URL 劫持的方法與 JS 進行交互岖研。雖然原理簡單卿操,不過它提供了系統(tǒng)方案,同時也統(tǒng)一了 Android 和 iOS 多端的調用方法孙援,如果是準備從0開始實現(xiàn)交互的話推薦使用害淤。