一个冷门库J2V8的赋能之旅——深度绑定机制的实现

不知道各位大神在工作中有没有用到过J2V8,简单介绍一下J2V8,这是谷歌开源的大名鼎鼎的JS执行引擎V8的java封装,旨在将V8引擎引入到java的开发项目中,这样就可以在java项目中执行js代码了。 为什么要使用这个库呢?因为我之前的工作是做SDK,这个SDK的一个核心功能就是作为JS的容器,将JS的代码在移动端跑起来,当然,WebView本身就可以执行JS代码,J2V8是作为高性能的替代引入的,目的是在运行JS游戏时更高效。

该文章改自我的博客文章J2V8深度绑定机制分享

项目源码链接:v8x

引入

groovy 复制代码
// 核心库(必选)
implementation 'com.eclipsesource.j2v8:j2v8:6.2.0@aar'

// 按需选择 CPU 架构支持(以下为常见架构)
implementation 'com.eclipsesource.j2v8:j2v8-4.8.0-android-arm64:6.2.0@aar'  // 64位 ARM
implementation 'com.eclipsesource.j2v8:j2v8-4.8.0-android-armeabi-v7a:6.2.0@aar'  // 32位 ARM

基本使用

kotlin 复制代码
val v8 = V8.createV8Runtime()
val canvas = V8Object(v8);
canvas.add("getContext", V8Function(v8) { _, _ ->
    createContextObject(v8, androidCanvas)
})
v8.add("canvas", canvas)
v8.executeVoidScript("这里是js代码")

问题背景

做JS小游戏的容器时,我们的架构是这样的,由Android的原声控件(主要是SurfaceView)作为画面的渲染,然后封装一个context(这里指的是web canvas的context,而非Android原生的Context类)的java对象,将这个对象传递到J2V8的JS运行环境中。这样做,就是为了能将JS的执行与游戏画面的渲染,都能达到一个最高效的状态。 但是同样也有问题,就是双层(JS层/Java层)的对象中变量不一致的问题,比如通过J2V8执行下面js代码。

javascript 复制代码
const ctx = canvas.getContext('2d'); // 这里的ctx是Java层返回的V8Object对象
ctx.lineWidth = 10;
...
ctx.stroke();

JS层的ctx实际上是有一个对应的Java对象的,在js代码中,为ctx赋值了ctx.lineWidth = 10;,然后再执行ctx.stroke()方法,同样的,stroke方法也是java层对象执行,但是问题是,ctx.lineWidth = 10这句代码,java层的对象并不能感知到。导致绘制时,就有问题。

旧的方案

旧的方案是,让写js框架的同学,写一个代理,在遇到ctx.lineWidth = 10这类代码时,执行一个ctx中约定的方法,将新值以及变量名都通知给java端,但是这样做就有问题了:

  1. 有这种需求的类可能很多,每一个都要JS层做相应的适配,这会增加工作量和调试沟通成本;
  2. 并不是所有属性的变化都是Java层关心的;
  3. JS层并不知道Java层的属性与类的关系,容易错乱,导致通知了属性变化,却不知道是哪个对象的属性变化了;
  4. 后期Java层类做了变动,需要JS层做相应修改,一旦遗漏,就会产生bug;
  5. js框架需要尽可能在多端保持一致,不可能单独为安卓写这么多定制代码;

所以,卑微的Android狗就只能土法炼钢、曲线救国搞出这个J2V8的深度绑定机制了。

二、J2V8Binding

J2V8Binding的目标是,将JS层属性的变更感知,全部控制在Java层内,无需JS层参与额外代码。

2.1 基本原理

与旧方案类似的,我们仍然需要一个JS层的代理,而与旧方案不同的,新方案的代理是由Java层"生成"。

kotlin 复制代码
private const val CREATE_PROXY_JS = """
function v8CreateProxy(obj) {
    return new Proxy(obj, {
        set: function(target, key, value) {
            // 在此拦截需要的值变化,并发送给Java层
        }
    })
}
"""
// ... 初始化V8时 ...
v8.executeScript(CREATE_PROXY_JS)

这里我们声明一段JS代码,这里用于创建JS层的代理对象。在初始化V8时,将此段JS代码注入到V8环境中,当我们需要一个JS层代理对象时,就可以执行此JS函数v8CreateProxy创建一个JS层的代理对象。

kotlin 复制代码
private fun createJSProxy(v8obj: V8Object): V8Object {
    return v8.executeObjectFunction("v8CreateProxy", V8Array(v8).apply { push(v8obj) })
}

2.2 封装

对以上基本逻辑进行封装,有关键类V8BindingV8ManagerV8FieldV8Method

2.2.1 V8Binding

一个需要绑定的类,需要实现V8Binding接口。

kotlin 复制代码
interface V8Binding {
    companion object {
        private const val TAG = "V8Binding"
    }
    // V8Binding类的唯一id,用于将JS层对象与Java层对象进行一一对应
    // 默认实现为该对象的hashCodeo().toString()
    fun getBindingId(): String {  return hashCode().toString() }
    // 获取或者创建一个与其对应的代理V8Object
    fun getMyBinding(v8: V8): V8Object {
        return V8Manager.obtain(v8).run {
            if (!isBound(getBindingId())) {
                createBinding(this@V8Binding)
            } else {
                getBinding(getBindingId())
            }
        }
    }
    // 没有绑定,但是仍然关心的属性,获取关心的属性名称
    fun getCareForFieldKeys(): Array<String> { return emptyArray() }
    // 关心的属性值变更时
    fun onCareForFieldChanged(key: String, newValue: Any?, oldValue: Any?) {}

    // 绑定的属性值由null变为非null值时触发,只针对非基本数据类型(数值与String)的属性触发
    fun onBindingCreated(target: V8Object, fieldInfo: Key, value: V8Object): V8Binding {
        throw NotImplementedError("onCreateBinding must be implement when new binding created")
    }
    // 绑定的属性值由非null变为null值时触发,只针对非基本数据类型(数值与String)的属性触发
    fun onBindingDestroyed(target: V8Object, fieldInfo: Key) {
        throw NotImplementedError("onBindingDestroyed must be implement when binding destroyed")
    }
    // 绑定的属性值发生变化时触发
    fun onBindingChanged(target: V8Object, fieldInfo: Key, newValue: Any?, oldValue: Any?)
}

getMyBinding方法中,我们使用V8Manager来创建或者获取一个与当前对象绑定的V8Object对象。

2.2.2 V8Manager

V8Manager是一个针对某个V8环境的管理类,主要是用于维护JS层对象与Java层的绑定关系,执行创建绑定关系、删除绑定关系,分发属性变更事件等作用,J2V8Binding的主要核心逻辑的所在。

由于此处涉及到大量代码细节,故不在此罗列代码具体分析。

2.2.3 V8Field和V8Method

这两个类是注解类,用于标记类内属性和方法。

@V8Field(binding=?): 标记属性,其中有一个binding属性,标记此属性是否需要绑定,如果需要绑定,则会有事件回调,默认值为false。

@V8Method(jsFuncName=?): 标记方法,其中有一个jsFuncName属性,用于标识对应的JS函数的名称。

2.3 基本使用方法

一个简单的示例类如下:

kotlin 复制代码
class User : V8Binding {
    // 不需要绑定的属性,值会传递到对应的JS对象,但该值在JS层发生变化,Java层不会知道
    @V8Field
    val name = "John"
    // 需要绑定的属性,初始值会传递到JS对象,该值在JS层发生变化,会传递到Java层
    @V8Field(binding = true)
    var age = 15

    // 需要绑定的V8Binding类型属性,与之对应的V8Object会传递到JS对象,
    // 在JS层,如果变更为其他JS层内部的对象,会解绑之前的关系,与新的JS对象建立新的绑定关系
    @V8Binding(binding = true)
    val location: V8Binding = ...

    // 在此方法中
    override fun onBindingChanged(target: V8Object, fieldInfo: Key, newValue: Any?, oldValue: Any?){
        when(fieldInfo.name) {
            "age" -> {}
        }
    }

    @V8Method(jsFuncName = "js层对应的名称,默认值与当前方法名一致")
    fun sayHello(helloTo: String) {
    }

    fun getCareForFieldKeys(): Array<String> { return arrayOf("introduction") }

    fun onCareForFieldChanged(key: String, newValue: Any?, oldValue: Any?) {
        when(key) {
            "introduction" -> {}
        }
    }

}

当使用这个User类的一个对象user时候,可以按照如下方式使用。

kotlin 复制代码
val v8 = V8.createRuntime()
val user = ...
v8.add("user", user.getMyBinding(v8))

这里,我们在JS环境中,增加了一个user变量。

然后在JS层,执行以下代码。

javascript 复制代码
user.name = "Smith";    // Java层不会感知到
user.age = 16;        // Java层可以在onBindingChanged感知到
user.location = {};    // Java层可以在onBindingChanged感知到,并解绑旧location值,与新值建立绑定关系
user.introduction = "Hi";    // Java层可以在onCareForFieldChanged感知到

在修改name属性时,由于此属性是被V8Field标记binding为false,则不会通知Java层此值的修改事件。

在修改age属性时,由于此值被V8Field标记binding为true,会通知Java层此值的修改事件。

在修改location属性时,虽然此值为一个对象,但是被V8Field标记binding为true,会通知Java层此值的修改事件。注意:用V8Field标记的对象类型,必须为V8可以接受的数据类型或者V8Binding类型。

在修改introduction时,虽然此值没有被V8Field标记,但是由于在getCareForFieldKeys返回的数组中,同样会有事件通知。

有了这种机制,小游戏开发中,就可以很方便的感知到绘制属性的变化。

2.4 仍然存在的问题

目前仍然有一种问题是无法解决的,那就是数组中某个数据发生变化时,Java层是无法得知的。

如下代码:

javascript 复制代码
const image = ...;
image.pixelBytes[0] = 0; // 修改R值
image.pixelBytes[1] = 0; // 修改G值
image.pixelBytes[2] = 0; // 修改B值
image.pixelBytes[3] = 0; // 修改A值

目前这种场景较少,只在部分demo中见到直接修改图像像素数据的情况。

三、总结

以上代码片段只为展示逻辑主脉络,省略了大量细节,需要看细节部分,可以从V8Manager 这里作为入口。源码链接:v8x

相关推荐
帅次11 分钟前
Flutter DropdownButton 详解
android·flutter·ios·kotlin·gradle·webview
际宇人16 分钟前
移动端APP阿里云验证码2.0接入实录
android
日月星辰Ace23 分钟前
SpringBootTest 不能使用构造器注入
java·spring boot
小米渣aa24 分钟前
Vue & React
前端·javascript·vue.js
.又是新的一天.30 分钟前
02_MySQL安装及配置
android·数据库·mysql
一只小闪闪37 分钟前
langchain4j搭建失物招领系统(五)---实现失物登记功能-大模型流式输出
java·人工智能·后端
SimonKing42 分钟前
Kafka 4.0.0震撼来袭,彻底摒弃Zookeeper
java·后端·架构
Java技术小馆43 分钟前
在分布式系统中如何应对网络分区
java·面试·架构
贵州晓智信息科技44 分钟前
Three.js 实现四元数(Quaternion)与常用运算
开发语言·javascript·ecmascript
学也不会1 小时前
d2025331
java·数据结构·算法