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 入口文件

...
module.exports = {
  simpleFunc, complexFunc
};

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

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

执行 webpack 打包命令

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

二、运行 JavaScript

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

1、创建一个 V8 实例

V8 v8 = V8.createV8Runtime();

2、读取 JavaScript 文件

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

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

v8.executeScript(scriptStr);

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

4、读取指定模块

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

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

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

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

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

5、运行指定方法

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

    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 的变量都会报如下错误,正确的做法是将所有资源进行关闭。

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 控制台,如下即可:

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 的方法使用如下:

PromiseMethod().then((result)=>{
    // success got result
  }).catch((e)=>{
    // error...
  });

在 J2V8中一样的实现

获取返回的 Promise 对象

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

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

jsPromise.apply {
        val onResolveParameter = V8Array(v8).push(onResolve)
        val onRejectParameter = V8Array(v8).push(onReject)
        executeVoidFunction("then", onResolveParameter)
        executeVoidFunction("catch", onRejectParameter)
        ....
 }

其中 onResolve

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript中的数组方法有很多,常用的包括以下几种: 1. push():向数组末尾添加一个或多个元素,并返回新数组的长度。 2. pop():从数组末尾移除并返回最后一个元素。 3. shift():从数组开头移除并返回第一个元素。 4. unshift():在数组开头添加一个或多个元素,并返回新数组的长度。 5. concat():将两个或多个数组合并成一个新数组,并返回新数组。 6. slice():返回一个从指定开始索引到结束索引(不包括结束索引)的新数组,原数组不会被修改。 7. splice():从指定索引开始,删除或替换指定个数的元素,并返回被删除元素组成的数组。 8. join():将数组的所有元素连接成一个字符串,并返回字符串。 9. indexOf():返回指定元素在数组中第一次出现的索引,如果没有找到则返回-1。 10. lastIndexOf():返回指定元素在数组中最后一次出现的索引,如果没有找到则返回-1。 11. every():检测数组的所有元素是否都满足指定条件,如果所有元素都满足条件则返回true,否则返回false。 12. some():检测数组的某个元素是否满足指定条件,如果至少有一个元素满足条件则返回true,否则返回false。 13. filter():根据指定条件筛选数组中的元素,并返回筛选后的新数组。 14. map():根据指定条件对数组中的每个元素进行操作,并返回操作后的新数组。 15. reduce():通过指定的函数将数组元素合并为一个值。 16. forEach():对数组中的每个元素执行指定操作,没有返回值。 以上是JavaScript中一些常用的数组方法,可以根据实际需求选用相应方法操作数组。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值