JavaAdapter 接口适配
JavaAdapter 接口适配
JavaAdapter 是 ScriptX 现在补进来的 Rhino 兼容接口适配能力。
它的核心作用很直白:
- Java / Kotlin 那边要求你传一个接口实例
- 你又不想单独写一份 Java 类
- 那就在脚本里直接临时实现这个接口
如果你已经熟悉 createProxy(...),那可以先把它理解成:
createProxy(...)是更底层、更明确的接口代理入口JavaAdapter(...)是更接近 Rhino / Auto.js 老习惯的兼容写法- 这两套底层最终走的是同一条代理分发链路
先记住 10 条:
JavaAdapter当前只支持接口,不支持普通类继承。new JavaAdapter(...)和JavaAdapter(...)都能用。- 也支持
JavaAdapter.call(...)、JavaAdapter.apply(...)这种函数式写法。 - 最后一个参数必须是回调函数,或者按方法名分发的 callbacks 对象。
- 前面的类型参数可以传多个,也就是“一次实现多个接口”。
- 传宿主 App 自己的接口时,优先先用
findClass(name, lpparam.classLoader)取到Class再传给它。 JavaAdapter本身没有单独的 classLoader 参数。- 如果你必须显式指定 loader,优先考虑
createProxy(..., ..., classLoader)。 - Kotlin
FunctionN、Continuation这类接口现在也能接。 - 它不是 Android/ART 上的“动态继承普通 Java 类”,那条路当前明确不支持。
JavaAdapter(type[, type...], callbacks) / new JavaAdapter(type[, type...], callbacks)
这是最核心的入口。
支持的调用形式
new JavaAdapter(type[, type...], callbacks)
JavaAdapter(type[, type...], callbacks)
JavaAdapter.call(null, type[, type...], callbacks)
JavaAdapter.apply(null, [type[, type...], callbacks])
参数规则
| 参数 | 类型 | 必填 | 可填值 | 说明 |
|---|---|---|---|---|
type | string / Class / 类代理 | 是 | 一个或多个接口类型 | 要实现的接口 |
callbacks | Function / object | 是 | 单函数,或按方法名分发的对象 | JS 回调实现 |
最常见例子:单接口 + 单函数
const runnable = new JavaAdapter("java.lang.Runnable", function () {
log("run");
});
callMethod(runnable, "run");
最常见例子:单接口 + callbacks 对象
const runnable = new JavaAdapter("java.lang.Runnable", {
run() {
log("run from object mode");
}
});
它和 createProxy(...) 最本质的差别
| 场景 | 更适合谁 |
|---|---|
你已经习惯 Rhino / Auto.js 风格,想直接写 new JavaAdapter(...) | JavaAdapter |
你要显式传 classLoader | createProxy(...) |
| 你想明确表达“这就是接口代理,不是兼容层” | createProxy(...) |
createProxy(interfaces, callbackOrCallbacks, classLoader?)
这页也把它一起讲,是因为两者底层共享:
- 接口解析
- 回调查找
- JS -> Java 返回值转换
- 暂停 / 停止时的行为
toString / equals / hashCode处理
什么时候反而优先用它
如果你现在手上的场景是:
- 接口类来自宿主 App
- 你必须指定
lpparam.classLoader - 你想把“接口列表”和“回调对象”写得更显式
那就优先:
const listener = createProxy(
"com.example.app.DownloadListener",
{
onSuccess(path) {
log(path);
},
onFailure(code, message) {
log(code + ": " + message);
}
},
lpparam.classLoader
);
这一页重点讲 JavaAdapter,createProxy(...) 更完整的逐项说明还在 反射与 Java 类操作。
JavaAdapter.name / JavaAdapter.length / JavaAdapter.arity
当前兼容层对外暴露的函数元信息是固定的。
log(JavaAdapter.name); // JavaAdapter
log(JavaAdapter.length); // 1
log(JavaAdapter.arity); // 1
这组信息平时不常用,但说明它在脚本层确实被当作一个普通 JS 函数来挂载。
type 能传哪些形式
JavaAdapter 和 createProxy(...) 一样,当前支持下面这些类型写法:
| 写法 | 是否支持 | 说明 |
|---|---|---|
"java.lang.Runnable" | 支持 | 完整类名字符串 |
java.lang.Runnable | 支持 | Rhino 风格类对象 |
java.lang.Class.forName("java.lang.Runnable") | 支持 | 真实 Class |
imports("java.lang.Runnable") 返回的类代理 | 支持 | 会被解包成真实 Class |
findClass("java.lang.Runnable") 的返回值 | 支持 | 直接传 Class |
例子:传字符串
const runnable = new JavaAdapter("java.lang.Runnable", function () {
log("run");
});
例子:传 findClass(...)
const ListenerClass = findClass(
"com.example.app.DownloadListener",
lpparam.classLoader
);
const listener = new JavaAdapter(ListenerClass, {
onSuccess(path) {
log(path);
}
});
例子:传导入后的类代理
const Runnable = imports("java.lang.Runnable");
const runnable = new JavaAdapter(Runnable, function () {
log("run from imported class proxy");
});
宿主类、插件类优先怎么写
这是新手最容易踩坑的点之一。
先说结论
如果接口来自:
- 目标 App
- 插件 dex
- 动态加载 dex
那最稳的写法不是直接传字符串,而是:
- 先
findClass(name, lpparam.classLoader) - 再把拿到的
Class传给JavaAdapter
const AppCallback = findClass(
"com.example.target.Callback",
lpparam.classLoader
);
const callback = new JavaAdapter(AppCallback, {
onValue(value) {
log(String(value));
}
});
为什么
因为 JavaAdapter 自己没有第三个 classLoader 参数。
它解析字符串类型时,会按当前桥接层的解析逻辑去找类。对于系统类通常没问题,但到了宿主自己的类,最稳妥的办法仍然是你自己先把 Class 找准。
第三方包和 Kotlin 包怎么写更稳
不要指望裸名字总能自动认出来。
下面这些写法更稳:
Packages.kotlin.jvm.functions.Function1
Packages.kotlin.coroutines.Continuation
Packages.okhttp3.Callback
或者:
"kotlin.jvm.functions.Function1"
"kotlin.coroutines.Continuation"
"okhttp3.Callback"
或者:
imports("kotlin.jvm.functions.Function1")
findClass("okhttp3.Callback", lpparam.classLoader)
例子:OkHttp Callback
const callback = new JavaAdapter(Packages.okhttp3.Callback, {
onFailure(call, error) {
log(error.getMessage());
},
onResponse(call, response) {
log("HTTP " + response.code);
response.close();
}
});
一次实现多个接口
前面可以连续传多个类型,最后一个参数仍然是回调实现。
例子:Runnable + Callable
const combined = new JavaAdapter(
"java.lang.Runnable",
"java.util.concurrent.Callable",
{
run() {
log("run()");
},
call() {
return "result";
}
}
);
callMethod(combined, "run");
log(callMethod(combined, "call"));
多接口时你要自己注意什么
如果多个接口里恰好有同名方法,那最终还是按方法名分发。
也就是说:
- 同名方法会共用同一个 JS 回调
- 你需要自己在回调里处理参数差异和语义差异
第二参数两种模式:函数模式和对象模式
函数模式
如果最后一个参数本身就是函数,那所有接口方法都会走这一份函数。
const runnable = new JavaAdapter("java.lang.Runnable", function () {
log("task running");
});
适合:
- 只有一个抽象方法的接口
- 你根本不打算按方法名拆逻辑
对象模式
如果最后一个参数是对象,就按方法名匹配同名函数。
const comparator = new JavaAdapter("java.util.Comparator", {
compare(left, right) {
return String(left).length - String(right).length;
}
});
找不到同名回调会怎样
不会静默跳过。
当前实现会抛出类似下面这种错误:
JavaAdapter missing callback for java.lang.Runnable#run()
所以对象模式下,不要只传空对象:
new JavaAdapter("java.lang.Runnable", {});
这种写法一旦真的有人去调 run(),就会直接炸。
回调里的 this 是谁
对象模式
this 就是你传进去的 callbacks 对象本身。
const runnable = new JavaAdapter("java.lang.Runnable", {
count: 0,
run() {
this.count += 1;
log("count=" + this.count);
}
});
函数模式
函数模式里,this 会落到当前脚本作用域,不是一个专门为代理创建的上下文对象。
所以如果你要在函数模式里记状态,优先用闭包:
let count = 0;
const runnable = new JavaAdapter("java.lang.Runnable", function () {
count += 1;
log("count=" + count);
});
回调收到的参数和返回值怎么处理
参数
Java 侧传进来的参数会按普通 ScriptX Java 桥规则包装成脚本可用值:
- Java 对象 -> 可继续调用的 Java 包装对象
null->null- 基本类型 / 字符串 -> 对应脚本值
返回值
返回值会按接口方法声明的 Java 返回类型去做收敛。
void
如果 Java 方法返回 void,你的 JS 回调返回什么都不会真的向外继续传。
const runnable = new JavaAdapter("java.lang.Runnable", {
run() {
return 123;
}
});
这里最终仍然按 void 处理。
基本类型
如果你忘了返回,或者结果不能正确收敛,当前实现会给基本类型一个默认值。
例如:
const comparator = new JavaAdapter("java.util.Comparator", {
compare(a, b) {
log("compare " + a + " vs " + b);
// 忘了 return
}
});
因为 compare(...) 返回 int,最终会被兜底成 0。
对象返回
如果方法返回对象类型:
null可以- 本来就是目标类型实例也可以
- 如果不能赋值到目标类型,会报“返回值不可赋给目标类型”的错误
一个容易忽略的特殊点
普通 Java 接口里,隐式 undefined 对对象返回值通常会按 null 处理。
但 Kotlin FunctionN 场景下,隐式 undefined 会被保留给 Unit.INSTANCE 这个特殊语义,下面单独说。
Kotlin FunctionN
现在可以直接实现 Kotlin 函数式接口。
例子:Function1
const Function1 = Packages.kotlin.jvm.functions.Function1;
const uppercase = new JavaAdapter(Function1, function (value) {
return String(value).toUpperCase();
});
log(callMethod(uppercase, "invoke", "hello"));
undefined 和 Unit
如果你实现的是 Kotlin FunctionN.invoke(...),并且 JS 回调没有显式返回值,那么当前实现会把这个“隐式 undefined”按 Kotlin Unit.INSTANCE 去处理。
const Function1 = Packages.kotlin.jvm.functions.Function1;
const unitFunction = new JavaAdapter(Function1, function (value) {
log(String(value));
// 没有 return
});
这里和普通 Java Callable<Object> 不一样:
- Kotlin
FunctionN的隐式空返回会走Unit - 普通 Java 对象返回类型的隐式空返回更接近
null
Kotlin Continuation
kotlin.coroutines.Continuation 也可以直接实现。
例子
const continuation = new JavaAdapter(
"kotlin.coroutines.Continuation",
{
resumeWith(result) {
log(String(result));
}
}
);
getContext() 没写会怎样
这是一个源码里已经补好的特殊行为。
如果你的 callbacks 对象没有写 getContext(),当前实现会自动给它返回:
kotlin.coroutines.EmptyCoroutineContext.INSTANCE
所以很多只关心 resumeWith(...) 的场景,不需要你再手动补一个空 context。
失败结果怎么传
如果你自己要从 JS 里主动调 resumeWith(...) 并构造失败值,需要用 Kotlin 的 ABI helper:
const failure = Packages.kotlin.ResultKt.createFailure(
new java.io.IOException("failed")
);
callMethod(continuation, "resumeWith", failure);
toString() / equals() / hashCode()
这一组现在不会再去找你的 callbacks,而是桥接层自己处理。
toString()
会返回带接口列表的稳定描述字符串。
equals(other)
走引用相等,不是业务语义上的“内容相等”。
hashCode()
走 Java identity hash。
这意味着什么
如果你在 callbacks 对象里手写:
toString() {}
equals() {}
hashCode() {}
也不要指望 Java 世界一定会调到它们;当前这 3 个基础对象方法是内部接管的。
暂停、停止、并发时会怎样
这部分是老手最关心、但很多文档经常一句带过的地方。
先说结论
当前所有回调执行都会经过同一个 Rhino 执行事务,所以:
- 不同 Java 线程同时回调进来时,会串行进入 JS
- 一个回调没跑完,另一个线程可能要等
暂停时
- 后台线程回调:会等待脚本恢复
- 主线程回调:不会一直卡着等,避免 UI 死锁 / ANR,而是尽快返回默认值
停止时
- 已停止的运行时不会再继续认真执行这些外部代理回调
- 这时再调代理,通常只会拿到 Java 默认返回值
一个很实际的提醒
创建了 JavaAdapter,并不等于“自动把脚本生命周期永久保活”。
如果外部异步系统长时间持有这个代理,你还是要自己处理好对应脚本的生命周期和持有关系。
失败行为和错误长什么样
这块当前实现做得比很多同类桥接更明确。
常见报错来源
- 传进去的类型不是可解析的 Java 类
- callbacks 参数不是函数 / 对象
- 多个父类混着传
- 某个方法被调到时没有同名回调
- 回调自己抛错
- 返回值不能转换成目标 Java 返回类型
典型错误信息会带什么
- 参数索引
- 类型名
- 接口名
- 方法完整签名
- classLoader 相关上下文
这意味着你排错时不要只看“炸了没”,要看它指的是:
- 哪个参数错了
- 哪个接口方法没实现
- 还是哪个返回值对不上
Android 上为什么不支持继承普通类
这是最容易被旧 Rhino / Auto.js 记忆误导的一点。
先说结论
下面这种想法当前不支持:
new JavaAdapter("java.lang.Thread", {
run() {
log("x");
}
});
为什么
因为 Android/ART 这边不能像桌面 Rhino 那样,随手把普通 Java 类动态生成为可加载的 JVM .class 子类。
当前实现明确只支持接口代理。
允许的“冗余父类”情况
如果你传的是:
new JavaAdapter(
java.lang.Object,
java.lang.Runnable,
function () {}
);
这一类“Object + 接口”组合会被归一化成普通接口代理。
但真正的具体类、抽象类、需要构造参数的超类适配,当前都不支持。
什么时候优先用 JavaAdapter
- 你正在迁移旧 Rhino / Auto.js 风格脚本
- 你就是想就地实现一个接口
- 你需要
Function1、Runnable、Comparator、Callback这类接口实例
什么时候优先用 createProxy(...)
- 你必须显式传
classLoader - 你想把“接口列表”和“回调对象”写得更清楚
- 你在排宿主接口、插件接口、动态 dex 接口的问题
一个完整例子:主线程 Runnable
const HandlerClass = findClass("android.os.Handler");
const LooperClass = findClass("android.os.Looper");
const mainLooper = callStaticMethod(LooperClass, "getMainLooper");
const handler = this["new"](HandlerClass, mainLooper);
const runnable = new JavaAdapter("java.lang.Runnable", function () {
log("posted on main looper");
});
callMethod(handler, "post", runnable);
一个完整例子:宿主接口监听器
const DownloadListener = findClass(
"com.example.target.DownloadListener",
lpparam.classLoader
);
const listener = new JavaAdapter(DownloadListener, {
onStart(url) {
log("start => " + url);
},
onSuccess(path) {
log("success => " + path);
},
onFailure(code, message) {
log("failure => " + code + ", " + message);
}
});
// 后面再把 listener 传给目标 Java API
