一、问题背景

在小游戏开发中,常见的一种操作就是设置绘制属性,比如lineWidth、fillStyle这种,这在WebView方案中,因为全跑在JS环境中,没有任何问题。但是在J2V8方案中,因为绘制逻辑在JS层,绘制实现在Java层,这就导致JS层的属性设置,无法传达到Java层。

本文项目地址:v8x

1
2
3
4
const ctx = canvas.getContext('2d'); // 这里的ctx是Java层返回的V8Object对象
ctx.lineWidth = 10;
...
ctx.stroke();

上述代码中,ctx.lineWidth = 10;修改后,在Java层是无法感知的。

1.1 以往方案

以往方案中,是通过JS层,为ctx对象做代理,拦截到属性变化,再通过调用J2V8的绑定方法,告知Java层,某个属性变化的了。

这样做,有以下几个缺点:

  1. 有这种需求的类可能很多,每一个都要JS层做相应的适配,这会增加工作量和调试沟通成本;

  2. 并不是所有属性的变化都是Java层关心的;

  3. JS层并不知道Java层的属性与类的关系,容易错乱,导致通知了属性变化,却不知道是哪个对象的属性变化了;

  4. 后期Java层类做了变动,需要JS层做相应修改,一旦遗漏,就会产生bug;

基于解决以上问题的目的,开发出新的方案——J2V8Binding。

二、J2V8Binding

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

2.1 基本原理

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

1
2
3
4
5
6
7
8
9
10
11
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层的代理对象。

1
2
3
private fun createJSProxy(v8obj: V8Object): V8Object {
return v8.executeObjectFunction("v8CreateProxy", V8Array(v8).apply { push(v8obj) })
}

2.2 封装

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

2.2.1 V8Binding

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 基本使用方法

一个简单的示例类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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时候,可以按照如下方式使用。

1
2
3
val v8 = V8.createRuntime()
val user = ...
v8.add("user", user.getMyBinding(v8))

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

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

1
2
3
4
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层是无法得知的。

如下代码:

1
2
3
4
5
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

本文采用CC-BY-SA-3.0协议,转载请注明出处
Author: boybeak