Android JavaScript 引擎学习之初探 V8
前言
之前一直都只是听说 V8 执行效率高,了解 Node 是运行在 V8 引擎上的,weex 在 Android 上也是使用 V8 引擎来执行 JS 的,但是对于 V8 的认识还是比较肤浅的层次。开始学习一下 V8 相关的内容,学习过程记录一下,利人利己。本系列文章可能更关注 V8 在 Android 上的应用,以及那些使用 V8 的框架到底做了一些什么工作。
JavaScript 引擎
目前在 Android/iOS 上运行 JavaScript,主要有两种方法:一种方法是利用系统的浏览器组件 WebView(Android)和 UIWebView/WKWebView(iOS);另一种方法是编译和集成一个功能全面的 JavaScript 引擎。
在 App 开发中经常会使用 WebView 加载一个网页,甚至构建一个完整应用,目前相关的框架和工具也比较多,例如使用 Cordova 构建多平台应用。当我们对于性能和体验要求比较高的时候,WebView 中解析 JS 的效率低(取决于 JavaScript 引擎)、DOM 渲染慢(取决于渲染引擎)以及系统碎片化严重对于 API 支持也不尽相同(取决于浏览器内核)往往影响性能瓶颈和开发体验。
目前基于高性能 JavaScript 引擎的方案也有很多,如 React Native、weex、NativeScript、Tabrisjs 等,每个方案之间都有一些不一样的地方,不过都离不开底层 JavaScript 引擎的支持。要想了解这些上层框架的原理,我们首先需要了解 JavaScript 引擎的基本内容。
JavaScript 引擎是一个专门处理 JavaScript 脚本的虚拟机,一般会附带在网页浏览器之中。在 Android 和 iOS 中可以运行的最主要的 JavaScript 引擎有 JavaScriptCore、V8、SpiderMonkey、Rhino,下面的表格列出他们在 iOS 和 Android 的兼容性。
JavaScript Engine | Android | iOS | 维护 |
---|---|---|---|
JavaScriptCore | Interpreter and JIT | Interpreter only | Apple |
V8 | JIT | JIT only for jailbroken devices | |
SpiderMonkey | Interpreter and JIT | Interpreter only | Mozilla |
Rhino | Interpreter | Unsupported | Mozilla |
JavaScriptCore 是一个在 WebKit 中提供 JS 引擎的开源框架,目前该引擎由 Apple 维护,使用于 Safari 浏览器,iOS7 后也集成到了 iPhone 平台。由于其使用 C 语言编写,因此在 Android 开发中并不能直接使用。Github 上的开源项目 AndroidJSCore(目前已经停止维护了,官方仓库指向LiquidCore) 能够帮助开发者经过调用 Java 接口而使用 JavaScriptCore。
V8 是由 Google 开发并维护的高性能开源 JS 引擎,采用 C++编写,使用于 Google Chrome 浏览器。同 JavaScriptCore 一样,在 Android 开发中,相关接口需要通过一层包装进行调用。Github 上的开源项目 J2V8 基于 jni 实现了 Java 对 V8 的封装,下文我们重点介绍一下。
SpiderMonkey 最初由 Netscape 开发,如今由 Mozilla 开发并维护,且被广泛用于 Mozilla 产品(如 FireFox)。SpiderMonkey 提供了一些核心的 JavaScript 数据类型,如数字,字符串,数组,对象等等,以及一些方法如 Array.push。它还使得每个应用程序都容易将其自己的对象和方法暴露给 JavaScript 代码。应用开发者可以决定应用如何将与所写脚本相关的对象暴露出来。
Rhino 是由 Mozilla 开发的开源 JS 引擎,采用 Java 编写,因此可以直接调用,在 JDK 6、JDK 7 中更是捆绑了该引擎,其提供的特性包括:
- JavaScript 1.7 的全部特性
- 可以用脚本方式调用 Java
- 用一个 JavaScript Shell 来执行 JavaScript 脚本
- 用一个 JavaScript 编译器来将 JavaScript 脚本文件转换成 Java 类文件
- 用一个 JavaScript 调试器来调试 Rhino 执行的脚本
Nashorn 由 Oracle 开发并维护,从 JDK 8 开始,Rhino 被 Nashorn 代替,成为 JDK 默认 JS 引擎。Nashorn 同 JDK8 一同发布和开源,较 Rhino 而言性能更好,但不支持 Android Dalvik 虚拟机。
结合在 Android 上使用 JS 引擎是一种什么样的体验?一文中的实验,可以得出结论:单从 JS 引擎来说,Rhino 执行不需要通过 JNI 且占用更少的内存,但执行效率很低;V8 和 JavaScriptCore 等 C 语言开发的引擎远胜于 Rhino 等 Java 开发的引擎,但需要一层 Java 包装层,并存在 JNI 调用性能问题。就 J2V8 和 AndroidJSCore 两个包装层而言:J2V8 的可用性、可靠性、健壮性更优;AndroidJSCore 还存在着不少的性能问题,在上述实验中出现较少,但实际开发中还存在很多坑。以上三种实现方案中更推荐 J2V8 方案。对于内存问题,J2V8 的内存释放机制较为完善,在实际开发中可以通过主动 release 来释放内存;对于 JNI 调用性能问题,J2V8 团队也在尝试通过批处理回调来进行优化,在将来的版本中会得到改善。
J2V8 框架初探
J2V8 是一套针对 V8 的 Java 绑定。J2V8 的开发为 Android 平台带来了高效的 Javascript 的执行环境,其以性能与内存消耗为设计目标。它采用了“基本类型优先”原则,意味着一个执行结果是基本类型,那么所返回的值也就是该基本类型。它还采用了“懒加载”技术,只有当 JS 执行结果被访问时,才会通过 JNI 复制到 Java 中。此外 J2V8 提供了 release()方法,开发者在某一对象不再需要时主动调用该方法去释放本地对象句柄,释放规则如下:
- 如果是由代码创建的对象,那么必须释放它;如果一个对象是通过返回语句传回来的话,系统会替你释放它;
- 如果是由系统创建的对象,则无需担心释放,而如果对象是 JS 方法的返回值,就必须手动的释放它。
- 官网地址:https://eclipsesource.com/blogs/tutorials/getting-started-with-j2v8/
- github 地址:https://github.com/eclipsesource/J2V8
- maven 仓库地址:http://central.maven.org/maven2/com/eclipsesource/j2v8
在 Android 中使用 J2V8 也很简单,我们从 maven 仓库上下载.arr 文件,然后放在工程目录下的 lib 文件夹中,修改 app 下 build.gradle 文件,添加如下内容:
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
...
compile(name: 'j2v8-4.8.0', ext:'aar')
}
然后就可以在我们的 Java 代码中通过 API 运行 Js 代码了,同样可以用 Js 调用 Java 中注册的方法。
先写一个 Hello World 测试一下我们的 j2v8 集成是否正常。
// 创建JS上下文
V8 runtime = V8.createV8Runtime();
// 执行js代码
int result = runtime.executeIntegerScript(""
+ "var hello = 'hello, ';\n"
+ "var world = 'world!';\n"
+ "hello.concat(world).length;\n");
// 打印返回值
Log.i(TAG, Integer.toString(result));
// 释放对象
runtime.release();
接着我们定义一个自定义方法注册到 JS 上下文。
// 创建Java方法,并注册到JS上下文
JavaVoidCallback callback = new JavaVoidCallback() {
public void invoke(final V8Object receiver, final V8Array parameters) {
Toast.makeText(getApplicationContext(), parameters.getString(0), Toast.LENGTH_SHORT).show();
}
};
runtime.registerJavaMethod(callback, "showToast");
自定义方法需要在使用前进行注册,然后我们可以通过executeVoidScript
执行代码
runtime.executeVoidScript("showToast('"+result+"')");
这些代码都是写在 Java 中,在实际应用中我们一般都会将 js 写.js
文件中。这里我们定义一个工具类:
import android.content.Context;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileUtil {
public static String getFromAssets(Context context, String fileName){
InputStream input = null;
try {
input = context.getAssets().open(fileName);
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
output.close();
input.close();
return output.toString();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
然后我们就可以这样执行 js:
String script = FileUtil.getFromAssets(this, "www/index.js");
runtime.executeScript(script);
React Native 初探
RN 会把应用的 JS 代码(包括依赖的 framework)编译成一个 js 文件(一般命名为 index.android.bundle), , RN 的整体框架目标就是为了解释运行这个 js 脚本文件,如果是 js 扩展的 API, 则直接通过 bridge 调用 native 方法; 如果是 UI 界面, 则映射到 virtual DOM 这个虚拟的 JS 数据结构中,通过 bridge 传递到 native , 然后根据数据属性设置各个对应的真实 native 的 View。 bridge 是一种 JS 和 Java 代码通信的机制, 用 bridge 函数传入对方 module 和 method 即可得到异步回调的结果。
对于 JS 开发者来说, 画 UI 只需要画到 virtual DOM 中,不需要特别关心具体的平台, 还是原来的单线程开发,还是原来 HTML 组装 UI(JSX),还是原来的样式模型(部分兼容 )。RN 的界面处理除了实现 View 增删改查的接口之外,还自定义一套样式表达 CSSLayout,这套 CSSLayout 也是跨平台实现。 RN 拥有画 UI 的跨平台能力,主要是加入 Virtual DOM 编程模型,该方法一方面可以照顾到 JS 开发者在 html DOM 的部分传承, 让 JS 开发者可以用类似 DOM 编程模型就可以开发原生 APP , 另一方面则可以让 Virtual DOM 适配实现到各个平台,实现跨平台的能力,并且为未来增加更多的想象空间, 比如 react-canvas, react-openGL, 而实际上 react-native 也是从 react-js 演变而来。
对于 Android 开发者来说, RN 是一个普通的安卓程序加上一堆事件响应, 事件来源主要是 JS 的命令。主要有二个线程,UI main thread, JS thread。 UI thread 创建一个 APP 的事件循环后,就挂在 looper 等待事件 , 事件驱动各自的对象执行命令。 JS thread 运行的脚本相当于底层数据采集器, 不断上传数据,转化成 UI 事件, 通过 bridge 转发到 UI thread, 从而改变真实的 View。 后面再深一层发现, UI main thread 跟 JS thread 更像是 CS 模型,JS thread 更像服务端, UI main thread 是客户端, UI main thread 不断询问 JS thread 并且请求数据,如果数据有变,则更新 UI 界面。
参考
- 在 Android 上使用 JS 引擎是一种什么样的体验?
- Comparison Shopping: Searching for Javascript Engines for Android
- Canvas 绘制 JustDraw
写这些代码也许就一两个小时的事,写一篇大家好接受的文章需要几天的酝酿,如果文章对您有帮助请我喝杯咖啡吧!