Android中Java与JavaScript交互的几种方式

0x00 背景

近年来,由于开发成本,开发效率,用户需求等原因,对于移动 App 的开发方案已经从原生开发趋向于混合(Hybrid)开发的方式,甚至于说直接基于一些大的 App 平台提供的 JS SD K直接开发 Web 页面,例如微信、手机QQ等超级 App。最近,就在我写这篇文章的时候,在微信公开课 Pro 活动上,张小龙提出了微信关于“应用号”的规划,具体请看这篇文章预埋两年的线索,传说会干掉 App 的微信应用号是什么?,可见混合开发这种开发方式的重要性。

基于混合开发方式的优势是非常明显的:它既能使用原生的一些手机特性,而且又拥有随时发布的能力。前者是通过提供相关 JS API 使 Web 页面具有一些原生的功能,而后者是 Web 页面天生具有的特性。也就是说:我们在开发原生应用的基础上嵌入 WebView,但是整体的架构使用原生应用提供。关于这种开发方式,如果你想要更进一步了解,请参考这篇《Hybrid App 开发实战》。

而本文的目的就是想要知道这些 JS API 是如何实现的,或者更直白一点:Android 中 Java 与 JavaScript 是怎么交互/通信的?你要知道 JavaScript 是运行在浏览器环境下的脚本语言。当然,网上关于这方面的资料非常多,但是我这里还是想总结与实践一下,因为之前的项目需求开发接触 Hybrid 开发这种方式,在 Android 上写了一些 JS API,后来又接触了前端开发,开始使用这些 JS API,所以很想了解一下其中的相关原理。

0x01 两类交互方式

在进入主题之前,还需要提到一点:本文主要是涉及 JavaScript 如何调用 Java?而反过来,Java 调用 JavaScript 因为比较简单一点,我这里稍微提下,直接上代码:


String url = "javascript:" + methodName + "(" + jsonParams + ");void(0);"

webView.loadUrl(url);

就是这么简单,直接调用 WebView 的loadUrl(url)方法,当然参数 url 是比较特殊,前面的javascript:伪协议让我们可以通过一个链接来调用 JavaScript 函数,中间methodName是 JavaScript 中实现的函数,jsonParams是传入的参数。关于后面的void(0);,可以参考这篇文档《void operator》中的说明。Java 调用 JavaScript 的方式就说到这里,下面我们继续讨论 JavaScript 是如何调用 Java 的,实现的方法有很多种,我把它归为两类:

  1. Android WebView api 本身就支持的方式addJavascriptInterface

  2. 通过伪协议拦截页面的“请求”,即需要 JavaScript 与 Java 端(native)事先约定,方法有shouldOverrideUrlLoadingwindow.promptConsole.logalert等,我们通常称这种方式为JsBridge

addJavascriptInterface

首先,我们来看第一类addJavascriptInterface,其方法声明如下所示:


public void addJavascriptInterface(Object object, String name)

该方法将参数中提供的 Java 对象(object)注入到 WebView 中。该对象会被注入到页面主框架(main frame)的 Javascript 上下文中,通过参数中提供的名称(name)访问。具体的使用方式,Android 官方文档有给出:


class JsObject {
    @JavascriptInterface

    public String toString() {

        return "injectedObject";

    }
}

webView.addJavascriptInterface(new JsObject(), "injectedObject"); // 只有页面再加载,该对象才可见

webView.loadData("", "text/html", null);

webView.loadUrl("javascript:alert(injectedObject.toString())");

这个例子大家一看就很明了,addJavascriptInterface这种方式非常简单好用。但是这种方式在 Android 4.2之前的版本中存在安全问题:在4.2之前被注入的对象的所有公共方法(包括从父类继承过来的方法)都可以被访问到;在4.2以后,只有通过@JavascriptInterface注解的公共方法才能被访问。具体请看这篇WebView中接口隐患与手机挂马利用

除此之外,对于该方法还需要注意的是:

  • 在该方式下,JavaScript 调用 Java 通过 WebView 的一个私有后台线程,所以,需要我们需要注意线程安全;

  • Java对象的域是不可访问的;

  • 在 Android 5.0及以上,被注入对象的方法可被 JavaScript 枚举。

下面,我们来看第二类方法,这类方法的特点是:JS 端与 Native 端存在一个伪协议,Native 端口根据这个协议去侦听/截获页面的相关行为。所以,我们首先需要定义一个协议(可参考上面的javascript:伪协议):协议名+方法名+相关参数。在本文中,我们假定该协议格式为:"jsbridge://" + "method" + "jsonParams",整个协议就是个特殊的字符串。之后我们要做的工作是把这个字符串从 JS 端传到 Native 端,然后 Native 去解析这个字符串并执行相关代码。这其中的关键就是如何传这个字符串,方法有很多,我们一个一个来看:

shouldOverrideUrlLoading

shouldOverrideUrlLoading是类WebViewClient中的一个方法。它的作用是控制当前 Webview 加载新 url 的相关行为。在默认情况下,Webview 没有设置 WebViewClient,所以它会请求 Activity Manager 来处理该 url (一般就是调用相关浏览器应用)。该方法的方法声明如下:


public boolean shouldOverrideUrlLoading(Webview view, String url)

从方法声明可知,我们将通过参数String url来传递我们协议字符串,所以在 Native 端我们创建设置 WebViewClient 子类,该子类覆写shouldOverrideUrlLoading方法,这个就可以拦截 Webview 加载新 url 了。那么在 JS 端该如何生成这个 url 呢?一般我们可以创建一个 iframe,设置它的 src 属性,并将其添加到页面的文档流中,或者直接设置window.location.href。相关代码如下:


// 方式(1) 直接设置window.location.href

window.location.href = "jsbridge://toast?{msg:jstojava}";

// 方式(2) 在需要js调用native api的时候,js在页面中创建一个不可见的iframe,设置这个iframe的地址

var iframe = document.createElement("iframe");

iframe.style.display = "none";

document.documentElement.appendChild(iframe);

iframe.src = "jsbridge://toast?{msg:jstojava}";

prompt,console.log,alert

这部分我们要讲的三个方法(原理同上),都是浏览器实现的API接口:

  1. prompt:默认显示一个对话框,对话框中包含一条文字信息,用来提示用户输入文字;

  2. console.log:默认向web控制台输出一条消息;

  3. alert:默认用于显示带有一条指定消息和一个 OK 按钮的警告框。

对于上述三个方法的默认行为,大家可通过 chrome 的开发者工具试试,调用方式非常简单。所以,我们只要能拦截这三个方法的默认行为并获得其中的参数即可。而 Android 中的类WebChromeClient确实存在相对应的方法来处理,只要覆写 WebChromeClient 中相对应的三个方法,并设置 Webview。下面是这三个方法的方法声明,要注意的是这三个方法的参数差异是有点大,具体使用那个参数可能需要与 JS 端配合:


class WebChromeClientImp extends WebChromeClient {

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {

    }

    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

    }

    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

    }

}

0x02 相关代码实现

在上面一节主要介绍了 JS 与 Java 互相调用的方法,在这一节主要是如何这些方法。虽然以上这些还是很简单的,但写个 demo 实践一下还是必要的。这个 demo 已上传至 github ,有兴趣的同学,请点这里 jsbridgeDemo。这个demo的功能如下:

  1. 利用上述的两种方式实现 JS 调用 Native 端的 toast 功能;

  2. Native 端调用 JS 端的方法实现修改页面背景色的功能。

在这个 demo 实现比较简单,我这里就稍微说明几点:

第一,这个 demo 项目需要一个页面来承载,这个页面可以发布在外网上或者它就写在本地项目中。本文使用了后面这种方式,因为比较方便,具体操作方式是:在 Android 项目的根目录下创建assets目录(如果该目录不存在的话),并创建页面jsdemo.html在该目录下,代码中加载页面的方式为webView.loadUrl("file:///android_asset/jsdemo.html")

第二,JS 端调用prompt()alert()后,Native 端必须给 JS 端回调确认,否则会有问题,因为两者都是会弹框,需要给响应:


result.confirm();

第三,在 Native 代码需要设置 Webview 启用WJavaScript:


WebSettings webSettings = webView.getSettings();

webSettings.setJavaScriptEnabled(true);

第四,目前实现只是对伪协议做了字符串比较,最好的方式当然是在 JS 和 Native 端各自封装相对模块来处理相关逻辑,后续有时间我会做下修改。

0x03 总结

本文主要介绍了 Java 与 JavaScript 相互调用的方式,特别是 JavaScript 调用 Java 的几种方法:当然,Android 原生提供的方式由于安全的问题是不被推荐的,但是随着 Android 4.2及之后版本的普及,这未必不是一种好的方式;关于其他几种方法应该都是可以使用的,但都需要自己做一定封装;还有就是这几种方法的相关调用性能估计是不一样,大家在选择的时候需要做下对比,本文暂时没有涉及到。

0x04 参考文章

  1. 在WebView中如何让JS与Java安全地互相调用
  2. android webview
  3. Load local HTML file into WebView
  4. WebView中实现js与java互相调用
  5. Building Web Apps in WebView
  6. 微信的jsbridge实现
  7. 让Java跟Javascript更加亲密

你可能感兴趣的:(Android中Java与JavaScript交互的几种方式)