threads.disposable() get到的值行为不统一
set的是布尔类型、数字类型时,get到的是java的包装类;set字符串、字面量json时,get到的是js的类型。 建议将这些简单类型统一转化为js类型,其他类型保留为java;或者统一为java类型,文档添加说明。
let a= threads.disposable()
threads.start(()=>a.setAndNotify(false))
let b=a.blockedGet()
log(b)
log(typeof b)
log(b instanceof String)
log(b instanceof Boolean)
log(printAllProperties(b))
log(b.getClass())
function printAllProperties(obj) {
const props = [];
for (let prop in obj) {
props.push(prop);
}
return props;
}
首先非常感谢你的反馈, 也很感谢你提交的 PR #439.
这个 Issue 我需要一个连续的大段时间来认真回复, 我会尽量在 8 月份结束前给出答复.
解决方案 1:
PR https://github.com/SuperMonster003/AutoJs6/pull/439.
即设置 org.mozilla.javascript.WrapFactory#isJavaPrimitiveWrap 为 false (默认为 true).
解决方案 2:
模块 threads 的 disposable 方法返回一个 JavaScript 对象 (而非 Java 对象).
对于其他 Auto.js 版本 (非 AutoJs6), 在 modules/__threads__.js 文件中拦截 disposable 方法, 返回一个自定义的 VolatileDispose 类 (可以是一个 JavaScript function 或 JavaScript object).
这样每一个方法都是 JavaScript 方法 (而非 Java 方法), 这样可以保证返回值符合预期.
对于 AutoJs6, 已经将内置模块 kotlin 化, 因此需要修改 org.autojs.autojs.runtime.api.augment.threads.Threads 文件, 增加一个 disposable 方法, 例如:
override val selfAssignmentFunctions = listOf(
... ...
::disposable.name,
)
companion object : FlexibleArray() {
... ...
@JvmStatic
@RhinoRuntimeFunctionInterface
fun disposable(scriptRuntime: ScriptRuntime, args: Array<out Any?>): VolatileDisposeNativeObject = ensureArgumentsIsEmpty(args) {
VolatileDisposeNativeObject()
}
}
新建 VolatileDisposeNativeObject.kt:
package org.autojs.autojs.runtime.api.augment.threads
import org.autojs.autojs.annotation.RhinoStandardFunctionInterface
import org.autojs.autojs.extension.FlexibleArray
import org.autojs.autojs.extension.FlexibleArray.Companion.component1
import org.autojs.autojs.extension.FlexibleArray.Companion.component2
import org.autojs.autojs.extension.FlexibleArray.Companion.component3
import org.autojs.autojs.util.RhinoUtils
import org.autojs.autojs.util.RhinoUtils.UNDEFINED
import org.autojs.autojs.util.RhinoUtils.coerceLongNumber
import org.mozilla.javascript.Context
import org.mozilla.javascript.Function
import org.mozilla.javascript.NativeObject
import org.mozilla.javascript.Scriptable
import org.mozilla.javascript.Undefined
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.Volatile
@Suppress("unused")
class VolatileDisposeNativeObject : NativeObject() {
@Volatile
private var value: Any? = null
private val lock = ReentrantLock()
private val ready = lock.newCondition()
private val mFunctionNames = arrayOf(
::blockedGet.name,
::blockedGetOrThrow.name,
::setAndNotify.name,
)
init {
RhinoUtils.initNativeObjectPrototype(this)
defineFunctionProperties(mFunctionNames, javaClass, PERMANENT)
}
private inline fun <T> withLock(block: () -> T): T {
lock.lock()
return try {
block()
} finally {
lock.unlock()
}
}
private fun awaitValue(timeoutMillis: Long, onInterrupted: (() -> Unit)? = null): Any? = withLock {
when {
timeoutMillis <= 0L -> {
try {
ready.await()
} catch (e: InterruptedException) {
onInterrupted?.invoke() ?: throw RuntimeException(e)
}
}
else -> {
var nanos = TimeUnit.MILLISECONDS.toNanos(timeoutMillis)
while (nanos > 0L) {
try {
ready.awaitNanos(nanos).also { nanos = it }
} catch (e: InterruptedException) {
onInterrupted?.invoke() ?: throw RuntimeException(e)
}
}
}
}
value
}
private fun <T : RuntimeException> instantiateRuntimeException(clazz: Class<T>) = try {
clazz.getDeclaredConstructor().newInstance()
} catch (e: Exception) {
RuntimeException(e)
}
companion object : FlexibleArray() {
@JvmStatic
@RhinoStandardFunctionInterface
fun blockedGet(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Any? = ensureArgumentsAtMost(args, 1) { argList ->
val (timeout) = argList
val self = thisObj as VolatileDisposeNativeObject
val timeoutMillis = coerceLongNumber(timeout, 0L)
self.awaitValue(timeoutMillis)
}
@JvmStatic
@RhinoStandardFunctionInterface
fun blockedGetOrThrow(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Any? = ensureArgumentsLengthInRange(args, 1..3) { argList ->
val (exception, timeout, defaultValue) = argList
require(exception is Class<*>) {
"Argument \"exception\" must be a RuntimeException Class for ${VolatileDisposeNativeObject::class.java.simpleName}::blockedGetOrThrow"
}
val self = thisObj as VolatileDisposeNativeObject
val timeoutMillis = coerceLongNumber(timeout, 0L)
self.awaitValue(timeoutMillis) {
@Suppress("UNCHECKED_CAST")
throw self.instantiateRuntimeException(exception as Class<RuntimeException>)
} ?: defaultValue
}
@JvmStatic
@RhinoStandardFunctionInterface
fun setAndNotify(cx: Context, thisObj: Scriptable, args: Array<Any?>, funObj: Function): Undefined = ensureArgumentsOnlyOne(args) { value ->
val self = thisObj as VolatileDisposeNativeObject
self.withLock {
self.value = value
self.ready.signalAll()
}
UNDEFINED
}
}
}
解决方案 3, 不修改任何 Auto.js 源码, 直接从 Rhino 引擎脚本代码进行修改.
直接使用 util.unwrapJavaObject 即可.
为此我编写了一个迷你测试工具:
let samples = [3, true, "x", null, {x: 3}, [3], () => 3];
let isUnwrap = false;
let isPrintAllProperties = false;
samples.forEach(printSample);
function printSample(sample) {
console.log('-'.repeat(16));
let a = threads.disposable();
threads.start(() => a.setAndNotify(sample));
let b = isUnwrap
? util.unwrapJavaObject(a.blockedGet())
: a.blockedGet();
log(`sample: ${util.format(b)}`)
log(`typeof: ${typeof b}`)
isJavaObject(b)
? console.log(`class: ${util.className(b)}`)
: console.log(`species: ${species(b)}`);
isPrintAllProperties && printAllProperties(b);
}
function printAllProperties(obj) {
const props = [];
for (let prop in obj) {
props.push(prop);
}
if (props.length > 0) {
console.log(`Properties: [\n${props.map(o => ` ${o}`).join(',\n')},\n]`);
} else {
console.log('Properties: []');
}
}
通常来说, threads 属于 Auto.js 内置全局模块, 它应该尽可能在访问方法即变量时, 返回或得到一个 JavaScript 对象, 毕竟编写脚本时, 一般是围绕 JavaScript 这个核心语言的. 从这个角度来讲, Auto.js 提供的每一个 JavaScript 内置模块, 不论怎样使用, 都应该返回一个 JavaScript 类型.
方案 2 的目标, 就是将暴露给用户使用的方法, 进行一层转换. 这也是 modules 文件夹中诸多 *.js 文件的作用之一.
方案 3 要求用户在脚本层面进行拆箱, 这与上述理念不相符, 仅适合当前所使用的 Auto.js 无法进行源码更新等情况.
而对于方案 1, 它虽然可以达到同样的目标 (针对当前 issue), 但它存在一定的副作用.
示例:
[
new java.lang.String("x"),
java.lang.String.valueOf("x"),
new java.lang.Integer(3),
java.lang.Integer.valueOf(3),
new java.lang.Long(4),
java.lang.Long.valueOf(4),
new java.lang.Double(5.6),
java.lang.Double.valueOf(5.6),
new java.lang.Boolean(true),
java.lang.Boolean.valueOf(true),
new java.lang.Character('c'),
java.lang.Character.valueOf('c'),
].forEach(print);
function print(o) {
log(`sample: ${util.format(o)}`)
log(`typeof: ${typeof o}`)
let cls = isJavaObject(o)
? `class: ${util.className(o)})`
: `species: ${species(o)}`;
log(cls);
log(`-`.repeat(cls.length));
}
针对 species 值的表格对比:
| Java Values | isJavaPrimitiveWrap: true (default) | isJavaPrimitiveWrap: false |
|---|---|---|
| new java.lang.String("x") | JavaObject | JavaObject |
| java.lang.String.valueOf("x") | String | String |
| new java.lang.Integer(3) | JavaObject | JavaObject |
| java.lang.Integer.valueOf(3) | JavaObject | Number |
| new java.lang.Long(4) | JavaObject | JavaObject |
| java.lang.Long.valueOf(4) | JavaObject | Number |
| new java.lang.Double(5.6) | JavaObject | JavaObject |
| java.lang.Double.valueOf(5.6) | JavaObject | Number |
| new java.lang.Boolean(true) | JavaObject | JavaObject |
| java.lang.Boolean.valueOf(true) | JavaObject | Boolean |
| new java.lang.Character('c') | JavaObject | JavaObject |
| java.lang.Character.valueOf('c') | JavaObject | String |
可以看到, 对于 new 这样显式构造 Java 对象, 它不受 isJavaPrimitiveWrap 的影响, 但 valueOf 静态方法获得的值会被影响.
我们再看其他获取 Java 对象的方式:
[
new java.lang.Boolean(true),
new java.lang.Boolean("true"),
java.lang.Boolean.valueOf(true),
java.lang.Boolean.valueOf("true"),
java.lang.Boolean.TRUE,
java.lang.Class.forName("java.lang.Boolean").getConstructor(java.lang.Boolean.TYPE).newInstance(true),
java.lang.Boolean.class.getDeclaredConstructor(java.lang.Boolean.TYPE).newInstance(true),
].forEach((o) => {
console.log(o + " (" + species(o) + ")");
});
结果:
/* isJavaPrimitiveWrap=true */
// true (JavaObject)
// true (JavaObject)
// true (JavaObject)
// true (JavaObject)
// true (JavaObject)
// true (JavaObject)
// true (JavaObject)
/* isJavaPrimitiveWrap=false */
// true (JavaObject)
// true (JavaObject)
// true (Boolean)
// true (Boolean)
// true (Boolean)
// true (Boolean)
// true (Boolean)
依然只有 new 不受影响.
这就意味着副作用, 当之前的用户习惯使用 java.lang.Boolean.TRUE, java.lang.Boolean.valueOf(true) 以及 xxx instanceof java.lang.Boolean 这样的代码时, isJavaPrimitiveWrap 的变更会导致上述代码全部受影响, 对于脚本开发者来说是十分不友好的.
因此不推荐 isJavaPrimitiveWrap = false 这样的方式来解决当前 issue.
PS: 包括 Boolean 在内, 受 isJavaPrimitiveWrap 影响的有 String/Number/Boolean/Character 这些类型.
如果你有其他疑问, 或者对 modules/__threads__.js 如何修改有兴趣, 或者对 WrapFactory 类有兴趣等等, 都可以在当前 issue 继续讨论.
最后, 感谢你提供的反馈, 同时也对于没有按照约定于 8 月份结束前给出答复表示歉意.
感谢回复,我有个疑问,没理解错的话,setAndNotify是一个java方法,传入js对象时get到的是js对象,这说明Rhino自动把js对象映射成java对象后入参,get时再把java对象映射回js对象(否则得到的应该是java Map之类的结构),这是双向的;但是如果传入js基本类型,如number,映射到java Double,get到的是仍是java Double,这是单向的。这是Rhino的设计还是autojs的问题?另外setAndNotify(3) 时get到的仍是3,而不是3.0,下面的示例却是3.0,这怎么回事?
var sb = new java.lang.StringBuffer();
sb.append("hi, mom");
sb.append(3); // this will add "3.0" to the buffer since all JS numbers are doubles by default
sb.append(true);
// Should print "hi, mom3.0true".
print(sb);
是的, setAndNotify 是一个 Java 方法, 它位于 org.autojs.autojs.concurrent.VolatileDispose#setAndNotify:
public void setAndNotify(T value) {
synchronized (this) {
mValue = value;
notify();
}
}
使用脚本调用时, 如 xxx.setAndNotify(true);, JavaScript 的 true 会被 Rhino 引擎转换为 java.lang.Boolean 类型传入 setAndNotify 方法中, 这个过程可以称为装箱.
装箱是自动按需完成的, Rhino 引擎的目标, 是按照目标方法签名自动进行适配传入的参数, 传入的参数如果是 JavaScript, 就进行装箱, 如果已经是 Java 且与目前参数匹配, 则无需装箱. 因此, 下面 4 行代码的效果, 其实是一样的:
d.setAndNotify(true);
d.setAndNotify(java.lang.Boolean.TRUE);
d.setAndNotify(new java.lang.Boolean(true));
d.setAndNotify(java.lang.Boolean.valueOf(true));
上面的过程, 其实是脚本到源码, 可以理解为 JavaScript 到 Java. 接下来就是 Java 到 JavaScript:
d.blockedGet();
示例中的 blockedGet 是一个 Java 方法:
public T blockedGet(long timeout) {
synchronized (this) {
if (mValue != null) {
return mValue;
}
try {
this.wait(timeout);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return mValue;
}
此时就需要考虑返回到 JavaScript 环境的值, 到底是保留 Java 包装类型还是 JavaScript 拆包类型.
再次以刚才提到的 java.lang.Boolean 为例, 首先 isJavaPrimitiveWrap 决定是否将其转换为 JavaScript, 通过查看 Rhino 源码可以看到线索:
/**
* Return <code>false</code> if result of Java method, which is instance of <code>String</code>,
* <code>Number</code>, <code>Boolean</code> and <code>Character</code>, should be used directly
* as JavaScript primitive type. By default the method returns true to indicate that instances
* of <code>String</code>, <code>Number</code>, <code>Boolean</code> and <code>Character</code>
* should be wrapped as any other Java object and scripts can access any Java method available
* in these objects. Use {@link #setJavaPrimitiveWrap(boolean)} to change this.
*/
public final boolean isJavaPrimitiveWrap() {
return javaPrimitiveWrap;
}
对于 String/Number/Boolean/Character 这些类型, 如果 isJavaPrimitiveWrap 为 false (默认值为 true), 则返回到 JavaScript 环境的值, 将会拆装为 JavaScript 对应的值. 而 java.lang.Boolean 恰好属于上述类型范围, 因此受到 isJavaPrimitiveWrap 的影响.
然后需要考虑一个特殊情况, new. 当使用 new 关键字构造 Java 对象时, 不受 isJavaPrimitiveWrap 影响, 相当于明确告诉 Rhino 引擎, 我需要创建一个 Java 对象并且不要进行自动拆箱. 因此我之前回复的 comment 中, "针对 species 值的表格对比" 提到的表格, 才会体现 new 得到的对象一直都作为 JavaObject 出现在 JavaScript 脚本中.
现在我们聚焦到当前 issue 最后提到的示例代码:
var sb = new java.lang.StringBuffer();
sb.append("hi, mom");
sb.append(3); // this will add "3.0" to the buffer since all JS numbers are doubles by default
sb.append(true);
// Should print "hi, mom3.0true".
print(sb);
可以看到, sb 对象是 new 构造出来的, 因此它一定是一个 Java 对象. 它调用 append 这个 Java 方法时, 参数会被装箱为 Java 对象:
"hi, mom": string -> java.lang.String
3: number -> java.lang.Double
true: boolean -> java.lang.Boolean
每次调用 append 时, sb 变量的变化:
""
-> "hi, mom"
-> "hi, mom3.0"
-> "hi, mom3.0true"
方法 append 是 Java 方法, 它进行拼接时, 会先将 JavaScript 装箱, 然后按照 Java 对象类型进行拼接, 因此 3 会被装箱为 Double 类型的 3.0.
方法 print 的参数 sb 本身就是 Java 对象, 因此无需再装箱. 而 print 可以通过源码看出, 它与 console.log 是等效的 (虽然在某种程度上可能不是很合理).
最后, 上面的实例如果将 sb.append(3); 修改为 sb.append(new java.lang.Integer(3));, 就会打印 hi, mom3true.
如果修改为 sb.append(java.lang.Integer.valueOf(3));, 打印结果将受到 isJavaPrimitiveWrap 的影响.
关于 new 得到的 Java String 对象 (new java.lang.String) 传入 setAndNotify 后为什么 blockedGet 会得到 JavaScript string, 我会在下一个 comment 回复 (主要因为当前 comment 已经很长了). 同时还会包含 wrapAsJavaObject 的作用, 当前 issue 提及的 "单向/双向" 问题的系统理解, Scriptable 与 NativeXxx 类族是什么等.
[
new java.lang.String("x"),
new java.lang.Character('y'),
new java.lang.Byte(0x7F),
new java.lang.Integer(0xFF),
new java.lang.Double(3.3),
new java.lang.Boolean(true),
].forEach(run);
function run(o) {
// 使用 runtime 的 threads 对象, 确保 disposable 返回的是 Java 对象以便于测试.
let d = runtime.threads.disposable();
threads.start(d.setAndNotify.bind(d, o));
let v = d.blockedGet();
console.log(v + ', ' + typeof v + ', ' + species(v));
}
结果如下:
x, string, String
y, object, JavaObject
127, object, JavaObject
255, object, JavaObject
3.3, object, JavaObject
true, object, JavaObject
会发现第一个打印结果不是预期的 x, object, JavaObject, 原因在于 wrap 方法, 位于 org.autojs.autojs.rhino.AndroidContextFactory.WrapFactory#wrap:
override fun wrap(cx: Context, scope: Scriptable, obj: Any?, staticType: Class<*>?): Any? = when {
obj is String -> bridges.toString(obj.toString())
staticType == UiObjectCollection::class.java -> (obj as? UiObjectCollection)?.let { bridges.asArray(it) } ?: UiObjectCollection.EMPTY
else -> super.wrap(cx, scope, obj, staticType)
}
可以看到 wrap 方法拦截了默认的 String 处理规则, 将传回 JavaScript 环境的 String 进行了 bridges.toString(obj.toString()) 处理:
override fun toString(obj: Any?): String = Context.toString(obj)
注意 Context 是 org.mozilla.javascript.Context, 而非常见的 android.content.Context.
Context.toString 方法将 obj 转换为 Rhino 引擎支持的 JavaScript string, 因此上述示例代码才会打印出 JavaScript 的变量信息.
总结起来, Java -> JS 过程中, Java String 被特殊处理了.
Java -> JS 过程中, 涉及到转换处理的地方, 有以下三处, 它们全部位于 Rhino 引擎的 org.mozilla.javascript.WrapFactory 类当中:
public Object wrap(Context cx, Scriptable scope, Object obj, Class<?> staticType) {
if (obj == null || obj == Undefined.instance || obj instanceof Scriptable) {
return obj;
}
if (staticType != null && staticType.isPrimitive()) {
if (staticType == Void.TYPE) return Undefined.instance;
if (staticType == Character.TYPE) return Integer.valueOf(((Character) obj).charValue());
return obj;
}
if (!isJavaPrimitiveWrap()) {
if (obj instanceof String
|| obj instanceof Boolean
|| obj instanceof Integer
|| obj instanceof Byte
|| obj instanceof Short
|| obj instanceof Long
|| obj instanceof Float
|| obj instanceof Double
|| obj instanceof BigInteger) {
return obj;
} else if (obj instanceof Character) {
return String.valueOf(((Character) obj).charValue());
}
}
Class<?> cls = obj.getClass();
if (cls.isArray()) {
return NativeJavaArray.wrap(scope, obj);
}
return wrapAsJavaObject(cx, scope, obj, staticType);
}
public Scriptable wrapNewObject(Context cx, Scriptable scope, Object obj) {
if (obj instanceof Scriptable) {
return (Scriptable) obj;
}
Class<?> cls = obj.getClass();
if (cls.isArray()) {
return NativeJavaArray.wrap(scope, obj);
}
return wrapAsJavaObject(cx, scope, obj, null);
}
public Scriptable wrapAsJavaObject(
Context cx, Scriptable scope, Object javaObject, Class<?> staticType) {
if (List.class.isAssignableFrom(javaObject.getClass())) {
return new NativeJavaList(scope, javaObject);
} else if (Map.class.isAssignableFrom(javaObject.getClass())) {
return new NativeJavaMap(scope, javaObject);
}
return new NativeJavaObject(scope, javaObject, staticType);
}
上面三个方法都属于 Java -> JS 的包装链路:
wrap相当于 Java -> JS 发生前的总入口, 可以理解为通用包装器wrapNewObject则作为new对象包装入口wrapAsJavaObject出现在了其余两个方法中作为 Fallback 机制 (兜底机制), 同时支持按类型定制, 是自定义 Java 类型在 JavaScript 环境呈现方式的主要扩展点
通过源码可以发现, isJavaPrimitiveWrap 出现在 wrap 中, 而没有出现在 wrapNewObject 中, 这就解释了 new 得到的对象不受 isJavaPrimitiveWrap 影响的原因.
对于 JS -> Java, 其转换发生在调用 Java 方法/构造器/字段写入等时机, 主要涉及到下面的一个或多个方法:
- Context.jsToJava(value, targetType), 顶层适配入口
- NativeJavaObject.coerceTypeImpl(...), 强制类型转换
- NativeJavaMethod/NativeJavaConstructor + JavaMembers/MemberBox, 方法解析与重载选择, 并对每个实参调用 jsToJava
- ScriptRuntime 的 toNumber/toBoolean/toString 等基础转换 (当前 comment 开始时提到的 Context.toString, 内部调用的就是 ScriptRuntime.toString)
- Wrapper 接口的 unwrap(), 可取出底层 Java 对象
大致总结:
Java -> JS
- wrap(...) (普通返回值/字段值) 或 wrapNewObject(...) (构造器 new)
- 若不能 JS 原始化/数组化等, 则回落到 wrapAsJavaObject(...) (支持扩展)
JS -> Java
- 解析目标方法/构造器 (NativeJavaMethod/Constructor)
- 对每个 JS 实参执行 Context.jsToJava(...) 等
- 反射调用, 返回值再经 wrap(...) 返回到 JS
上面提到的 NativeXxx 是 Rhino 内置的 "原生宿主实现", 每个类实现 Scriptable, 用来在引擎里承载具体的 JS 内建对象或宿主桥接对象, 让 JS 和 Java 的类型系统在运行时有清晰的语义映射关系, 例如:
- NativeObject/NativeArray/NativeFunction/NativeRegExp/NativeDate/NativeError/NativeString/NativeNumber/NativeBoolean
- 分别对应 JS 的 Object/Array/Function/RegExp/Date/Error 以及装箱对象等, 定义了属性/原型/内建方法与运行时行为
- NativeJavaObject
- Java -> JS 的通用桥接包装. 把任意 Java 对象包装 (更确切的说, 应该是投影, 强调将对象转换为另一种新形式) 为一个 JS 可访问的对象
- NativeJavaClass
- 表示一个 Java 的 Class<?> 在 JS 中的形态, 支持静态方法/静态字段访问以及作为构造器可被 new 构造
- NativeJavaArray
- 把 Java 数组暴露为 JS 可索引的对象
- NativeJavaMethod/NativeJavaConstructor
- 封装 Java 方法/构造器, 负责重载解析/参数转换与调用
- NativeJSON/NativePromise/NativeIterator 等
- 对应 ES 标准或 Rhino 扩展的内建对象
- NativeJavaList/NativeJavaMap
- Java 容器桥接包装, 让 JS 端以更自然的语义访问 Java List/Map (比如用数组/对象式操作)
回到单向双向问题 (仅针对 Java 方法调用):
传入 js 对象时 get 到的是 js 对象, 这说明 Rhino 自动把 js 对象映射成 java 对象后入参, get 时再把 java 对象映射回 js 对象 (否则得到的应该是 java Map 之类的结构), 这是双向的. 传入 js 基本类型, 如 number, 映射到 java Double, get 到的是仍是 java Double, 这是单向的.
简化为:
JavaScript object -> [ Java Object ] -> JavaScript object JavaScript number -> [ Java Double ] -> Java Double
JS -> Java (调用 setAndNotify 时):
- Rhino 会按 Java 形参类型做 JS -> Java 适配.
setAndNotify的参数是 Object (或 T), 因此:- 传入 JS 对象/数组时, Rhino 会把它以 Scriptable (例如 NativeObject/NativeArray) 形态直接传给 Java. Java 端实际拿到的是一个 Rhino 的 Scriptable 实例 (本质仍在 JS 世界)
- 传入 JS 基本类型 (number/boolean/string) 时, Rhino 会装箱为对应的 Java 包装类型 (Double/Boolean/String 等), 再传给 Java
Java -> JS (调用 blockedGet 取回时):
- Rhino 会走 WrapFactory 包装 Java -> JS, 规则如下:
- 如果返回值本身就是 Scriptable (JS 对象是以 Scriptable 存入的), 则原样返回, 在 JS 中继续是 JS 对象/数组, 看起来是 "双向" 的
- 如果返回值是 Java 包装类 (如 Double/Boolean), 是否把它 "还原" 为 JS 原生类型, 取决于 WrapFactory 配置与覆写:
- isJavaPrimitiveWrap=true (Rhino 默认): 保留为 Java 对象 (表现为 JavaObject)
- isJavaPrimitiveWrap=false: 转为 JS 原生 number/boolean/string
- 项目可通过覆写 wrap(...) 做更细粒度的控制 (如上文提及的 Auto.js 对于 Java String 的统一控制).
结论:
- JS 对象调用 Java 函数会出现不确定性, 这取决于 WrapFactory 策略 (Rhino 机制, Auto.js 可以通过覆写或开关进行部分干预).
- 而 JS 对象调用 JS 函数可保持 JS 对象, 是 Rhino 的设计 (因为传参时就是 Scriptable), 并非 Auto.js 特例. 我在最早的 comment 提到新建一个 VolatileDisposeNativeObject.kt 文件, 它就是模拟 JavaScript 函数, 让方法调用直接走 JS -> JS 路线.
还剩下最后一个疑问点:
另外 setAndNotify(3) 时 get 到的仍是 3, 而不是 3.0
如果你使用的是 AutoJs6 6.6.4, 而且使用了 console.log 打印结果, 那么确实会得到不正确的 3 (正确结果应该为 3.0, 这是 AutoJs6 的 bug). 一个验证方法是使用 species 或 typeof 显示结果的种类或类型:
console.log(new java.lang.Double(1)); // 1 或 1.0
console.log(typeof new java.lang.Double(1)); // object
console.log(species(new java.lang.Double(1))); // JavaObject
一个小扩展, 经典的 paint.setColor 问题.
详见 AutoJs6 issue #38, 利用 wrapAsJavaObject 覆写可以很好地解决上述问题, 具体可以怎样实现呢.
下一个 comment 会给出一个可参照方案.
一个小彩蛋, Rhino 引擎知识测验.
下面的两行代码, 打印的结果会是什么:
console.log(1 + ": " + new java.lang.Double(1));
console.log(1 + ": " + new java.lang.Double(1).toString());
下一个 comment 会给出结果并解释原因.
如有其他疑问, 欢迎继续讨论.
paint.setColor 问题, 除 AutoJs6 的 #38 之外, 在 AutoX 项目也有涉及.
因 AutoX 项目暂时无法访问, 此处将相关 issue 大致内容粘贴出来.
[kkevsekk1/AutoX] 画布设置画笔颜色失败,不管设置什么颜色都显示白色 (Issue 756)
Cryogenic-yosa @ Dec 4, 2023, 4:59 PM
Autox.js 版本:655 Autox.js 下载渠道:https://github.com/kkevsekk1/AutoX/releases Android 版本:Android 13 Android 机型:iqoo Android 系统类别:橘子 问题描述:官方文档canvas里设置画笔颜色失败。如果不设置颜色那就是黑色,设置颜色只会变成白色,有没有办法让设置颜色成功?疑似aj4.1就存在这个问题 代码:
"ui"; //ui布局为一块画布
ui.layout(
<vertical>
<canvas id="board" h = "*" w = "*"/>
</vertical>
);
ui.board.on("draw", function(canvas) {
let paint = new Paint(); // 设置画笔为描边,则绘制出来的图形都是轮廓
paint.setStyle(Paint.Style.STROKE); // 设置画笔颜色为红色
paint.setColor(colors.RED); // 绘制一个从坐标(0, 0)到坐标(100, 100)的正方形
canvas.drawRect(0, 0, 100, 100, paint);
});
IMG_20231204_165544.jpg (view on web)
LYS @ Dec 6, 2023, 9:03 AM
setColor 安卓10参数变更,使用 pack 转换一下
let color = android.graphics.Color.pack(colors.RED)
paint.setColor(color);
Cryogenic-yosa @ Dec 6, 2023, 10:21 AM
感谢老铁🙏
[kkevsekk1/AutoX] 安卓 14 上 Paint.setColor() 方法报错 (Issue 1182)
pansong @ Sep 8, 2024, 11:08 PM
Autox.js 版本:664 Autox.js 下载渠道:https://github.com/kkevsekk1/AutoX/releases Android 版本:Android 14 Android 机型:小米13 Android 系统类别:MIUI
代码如下:
const window = floaty.window(
<frame gravity="center">
<text id="text" text="点击可调整位置" textSize="16sp"/>
<canvas id="board" w="*" h="*"/>
</frame>
);
window.exitOnClose();
window.text.click(()=>{
window.setAdjustEnabled(!window.isAdjustEnabled());
});
//画笔
var paint = new Paint();
//设置画笔颜色
paint.setColor(colors.parseColor("#ff0ff0ff"));
window.board.on("draw", function(canvas){
var w = canvas.getWidth();
var h = canvas.getHeight();
canvas.drawRect(200,200,500,500,paint);
});
setInterval(()=>{}, 1000);
22:59:46.174/E: Wrapped java.lang.IllegalArgumentException: Invalid ID, must be in the range [0..18) (/storage/emulated/0/脚本/test.js#17)
Wrapped java.lang.IllegalArgumentException: Invalid ID, must be in the range [0..18)
at /storage/emulated/0/脚本/test.js:17:0
然后我把设置颜色那里的代码改成了这样的字符串:
paint.setColor("#ff0ff0ff");
得到如下报错:
23:05:58.254/E: The choice of Java method android.graphics.Paint.setColor matching JavaScript argument types (string) is ambiguous; candidate methods are:
void setColor(int)
void setColor(long) (/storage/emulated/0/脚本/test.js#17)
The choice of Java method android.graphics.Paint.setColor matching JavaScript argument types (string) is ambiguous; candidate methods are:
void setColor(int)
void setColor(long)
at /storage/emulated/0/脚本/test.js:17:0
pansong @ Sep 10, 2024, 10:15 AM
这是 Rhino 无法解决的一个痛点,目前的解决办法: 1180 (comment)
pansong @ Sep 10, 2024, 3:29 PM
看起来似乎是优先调用了 setColor(long) 才报的错。
那么可能的解决方案:
try {
paint.setColor(colors.pack(colors.parseColor("#ff0ff0ff")));
} catch(e) {
paint.setColor(colors.parseColor("#ff0ff0ff"));
}
其中 colors.pack 详见 https://developer.android.google.cn/reference/android/graphics/Color#pack(int)
aiselp @ Sep 24, 2024, 10:47 PM
Rhino重载问题确实不好解决,受你提供的示例启发,我发现可以直接使用反射来解决
var Integer = java.lang.Integer;
var paint = new Paint();
paint
.getClass()
.getMethod("setColor", Integer.TYPE)
.invoke(paint, Integer.valueOf(colors.parseColor("#cc0ff0ff")));
pansong @ Sep 24, 2024, 11:57 PM
[at] aiselp 反射是我最开始尝试的方法,如下代码:
Packages.java.lang.Class.forName('android.graphics.Paint')
.getMethod('setColor', Packages.java.lang.Integer.TYPE)
.invoke(paint, color)
很遗憾,上面这个代码是不行的,仍然会报错:
Wrapped java.lang.IllegalArgumentException: Invalid ID, must be in the range [0..18)
这里的 color 并没有使用 Integer 进行包装,所以它传入的值还是会被转为 long。 不知道包装后能不能行得通,我没有测试过。
aiselp @ Sep 25, 2024, 9:37 AM
我测了必须使用反射+包装类型才能避免被转换,直接传入包装类型也不行
pansong @ Sep 25, 2024, 3:46 PM
[at] aiselp 反射调用是因为 invoke() 方法的形参是 Object[],然后传入的实参是 Integer,这样的话我猜测可能 Rhino 就没有去做类型转换,如果是普通方法 setColor(),虽然实参是 Integer,但 Rhino 仍然做了类型转换?
aiselp @ Sep 25, 2024, 4:46 PM
大概就是这样,Rhino优先获取到的方法是接收Long的,而Integer是可以无损转换成Long就直接使用此方法了,而没有再去判断其他方法是否有更优选择
pansong @ Nov 14, 2024, 6:12 PM
附:自定义 View 的 js 实现
1179 (comment)
AutoJs6 截止 6.6.4 版本给出的解决方案, 是使用 colors.setPaintColor 方法替代 paint.setColor, 例如:
let paint = new android.graphics.Paint();
/* 安卓 10 及以上系统无法正常设置颜色. */
// paint.setColor(colors.toInt('blue'));
/* 使用 colors 模块实现原始功能. */
colors.setPaintColor(paint, 'blue');
不过这需要脚本开发者修改原有的代码, 给开发体验带来影响.
借助我们在当前 issue 中多次提到的 wrapAsJavaObject 方法覆写, 可以实现不修改脚本代码就可以正常使用 paint.setColor 的目标.
定位 org.autojs.autojs.engine.RhinoJavaScriptEngine.WrapFactory#wrapAsJavaObject:
override fun wrapAsJavaObject(cx: Context?, scope: Scriptable, javaObject: Any?, staticType: Class<*>?): Scriptable? {
return when (javaObject) {
is View -> ViewExtras.getNativeView(scope, /* view = */ javaObject, staticType, runtime)
else -> super.wrapAsJavaObject(cx, scope, javaObject, staticType)
}
}
添加对于 android.graphics.Paint 分支:
override fun wrapAsJavaObject(cx: Context?, scope: Scriptable, javaObject: Any?, staticType: Class<*>?): Scriptable? {
return when (javaObject) {
is View -> ViewExtras.getNativeView(scope, /* view = */ javaObject, staticType, runtime)
is Paint -> super.wrapAsJavaObject(cx, scope, RhinoPaint(javaObject), staticType)
else -> super.wrapAsJavaObject(cx, scope, javaObject, staticType)
}
}
创建 RhinoPaint 类:
package org.autojs.autojs.core.image
import android.graphics.Paint
import org.autojs.autojs.runtime.api.augment.colors.Colors
class RhinoPaint(paint: Paint) : Paint(paint) {
override fun setColor(color: Long) = setColor(Colors.toIntRhino(color))
fun setColor(color: Any?) = setColor(Colors.toIntRhino(color))
}
这样, 就可以在 JS -> Java 链路上, 将 Paint 类进行处理, 明确传入 int 参数 (而非 long 参数) 到 setColor 方法当中.
接下来分析下面的两行代码, 打印的结果会是什么:
console.log(1 + ": " + new java.lang.Double(1));
console.log(1 + ": " + new java.lang.Double(1).toString());
这里涉及到字符串拼接 (已明确是 "字符串" 拼接), 在 Rhino 内部, 字符串拼接会将所有操作数进行 org.mozilla.javascript.Context.toString 处理. 因此, new java.lang.Double(1) 经过 Context.toString 处理, 按照 JavaScript number 接受参数, 于是相当于 JavaScript 代码 (1).toString(), 得到 1. 第一行打印结果为 1: 1.
new java.lang.Double(1).toString() 直接调用的是 Java Double 的 toString 方法, 得到 1.0, 因此拼接后的结果为 1: 1.0.
最终结果:
console.log(1 + ": " + new java.lang.Double(1)); // "1: 1"
console.log(1 + ": " + new java.lang.Double(1).toString()); // "1: 1.0"
看了几遍,没理解错的话,Scriptable 可以看成一个特殊的桥接对象,既是java对象也是“原生js对象”,所有的js对象都是java对象,所以入参时不用转换,get到的没变,本质仍是java对象,但又可以当做原生js对象使用。这样又有个疑问,那为什么不设计一个既是java 包装类又是“js 基本类型”的桥接对象,这样可以避免入参时装箱,返回时看设置决定是否拆箱的情况,是因为js基本类型不是对象难以桥接吗?
最后的测试没看懂,new java.lang.Double(1) 是一个java对象,Context.toString怎么按照 JavaScript number 接受参数,如果拼接的是别的java对象,如 console.log(1 + ": " + new java.io.File("/sdcard")); 又怎么处理,是不是Context.toString对这些数值型的java对象做了特殊处理?
先分析 console.log(1 + ": " + new java.lang.Double(1));, 之前的回复是希望强调, Context.toString 会按照 JavaScript number 那样解析 Java Double, 而不是调用 Java Double 的 toString 方法.
加号操作符出现两次, 有三个操作数, 过程如下:
1 (JavaScript number) -> [装箱] -> java.lang.Double(1) -> [传入] -> Context.toString(java.lang.Double(1)) -> 1 (JavaScript string)
":" (JavaScript string) -> [装箱] -> java.lang.String(":") -> [传入] -> Context.toString(java.lang.String(":")) -> ":" (JavaScript string)
java.lang.Double(1) -> [直接传入] -> Context.toString(java.lang.Double(1)) -> 1 (JavaScript string)
第三行与第一行装箱后的后续过程是一致的, 因此才有 "Context.toString 会按照 JavaScript number 那样解析 Java Double" 的说法.
同样, console.log(1 + ": " + new java.io.File("/sdcard")); 也是类似的过程, Context.toString 调用 java.io.File("/sdcard") 返回 "/sdcard".
Context.toString 相关源码:
/**
* Convert the value to a string.
*
* <p>See ECMA 9.8.
*/
public static String toString(Object val) {
for (; ; ) {
if (val == null) {
return "null";
}
if (Undefined.isUndefined(val)) {
return "undefined";
}
if (val instanceof String) {
return (String) val;
}
if (val instanceof CharSequence) {
return val.toString();
}
if (val instanceof BigInteger) {
return ((BigInteger) val).toString(10);
}
if (val instanceof Number) {
// XXX should we just teach NativeNumber.stringValue()
// about Numbers?
return numberToString(((Number) val).doubleValue(), 10);
}
if (val instanceof Boolean) {
return val.toString();
}
if (isSymbol(val)) {
throw typeErrorById("msg.not.a.string");
}
if (val instanceof Scriptable) {
// Assert: val is an Object
val = toPrimitive(val, StringClass);
// Assert: val is a primitive
} else {
warnAboutNonJSObject(val);
return val.toString();
}
}
}
public static String numberToString(double d, int base) {
if ((base < 2) || (base > 36)) {
throw ScriptRuntime.rangeErrorById("msg.bad.radix", Integer.toString(base));
}
if (Double.isNaN(d)) return "NaN";
if (d == Double.POSITIVE_INFINITY) return "Infinity";
if (d == Double.NEGATIVE_INFINITY) return "-Infinity";
if (d == 0.0) return "0";
if (base != 10) {
return DToA.JS_dtobasestr(base, d);
}
// V8 FastDtoa can't convert all numbers, so try it first but
// fall back to old DToA in case it fails
String result = FastDtoa.numberToString(d);
if (result != null) {
return result;
}
StringBuilder buffer = new StringBuilder();
DToA.JS_dtostr(buffer, DToA.DTOSTR_STANDARD, 0, d);
return buffer.toString();
}
其中 numberToString 是按 ECMAScript 规范生成 "最短且可还原" 的十进制字符串表示, 它通常会去掉无意义的小数点和尾随 0. 因此传入 Double(3.0) 会得到 "3". 而 File 是一个 Java 对象, 通过 toString 源码可以看出, 最终会直接调用 File#toString 方法.
接下来讨论一下 Scriptable 和桥接对象的设计问题.
Rhino 引擎中的 JavaScript 对象, 在 Java 侧都以 Scriptable 接口的实现来表示. 例如把 JS [ 对象/数组/函数 ] 等作为参数传给 Java 方法时, Rhino 会直接把对应的 Scriptable 实例传给 Java, 即一个实现了 Scriptable 的对象, 此时的入参无需额外转换. 但如果传入 Java 的是 JS 基本类型 [ number/boolean/string/null/undefined/... ] 时, 仍要进行 JS -> Java 做类型适配, 如装箱为 Java [ Double/Boolean/String/null/Undefined/... ].
为何不设计一个既是 Java 包装类又是 JS 基本类型的桥接对象? JS 基本类型 (number/boolean/string/null/undefined/bigint/symbol) 在 ECMAScript 里不是对象, 不能直接作为具有属性表/原型链/对象标识的实体存在. 引擎只能在需要对象语义时临时做装箱 (生成一个短生命周期的包装对象, 如 NativeNumber/NativeString 实例), 而不是让这个基本类型值本身成为一个 Scriptable.
JavaScript 基本类型是 "非对象" 的抽象值: 没有可变属性表/没有对象身份/引用语义/没有原型链/typeof 必须返回 "number"/"string"/"boolean"/"bigint"/"symbol" 而非 "object". 如果把基本类型直接实现为 Scriptable, 就会违反 ECMAScript 语义:
- typeof 会全部变为 "object"
- 属性读写/原型链/可枚举性都会偏离规范 (比如
(1).x = 1;变为合法) - [ ==/===/Object.is/JSON.stringify/结构化拷贝/... ] 出现非预期结果
临时装箱策略可以很好地解决上述问题, 例如 "abc".toUpperCase() 或 (42).toFixed(0) 代码执行时, Rhino 引擎会短暂创建一个包装对象 (NativeString/NativeNumber), 用完后生命周期立即结束. 既实现了方法调用, 又不破坏 "基本类型不是对象" 的语义.
最后总结一下, 不能做一个既是 Java 包装类又是 JS 基本类型的桥接体, 那会同时破坏 [ typeof/属性语义/原型链/相等性 ] 等大量 ECMAScript 规范的语义和要求. 当前 Rhino 采用 "值是值/对象是对象; 必要时临时装箱" 的策略, 并用 WrapFactory 等机制在 Java <-> JS 链路做可控转换或包装.
看懂了,感谢作者的慷慨指导。作者好厉害,平时是怎么学习的,直接读源码对我来说还是太困难了,numberToString 只能读懂到 if (d == 0.0) return "0" 这一行,DToA.JS_dtobasestr(base, d) 点进去勉强读懂,FastDtoa.numberToString(d)层层封装已经读不懂了(也不知道 按ECMAScript 规范生成 "最短且可还原" 的十进制字符串)
源码其实了解一下就可以, 尤其是 DTOA 本身就偏学术.
可以通过 Rhino 源码逐步找到自己可以了解的内容.
先从 numberToString 入手, 它是 Context.toString 方法传入一个可被视为 JavaScript number 的参数时被方法内部调用的核心代码.
Rhino 的 Context.toXxx 方法是按 ECAMScript 标准进行编写的, 因此 numberToString 也一定离不开 ES 标准.
于是可以了解以下两个与 "数字转换为十进制字符串" 相关的文档: https://tc39.es/ecma262/#sec-tostring-applied-to-the-number-type https://tc39.es/ecma262/#sec-number.prototype.tostring
接下来进入 numberToString 方法, 看到有 DToA 和 FastDtoa 两个关键类, 还有注释内容可以作为线索:
// V8 FastDtoa can't convert all numbers, so try it first but
// fall back to old DToA in case it fails
可以体会到 FastDtoa (Google V8 使用) 与 DToA 在性能与正确性折中思想在此处的应用.
分别进入两个类, 从文件开头注释部分可以分别了解到下面两个内容: http://www.netlib.org/fp/dtoa.c (David M. Gay) https://github.com/google/double-conversion/blob/master/double-conversion/fast-dtoa.h (Google Double Conversion)
术语 "Double conversion round-trip" (双重转换往返) 指的是先将双精度浮点数转换为字符串表示, 再把该字符串转换回双精度浮点数的过程. 其中 "round-trip" 的含义是最终得到的 double 值应与原始值完全相同, 也就是说在字符串转换与解析过程中不应出现任何精度损失.
最后, 之前提到的 "最短且可还原", 包含以下两个核心思想:
- 得到的十进制字符串应当 "最短" (shortest)
- 该字符串再解析回二进制浮点时应当得到 "原始值" (round-trip)
OK
关于java的类型转换,这里说一下我的看法 。 首先看一下这个示例
let d = new JavaAdapter(
java.io.File,
{
test(n) {
console.log('n',typeof n);//boolean
return true;
},
},
"/j",
);
console.log(typeof d.test(true)); //object
console.log(typeof d.isFile()); //boolean
这里动态生成了一个java实例,可以看到同样是一个java对象,isFile得到一个js基本类型,动态生成的test方法却得到一个java包装类型,原因在于isFile的return签名是boolean(java基本类型),而动态生成的test方法不确定反回值,return签名是Object,因此java将装箱为一个包装类。
我认为在JavaScript脚本环境中,应尽可能的返回JavaScript类型,并且无论在java或是JavaScript中,包装类型都是不建议使用的。
将isJavaPrimitiveWrap设置为false确实会影响使用valueOf获取包装类型的代码,但大多数情况下,使用js基本类型和包装类型行为基本一致,除了少数有多个重载类型的java方法。
比起像上面示例带来差异,将所有java包装类型转为js类型或许更好,并不是所有人都能区分这其中的隐式转换
我也支持isJavaPrimitiveWrap设置为false的做法,从开发的角度思考,当我在js里使用java包装类的时候,只有两种情况,一是使用类里的方法,二是传参,在使用valueOf之类的方法创建包装类时,由于isJavaPrimitiveWrap设置为false,实际仍为js类型,运行脚本会报错没有这个方法,这时我会立刻知道包装类没创建成功,从而尝试使用new之类的方式创建,而原来的隐形转换不仅反常识(输入一个js基本类型得到一个java对象)还有隐形的坑,如开头的示例,let b = a.blockedGet() 如果是一个false的java 包装类,在js 用if (b)判断恒为真,开发者很难会这样写 if (b===true),直接打印b也是false,开始怀疑人生。原作者单独对 String 做了转换,猜测是因为string高频使用,不然js里大把java.lang.String难以理解。
欢迎 A 大参与讨论. :heart:
针对 A 大提供的例子, 我做了以下测试:
let brief = (o) => log([o.toString(), typeof o, species(o), o.class?.name].filter(Boolean).join(" | "));
let f = new File(".");
brief(f.isDirectory());
let jaf = new JavaAdapter(File, {test: (n) => (brief(n), 2)}, ".");
brief(jaf.test(1));
brief(jaf.isDirectory());
let s = new java.util.function.Supplier({get: () => 3});
brief(s.get());
let jas = new JavaAdapter(java.util.function.Supplier, {get: () => 3});
brief(jas.get());
let c = new java.util.function.Consumer({accept: brief});
c.accept(4);
let jac = new JavaAdapter(java.util.function.Consumer, {accept: brief});
jac.accept(4);
结果:
--------------------------------------------
isJavaPrimitiveWrap: true
--------------------------------------------
true | boolean | Boolean
1.0 | object | JavaObject | java.lang.Double
2.0 | object | JavaObject | java.lang.Double
true | boolean | Boolean
3.0 | object | JavaObject | java.lang.Double
3.0 | object | JavaObject | java.lang.Double
4 | number | Number
4.0 | object | JavaObject | java.lang.Double
--------------------------------------------
isJavaPrimitiveWrap: false
--------------------------------------------
true | boolean | Boolean
1 | number | Number
2 | number | Number
true | boolean | Boolean
3 | number | Number
3 | number | Number
4 | number | Number
4 | number | Number
--------------------------------------------
isJavaPrimitiveWrap 设为 false 时, 每次看到这样的结果都会心动.
只是还是会考虑到以下几个方面:
- Auto.js 原始代码在
WrapFactory#wrap中已对 String 做了 JavaScript 原始化处理, 但对 Number/Boolean/Character 没有做. 如果isJavaPrimitiveWrap设为false, 相当于 String 的原始化强调将不再有意义. java.lang.Boolean.TRUE及java.lang.Boolean.valueOf(true)等应该与 JavaScript 的true有所区分obj instanceof java.lang.Boolean这样的判断希望不受影响- 当明确进入 Java 世界时, 脚本开发者可以习惯性产生 "装箱/拆箱" 的意识
我在想如果以 "不改变 isJavaPrimitiveWrap 原有的值" 为前提, 是否可以尽最大可能将这样的差异缩小, 比如:
JavaAdapter相对特殊, 可以尝试以JavaAdapter为基础创建一个新的方法 (如JavaAdapterNative等), 其内部自动将参数和返回值进行一次 unwrap, 保证内部与返回值都符合 JavaScript 原始值, 同时不破坏原有JavaAdapter的功能特性- 对于内置全局模块, 类似
threads.disposable的处理方式, 尽量暴露 JavaScript 方法 - 需要时, 脚本开发者可以使用
util.unwrapJavaObject方法将 JavaObject 参数转换回 JavaScript 原始值 - ... ...
@vtrayy @aiselp
我在想是否可以在 Auto.js 应用的设置页面增加一个 isJavaPrimitiveWrap 相关的选项开关, 同时支持脚本控制 (如 runtime.setJavaPrimitiveWrap(false)), 优先级大于设置页面的开关. 这样的话开发者可以根据偏好自行控制原始值包装策略.
就是String 做了 JavaScript 原始化处理才加深了思维惯性,作为js开发者的时候,传入js对象得到js对象符合逻辑,传入string得到的也是string也符合逻辑,这两个都是高频使用的类型,当有一天传入别的js基本类型根本不会意识到问题,即使有一定的java基础也不会想到装箱、拆箱,因为对Rhino的运行并不了解以及之前的思维惯性只会认为传入什么得到什么。
@vtrayy @aiselp 我在想是否可以在 Auto.js 应用的设置页面增加一个
isJavaPrimitiveWrap相关的选项开关, 同时支持脚本控制 (如runtime.setJavaPrimitiveWrap(false)), 优先级大于设置页面的开关. 这样的话开发者可以根据偏好自行控制原始值包装策略.
这个可以
String的特殊处理大概只是之前开发者不了解WrapFactory吧,因为String使用频率较高只处理了这一种类型,实际上我也是最近才发现isJavaPrimitiveWrap这个属性。
对于obj instanceof java.lang.Boolean是有破坏语义,但这就像在js中使用
obj instanceof Boolean,用instanceof运算符判断基本类型本身不是一个可行的办法。
也可以说正是因为存在java包装类型混乱的问题,才会使用这种判断写法
也许增加一个 isJavaPrimitiveWrap 的设置是最优选择