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 Google
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 界面。

参考

写这些代码也许就一两个小时的事,写一篇大家好接受的文章需要几天的酝酿,如果文章对您有帮助请我喝杯咖啡吧!