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 的,实现的方法有很多种,我把它归为两类:
Android WebView api 本身就支持的方式
addJavascriptInterface
;通过伪协议拦截页面的“请求”,即需要 JavaScript 与 Java 端(native)事先约定,方法有
shouldOverrideUrlLoading
、window.prompt
、Console.log
和alert
等,我们通常称这种方式为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接口:
prompt:默认显示一个对话框,对话框中包含一条文字信息,用来提示用户输入文字;
console.log:默认向web控制台输出一条消息;
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的功能如下:
利用上述的两种方式实现 JS 调用 Native 端的 toast 功能;
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及之后版本的普及,这未必不是一种好的方式;关于其他几种方法应该都是可以使用的,但都需要自己做一定封装;还有就是这几种方法的相关调用性能估计是不一样,大家在选择的时候需要做下对比,本文暂时没有涉及到。