V8 引擎是由 Google 开源的 JavaScript 引擎,Chrome 就是基于 V8 开发,V8 是跨平台的,J2V8 基于 V8 进行开发,使得 js 代码能够在 Android 平台上脱离 WebView 运行。目前,也有很多关于 Android J2V8 的文章,不过讲解不是特别细(可能也是我太菜了,看完了之后,依然遇到很多问题),自己在调研的过程中遇到很多坑,所以这里记录一下,本文主要记录整个 J2V8 框架的使用方法,以及一些坑。

一、Webpack 打包

通常业务逻辑的 js 文件是有多个的,我们需要借助一些打包工具将多个文件打包成一个 js 文件供 J2V8 使用,我们可以使用 Gulp、Webpack、Browserify,本文主要讲 Webpack 的使用。
主要流程如下:

编写基础逻辑并通过 module.exports 对外部提供

编写 index.js 入口文件

1
2
3
4
5
...
module.exports = {
simpleFunc, complexFunc
};

** 编写webpack.config打包配置**

1
2
3
4
5
6
7
8
9
module.exports = {
entry: './src/example/index.js',
output: {
library: 'libExample', // j2v8 加载该lib
path: path.resolve(__dirname, 'dist'),
filename: 'example.js', // 导出指定命名的 js 文件
},
...
};

执行 webpack 打包命令

1
./node_modules/.bin/webpack --config webpack.config.js

二、运行 JavaScript

到这里我们已经有一份通过 Webpack 打包好的 js 文件了,要在 j2v8 中运行 JavaScript 文件,使用以下步骤:

1、创建一个 V8 实例

1
V8 v8 = V8.createV8Runtime();

2、读取 JavaScript 文件

1
var scriptStr = String(Files.readAllBytes(Paths.get("example.js")))

3、在 V8 实例中执行 JavaScript 代码

1
v8.executeScript(scriptStr);

这一步已经让整个 js 文件运行起来,但我们还不能调用我们的方法

4、读取指定模块

由于是通过 Webpack 打包,在 Webpack 的 output.library 配置,选项用于将打包后的代码作为一个库(library)暴露出去,以便其他应用程序或模块可以使用它。

1
val rootLib =v8.getObject(libName); // 这里的 libName 就是 output.library 配置的名字

如果是访问模块的导出对象中的子对象,那么继续:

1
val subLib =rootLib.getObject(subLibName); // 这里的 subLibName 是 index 文件中 module.exports 里面的模块名

如果子对象还有子对象,继续.getObject 即可

5、运行指定方法

接下来就简单了,直接通过如下方法执行 js 中的指定方法

1
2
3
4
5
public void executeVoidFunction(String name, V8Array parameters)
public String executeStringFunction(String name, V8Array parameters)
public double executeDoubleFunction(String name, V8Array parameters)
public int executeIntegerFunction(String name, V8Array parameters)
……

V8Object 提供了很多数据格式调用,不过都差不多,主要是在返回值那里帮你实现了数据的转化,如果不想用转化好的格式,希望自己来操作的话,使用public V8Object executeObjectFunction() 拿到返回值,自己去转化即可

6、释放资源

由于 V8 运行消耗较多的资源,执行结束的时候要将在过程中创建的所有的资源释放,避免导致内存泄漏。
V8提供了close方法,如果只使用 v8.close() 进行释放,或者未关闭过程中有用到 v8 runtime 的变量都会报如下错误,正确的做法是将所有资源进行关闭。

1
java.lang.IllegalStateException: 3 Object(s) still exist in runtime

三、进阶

通过以上的方式已经能执行很多逻辑了,但在实践过过程中发现:如何 js 的返回值是 Promise 的话不会等到最终的结果给我们,而是直接返回了一个 Promise 对象,以及看不到 console.log 打印的日志…… 诸如此类的问题需要解决,这里主要讲讲这两种方法的实现。

注册 Native 插件

J2V8 是一个基于 V8 引擎的 Java 库,它允许在 Java 中执行 JavaScript 代码。由于 J2V8 是在 Java 中运行的,它没有直接访问浏览器或控制台的能力,因此无法直接使用 console.log 函数来输出日志,总结 J2V8 不能实现以下功能:

  • 浏览器 API:j2v8 是在 Java 中运行的,因此无法直接访问浏览器 API,如 DOM、BOM 等。这意味着 j2v8 无法直接操作网页内容、处理事件等
  • 文件系统访问:j2v8 在 Java 中运行,无法直接访问文件系统。如果需要访问文件系统,需要使用 Java 提供的文件操作 API。
  • 定时器:JavaScript 中有多种定时器函数,如 setTimeout、setInterval 等,可以在指定时间后执行代码。但 j2v8 无法实现这些定时器函数,因为它无法直接访问系统的计时器。
  • Web Worker:Web Worker 是 JavaScript 中的一个特殊对象,可以在后台线程中执行代码,以避免阻塞主线程。但 j2v8 无法实现 Web Worker,因为它无法直接访问操作系统的线程。
  • Node.js API:j2v8 主要是为了在 Java 中执行浏览器端的 JavaScript 代码而设计的,因此无法直接访问 Node.js API。如果需要在 Java 中执行 Node.js 代码,可以考虑使用 Nashorn 等其他工具。

这里是 console.log的一个简单实现:

V8Object 是 J2V8 中的一个类,它代表了一个 JavaScript 对象,对于 console.log 我们可以将 console 看作一个对象,其有一个叫 log 的方法,要实现在 js 中打印日志到 Android Studio 控制台,如下即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ConsolePlugin {

fun log(message: Any) {
Log.d("ConsolePlugin", message.toString())
}

fun register(v8: V8) {
val v8Console = V8Object(v8)
// 第一个 log 表示 在 Java 中该方法的名字,第二个 log 表示在 JavaScript 中调用的名字
v8Console.registerJavaMethod(this, "log", "log", arrayOf<Class<*>>(Any::class.java))
v8Console.setWeak()
// 将含有叫"log"方法的一个对象加到运行环境中,该对象被命名为 "console"
v8.add("console", v8Console)
}
}

ConsolePlugin().register(v8)

具体代码可参考:J2V8_tutorial

执行返回值是 Promise 类型的方法

之前将的方法调用都是返回数据为基础类型,由于在 Java/kotlin 中没有Promise类型的方法,所以对于 Promise 方法我们需要进行一些特殊处理,我们通过使用 CountDownLatch 可以来实现一个 “异步变同步” 的操作,我们需要考虑的是如何接受到 resolve rejcet的调用,js 中 Promise 的方法使用如下:

1
2
3
4
5
6
PromiseMethod().then((result)=>{
// success got result
}).catch((e)=>{
// error...
});

在 J2V8中一样的实现

获取返回的 Promise 对象

1
val promiseObj = v8.executeFunction(functionName, v8Array) as V8Object

**执行 Promise 对象的 then 和 catch 方法 **

1
2
3
4
5
6
7
jsPromise.apply {
val onResolveParameter = V8Array(v8).push(onResolve)
val onRejectParameter = V8Array(v8).push(onReject)
executeVoidFunction("then", onResolveParameter)
executeVoidFunction("catch", onRejectParameter)
....
}

其中 onResolve

1
2
3
val onResolve = V8Function(jsRuntime) { receiver, parameters ->
       ……
}

具体代码可参考:J2V8_tutorial

四、总结

以上基本上能解决大部分 Android 调用 js的代码逻辑了,这里对整体执行的流程进行一个总结

1、通过 webpack 对多个 .js 文件打包
2、初始化 V8 环境并加载 .js 文件
3、注册 Java 方法,供 js 进行调用
4、读取指定的模板
5、执行目标 js 方法,并释放 v8 执行过程中产生的资源

踩过的一些坑

1、java.lang.UnsupportedOperationException: StartNodeJS Not Supported.

这个库有一个 NodeJS.createNodeJS()方法,以为是完美结合 NodeJs 的,查了下不太支持 Android,不过也有人提出解决方法:https://stackoverflow.com/questions/42574824/how-to-use-nodejs-in-android-using-j2v8

2、java.lang.IllegalStateException: 3 Object(s) still exist in runtime

这是调用 `v8.close`` 总是会遇到的问题,一定需要确保使用了 v8 Runtime 过程变量有被释放掉,可能有时候不知道具体哪个变量没有被释放

3、setTimeout、setInterval 无效

这是我最开始遇到的问题,简单想着“既然能执行js代码,那 setTimeout、setInterval 这些方法都是 js 最普通的方法应该没问题吧”,如果有一些平时在 js 很常见的操作如果无法执行,最好 check 一下 J2V8 是否支持

4、Undefined 相关

虽然源码里面通过了一个 Undefined 的类,但是不能直接使用,如果方法返回的 Undefined,通过 V8ObjectisUndefined() 去判断

引用

[1]J2V8 https://eclipsesource.com/blogs/tutorials/getting-started-with-j2v8/

[2] Registering Java Callbacks with J2V8 https://eclipsesource.com/blogs/2015/06/06/registering-java-callbacks-with-j2v8/

[3] Simple JS in Node.js https://yenhuang.gitbooks.io/android-development-note/content/wrap-js-library/simple-js-with-nodejs.html