Android 基于 J2V8 运行 JavasScript 实践
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 | ... |
** 编写webpack.config
打包配置**
1 | module.exports = { |
执行 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 | public void executeVoidFunction(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 | class ConsolePlugin { |
具体代码可参考:J2V8_tutorial
执行返回值是 Promise 类型的方法
之前将的方法调用都是返回数据为基础类型,由于在 Java/kotlin 中没有Promise
类型的方法,所以对于 Promise
方法我们需要进行一些特殊处理,我们通过使用 CountDownLatch
可以来实现一个 “异步变同步” 的操作,我们需要考虑的是如何接受到 resolve
rejcet
的调用,js 中 Promise 的方法使用如下:
1 | PromiseMethod().then((result)=>{ |
在 J2V8中一样的实现
获取返回的 Promise 对象
1 | val promiseObj = v8.executeFunction(functionName, v8Array) as V8Object |
**执行 Promise 对象的 then 和 catch 方法 **
1 | jsPromise.apply { |
其中 onResolve
1 | 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,通过 V8Object
的 isUndefined()
去判断
引用
[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