转自:https://imnerd.org/android-webview-and-js.html
WebView 是移动端应用中的一个控件,提供了类似浏览器可以在 App 中加载网页的功能。现在市面上很多应用都会使用这种方式内嵌一些 h5 页面用来实现产品功能。使用这种方式带来的好处就是支持快速迭代更新,并且页面的功能是全网升级。当然目前 RN 和 Codorva 给我们带来的热更新方案也是可以的,只是目前 Apple 的态度很拒绝,这里我们略过不表。在 WebView 中的网页势必存在和客户端进行交互的动作,进行数据的共享。下面我们就来说说 Android WebView 中 JS 和 Native 的交互方式。
客户端调用 JS
loadUrl()
我们明白 WebView 其实就是在加载网页,所以客户端可以直接访问 javascript:console.log('hello') 这样的伪 URL 即可实现在页面注入需要执行的 JS 代码。调用方法如下:
WebView webview = (WebView) findViewById(R.id.webView);
webview.loadUrl("javascript:console.log('hello')");
这样我们就实现了调用 JS 的目的了。loadUrl() 的方案从另外一个角度来看可以算是 hack 方案了,对客户端来说,他们的 JS 交互本质上其实就是一个拼接 JS 字符串的过程。
evaluateJavascript()
刚才我们也说了 loadUrl() 不是 Android 的正经解决方法。好在官方也想到了这点,在 Android 4.4+ 之后,官方给提供了原生的方法支持调用,那就是 evaluateJavascript()。这个方法最大的好处就是能够直接在一次执行的时候获取到 JS 返回的结果。如果是使用 loadUrl() 的方式的话,执行完后对客户端来说这句话就结束了,如果想要拿到返回的结果的话另外需要 JS 调用客户端的方法返回。
WebView webview = (WebView) findViewById(R.id.webView);
webview.evaluateJavascript("javascript:Date.now()", new ValueCallback
@Override
public void onReceiveValue(String value) {
System.out.println(value); //1515827651551
}
});
可以看到调用方法和 loadUrl() 非常类似,区别是增加了一个 callback 方法可以获取到 JS 返回的值。该方法无疑比较优秀,不过对兼容性有要求,目前市面上用还是使用前一种方法的比较多。
JS 调用客户端
相比较客户端调用 JS 的方法,JS 调用客户端的方法就比较多了,简单归类一下其实可以分为注入映射和方法劫持两种。注入映射主要是使用官方提供的 addJavascriptInterface() 方法将 Java 对象和 JS 对象进行映射。而方法劫持则是利用 JS 的一些系统方法调用会触发 Java 的事件回调,然后在回调中进行事件劫持,从而执行客户端方法。下面我们来具体看看。
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'); //注入后加载页面
这样加载的页面中就可以直接执行 sdk.hello() 方法来执行客户端方法了。不过这种官方推荐的方法在 4.2- 的系统上存在远程执行安全漏洞,对 4.2 以下系统版本有要求的应用需要谨慎使用。目前来看 4.2 还是需要保持支持的。
URL劫持
URL劫持主要是使用 shouldOverrideUrlLoading() 进行 WebView URL 劫持。从方法名可以看出,它是 WebView 拦截 URL 的一种回调,当 WebView 发生 URL 跳转的时候会触发该回调。在该回调中我们能够获取到前端提供的 URL 地址。我们通过构造约定协议的 URL 地址提供给客户端识别,识别成功后执行对应的方法即可。
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 的一些方法执行时会触发 Android 客户端中的一些回调,通过对前端参数进行识别来执行对应的客户端代码。目前前端主要有以下四种方法会触发对应的回调方法,对应关系如下:
| 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 对应的回调写了两种,三参数的是老版本方法,在新API中已经被废弃,推荐使用 ConsoleMessage 对象传参方式。
总结
以上讲述了 JS 调用客户端的方法,以及客户端调用前端的方法。除了这两种单向调用的方式之外,往往比较多的是 JS 调用客户端方法,客户端再调用 JS 返回结果的双向调用。在 JS 调用的时候需要传入一个回调方法名,然后客户端直接执行回调方法。这样就完成了一个完成的信息交流的过程。
window.hello = function(text) {
console.log(text);
};
console.log('$hello:{"callback": "hello"}');
webview.loadUrl('javascript:hello("hello world")');
这些调用方法有两点需要注意:
不管是前端调用还是客户端调用,所有的调用的结果返回都是异步的。客户端 loadUrl() 需要另外通过 JS 异步回调客户端方法告诉结果,evaluateJavascript() 也需要传如一个异步回调方法。前端调用中 addJavascriptInterface() 是无返回值的,而方法劫持中,需要等待客户端回调我们的 JS 方法才能异步获取到数据。所以我们需要对异步通信进行妥善处理。
由于 JS 和客户端无法实现内存共享,所以所有的数据必须字符串化,只能通过字符串进行交流。例如两边的复杂对象数据,需要使用类似 JSON 的格式进行字符串化,而文件/图片等二进制数据最好使用 base64 字符串化。
后记
基本上交互的基本方式就是以上几种,不过有人将通信机制进行了封装,形成一套完善的 WebviewJSBridge 方案,提供了客户端调前端,前端调用客户端的系统解决方案。例如 lzyzsd/JsBridge 项目,我们从代码中可以看到,其实它在底层是使用了 URL 劫持的方法与 JS 进行交互。虽然原理简单,不过它提供了系统方案,同时也统一了 Android 和 iOS 多端的调用方法,如果是准备从0开始实现交互的话推荐使用。