前端 WebView 指南之 Android 交互篇

提醒:本文最后更新于 2467 天前,文中所描述的信息可能已发生改变,请谨慎使用。

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<String>() {
    @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('https://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('https://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方法客户端回调
alertonJsAlert
promptonJsPrompt
confirmonJsConfirm
console.logonConsoleMessage

将这四个方法列在一块是因为这几个方法的本质上都是差不多,定义好对应的回调方法即可。客户端具体的配置如下:

//定义好劫持回调类
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('https://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")');

这些调用方法有两点需要注意:

  1. 不管是前端调用还是客户端调用,所有的调用的结果返回都是异步的。客户端 loadUrl() 需要另外通过 JS 异步回调客户端方法告诉结果,evaluateJavascript() 也需要传如一个异步回调方法。前端调用中 addJavascriptInterface() 是无返回值的,而方法劫持中,需要等待客户端回调我们的 JS 方法才能异步获取到数据。所以我们需要对异步通信进行妥善处理。
  2. 由于 JS 和客户端无法实现内存共享,所以所有的数据必须字符串化,只能通过字符串进行交流。例如两边的复杂对象数据,需要使用类似 JSON 的格式进行字符串化,而文件/图片等二进制数据最好使用 base64 字符串化。

后记

基本上交互的基本方式就是以上几种,不过有人将通信机制进行了封装,形成一套完善的 WebviewJSBridge 方案,提供了客户端调前端,前端调用客户端的系统解决方案。例如 lzyzsd/JsBridge 项目,我们从代码中可以看到,其实它在底层是使用了 URL 劫持的方法与 JS 进行交互。虽然原理简单,不过它提供了系统方案,同时也统一了 Android 和 iOS 多端的调用方法,如果是准备从0开始实现交互的话推荐使用。

参考资料:

Avatar
怡红公子 擅长前端和 Node.js 服务端方向。热爱开源时常在 Github 上活跃,也是博客爱好者,喜欢将所学内容总结成文章分享给他人。

4 评论

Tim Zhao Chrome105.0 Mac OS 10.15.7
2022-10-01 23:22:16 回复

这篇文章写得真的太好了,我这个纯web前端都能够读懂吸收。
后悔!要是能早点看到这篇文笔这么好的解释jsbridge底层原理文章,也许我好多面试就不会挂掉了(哭
感谢!

怡红公子 Chrome105.0 Mac OS 10.15.7
2022-10-02 02:01:47 回复

@Tim Zhao: 感谢喜爱,能帮到你我就很开心啦如果有啥需要交流的欢迎随时评论

snadn Chrome63.0 Windows 10
2018-01-16 00:52:14 回复

话说这是火麒麟吗?

公子 Chrome63.0 Mac OS 10.13.2
2018-01-16 04:05:04 回复

@snadn哈哈哈,并不是,请看脚注“自豪地采用 Typecho”→_→