反射与 Java 类操作
反射与 Java 类操作
反射这块是 JSXHook 里最容易把人卡住的一节,因为它同时牵扯 4 件事:
- 你要先拿到对的
Class - 你要分清自己现在调的是静态还是实例
- 你要知道参数到底要传什么类型
- 一旦遇到重载、私有字段、隐藏构造器,就不能只靠“猜”
所以这页我不打算只把 API 列一遍,而是按真正写脚本时的思路,把它拆成“怎么找类”“怎么调方法”“怎么读写字段”“怎么构造对象”“什么时候该下沉到原生反射”几块来讲。
先建立脑图
你可以先把 JSXHook 里的反射相关能力分成 3 套写法:
| 写法 | 典型入口 | 适合场景 | 优点 | 容易踩的坑 |
|---|---|---|---|---|
| 类代理写法 | imports() / importClass() | 同一个类要反复用 | 写起来最顺手,像普通 JS 一样 | 重载复杂时不够精确 |
| 辅助函数写法 | findClass() / callMethod() / getField() | 日常 Hook 最常见 | 比原生反射短很多 | 参数类型匹配比你想象中更严格 |
| 原生反射写法 | getDeclaredMethod() / getDeclaredField() / getDeclaredConstructor() | 重载很多、签名必须精确、要动私有成员 | 最稳、最可控 | 写法更长,需要你自己管好类型 |
如果你只先记住一句话:
- 正常开发先用
imports/findClass+ 辅助函数 - 一旦遇到重载、私有成员、签名不准,立刻改走原生反射
先分清几组 API
找类
imports(className)importClass(className)findClass(nameOrClass, classLoader?)
调方法
callStaticMethod(clazz, methodName, ...args)callMethod(instance, methodName, ...args)invoke(target, methodName, ...args)
读写字段
getField(obj, fieldName)setField(obj, fieldName, value)getStaticField(clazzOrName, fieldName)setStaticField(clazzOrName, fieldName, value)printFields(target, separator?)
构造对象和数组
new 导入后的类(...)this["new"](clazzOrName, ...args)this["new"](className, classLoader, ...args):第一参数是字符串类名时可显式指定 loadernewArray(typeName, size)ByteArray(size)
一眼分清 3 个“类”的来源
这是整个反射章节最基础、也最容易混的地方。
1. imports() / importClass() 返回的是“类代理”
你可以把它理解成“脚本里可直接拿来用的 Java 类门面”。
它最适合你后面要反复写这个类的场景。
const ArrayList = imports("java.util.ArrayList");
const Toast = importClass("android.widget.Toast");
const list = new ArrayList();
callMethod(list, "add", "jsxhook");
类代理最顺手的地方在于:
- 可以直接
new ArrayList() - 可以直接调静态方法,比如
Toast.makeText(...) - 可以直接读静态字段,比如
BuildVersion.SDK_INT
2. findClass() 返回的是真正的 Java Class
它更像“精准定位后的原始类对象”。
适合这几类情况:
- 目标类来自宿主 App
- 你要显式传
classLoader - 你后面要拿
getDeclaredMethod()/getDeclaredField()/getDeclaredConstructor()
const ProfileManagerClass = findClass(
"com.example.target.ProfileManager",
lpparam.classLoader
);
3. 字符串类名只是“待解析名字”
很多 API 第一参数既支持类代理,也支持 Class,还支持字符串类名:
getStaticField("android.os.Build$VERSION", "SDK_INT");
但不要因为“字符串能用”就一路都传字符串。
一旦是宿主类、插件类、动态 dex 里的类,优先先 findClass()。
imports(className) / importClass(className)
这两个名字在当前实现里本质上是一回事,importClass 就是 imports 的别名。
单类导入
const ArrayList = imports("java.util.ArrayList");
const Toast = importClass("android.widget.Toast");
const BuildVersion = importClass("android.os.Build$VERSION");
返回值
- 单类导入时,返回值是一个类代理
- 这个类代理既能构造对象,也能访问静态方法 / 静态字段
最常见用法:导入后反复使用
const ArrayList = imports("java.util.ArrayList");
const list = new ArrayList();
list.add("first");
list.add("second");
log(`size=${list.size()}`);
这段代码能这样写,是因为类代理支持:
new ArrayList()构造实例list.add()调实例方法list.size()调实例方法
用类代理直接调静态方法
const ActivityThread = importClass("android.app.ActivityThread");
const application = ActivityThread.currentApplication();
log(`application=${application}`);
用类代理直接读静态字段
const BuildVersion = importClass("android.os.Build$VERSION");
log(`sdk=${BuildVersion.SDK_INT}`);
log(`release=${BuildVersion.RELEASE}`);
用类代理直接改静态字段
const DebugFlags = importClass("com.example.target.DebugFlags");
DebugFlags.ENABLE_LOG = true;
log(`ENABLE_LOG=${DebugFlags.ENABLE_LOG}`);
这其实等价于:
setStaticField("com.example.target.DebugFlags", "ENABLE_LOG", true);
通配导入
imports("java.util.*");
const list = new ArrayList();
const map = new HashMap();
list.add("demo");
map.put("lang", getAppLanguage());
log(`size=${list.size()}`);
log(`lang=${map.get("lang")}`);
通配导入的真实规则
- 只导入这个包下的直接类
- 不会递归导入子包
- 返回值是成功导入的类数量
也就是说:
const count = imports("java.util.*");
log(`count=${count}`);
这里的 count 是数字,不是类代理。
什么情况下优先用 imports
这几种情况最适合:
- 同一个类要用很多次
- 你想让脚本更短、更接近日常 Java 调用风格
- 这个类本身就不依赖复杂
classLoader
什么情况下别只靠 imports
这几种情况更建议转去 findClass():
- 类来自宿主 App 或插件 dex
- 你后面要拿精确构造器 / 精确方法
- 你要查私有字段、私有方法、隐藏成员
findClass(nameOrClass, classLoader?)
这是“我就是要找到这个类本体”时最重要的 API。
参数
| 参数 | 类型 | 必填 | 可填值 | 说明 |
|---|---|---|---|---|
nameOrClass | string / Class | 是 | 完整类名,或已经拿到的 Java Class | 传字符串时做类查找;传 Class 时直接原样返回 |
classLoader | ClassLoader | 否 | 常见是 lpparam.classLoader | 宿主类、插件类建议显式传 |
它现在不只是“查字符串类名”
当前实现里,findClass(...) 可以吃两类输入:
- 字符串类名
- 已经拿到的 Java
Class对象
如果第一参数本来就是 Class,它不会再重复查找,而是直接把这个 Class 返回给你。
这个行为很适合做“统一入口”或“归一化参数”。
最基础写法:传字符串
const ActivityThread = findClass("android.app.ActivityThread");
传已拿到的 Class:直接原样返回
const FileClass = findClass("java.io.File", lpparam.classLoader);
const SameFileClass = findClass(FileClass);
log(FileClass === SameFileClass);
你可以把它理解成:
- 传字符串:
帮我找类 - 传
Class:帮我确认并原样拿回来
查宿主自己的类
const TargetClass = findClass(
"com.example.target.ProfileManager",
lpparam.classLoader
);
在 Hook 回调里继续顺着当前对象的加载器查类
hook({
class: "com.example.target.EntryActivity",
classloader: lpparam.classLoader,
method: "onCreate",
params: ["android.os.Bundle"],
after(it) {
const loader = it.thisObject.getClass().getClassLoader();
const ProfileClass = findClass("com.example.target.Profile", loader);
log(`profileClass=${ProfileClass}`);
}
});
写工具函数时很有用:统一吃字符串或 Class
这个能力最适合的一个场景,就是你自己封装工具函数时不想强迫调用方只能传字符串。
function ensureClass(nameOrClass, loader) {
return findClass(nameOrClass, loader);
}
const FileClass1 = ensureClass("java.io.File", lpparam.classLoader);
const FileClass2 = ensureClass(FileClass1, lpparam.classLoader);
log(FileClass1 === FileClass2);
这样调用方就可以:
- 还没查类时传字符串
- 已经查过类时直接传
Class
你这边不需要分两套逻辑。
不传 classLoader 时,内部大概会怎么找
当前实现不是只死盯一个加载器,而是会按下面思路尝试:
- 你显式传入的
preferredLoader lpparam.classLoader- 已加载的 dex loaders
- host loader
Class.forName(...)
所以:
- 系统类很多时候不传也能找到
- 宿主类有时不传也能撞到
- 第一参数如果已经是
Class,这里这套查找流程就不会再跑 - 但不要把“偶尔能找到”误当成稳定行为
什么时候必须显式传 lpparam.classLoader
这几种情况都建议你别偷懒:
- 目标类是宿主 App 自己的业务类
- 目标类来自被你
loadDex()过的 dex - 你已经见过
Class not found - 同名类可能在多个加载链里同时存在
查类之后马上做静态调用
const ActivityThread = findClass(
"android.app.ActivityThread",
lpparam.classLoader
);
const app = callStaticMethod(ActivityThread, "currentApplication");
log(`app=${app}`);
查类之后马上下沉到原生反射
const ActivityThread = findClass(
"android.app.ActivityThread",
lpparam.classLoader
);
const method = ActivityThread.getDeclaredMethod("currentApplication");
method.setAccessible(true);
const app = method.invoke(null);
log(`app=${app}`);
callStaticMethod(clazz, methodName, ...args)
这个是“已知静态方法,直接调”的辅助函数。
第一参数能传什么
| 写法 | 例子 |
|---|---|
| 类名字符串 | "android.os.Build$VERSION" |
Class | findClass("android.app.ActivityThread", lpparam.classLoader) |
| 类代理 | importClass("android.text.TextUtils") |
调无参静态方法
const ActivityThread = importClass("android.app.ActivityThread");
const application = callStaticMethod(ActivityThread, "currentApplication");
log(`application=${application}`);
调带参静态方法
const TextUtils = importClass("android.text.TextUtils");
log(callStaticMethod(TextUtils, "isEmpty", ""));
log(callStaticMethod(TextUtils, "isEmpty", "hello"));
直接传字符串类名
const sdk = getStaticField("android.os.Build$VERSION", "SDK_INT");
log(`sdk=${sdk}`);
虽然这里演示的是 getStaticField(),但第一参数支持规则和静态调用是一类思路。
callMethod(instance, methodName, ...args)
这个是“我手里已经有对象了,现在要调它的方法”。
最基础例子
const ArrayList = imports("java.util.ArrayList");
const list = new ArrayList();
callMethod(list, "add", "first");
callMethod(list, "add", "second");
log(`size=${callMethod(list, "size")}`);
log(`first=${callMethod(list, "get", 0)}`);
Hook 回调里调 thisObject
hook({
class: "com.example.target.ProfileManager",
classloader: lpparam.classLoader,
method: "loadProfile",
after(it) {
const profile = callMethod(it.thisObject, "getCurrentProfile");
const name = callMethod(profile, "getName");
log(`name=${name}`);
}
});
一个很实用的小细节:直接传 it
在当前实现里,callMethod() 的第一个参数如果是 XC_MethodHook.MethodHookParam,内部会自动取它的 thisObject。
所以这类写法也能工作:
callMethod(it, "finish");
不过日常更推荐你写成:
callMethod(it.thisObject, "finish");
因为可读性更高,别人一眼能看懂你到底在调谁。
invoke(target, methodName, ...args)
它是一个“通用入口”:
- 第一参数如果是类 -> 走静态调用
- 第一参数如果是对象 -> 走实例调用
传类,走静态
const ActivityThread = importClass("android.app.ActivityThread");
const application = invoke(ActivityThread, "currentApplication");
log(`application=${application}`);
传对象,走实例
const list = new (imports("java.util.ArrayList"))();
invoke(list, "add", "jsxhook");
invoke(list, "add", "reflection");
log(`size=${invoke(list, "size")}`);
适合什么时候用
适合封装通用工具时:
function safeInvoke(target, methodName, ...args) {
try {
return invoke(target, methodName, ...args);
} catch (error) {
log(`invoke failed: ${error}`);
return null;
}
}
什么时候不如分开写
如果你已经明确知道:
- 这就是静态方法
- 这就是实例方法
那直接写 callStaticMethod() / callMethod() 更清楚。
methodName 现在不一定非得手写成普通 JS 字符串
当前桥接层在处理 callStaticMethod()、callMethod()、invoke() 的 methodName 时,会先做一次“解包 -> 转字符串”。
这意味着下面几类值现在都更稳:
- 普通 JS 字符串:
"login" - Rhino 包装后的字符串值
- Java 侧对象上返回的字符串类结果
- 例如 DexKit / 反射结果里的
matches[0].name、methodData.name
const methodData = matches[0];
callMethod(target, methodData.name);
callStaticMethod(TargetClass, methodData.name, arg0);
invoke(TargetClass, methodData.name);
如果方法名本来就是固定的,还是推荐你直接写字面量字符串,因为这样最清楚:
callMethod(target, "login");
DexKit 结果里的 isXxx 现在也能直接当布尔属性读
这条虽然不是 callMethod() 本身的参数规则,但和实际联动非常强,值得放在这里一起记。
像下面这些 Kotlin 布尔属性:
MethodData.isConstructorMethodData.isStaticInitializerMethodData.isMethodClassData.isArray
现在在 JSXHook 里直接读就是 true / false,可以直接这样判断:
const hit = methodHits.firstOrNull();
if (hit && hit.isMethod) {
callMethod(target, hit.name);
}
注意这里应该写:
hit.isMethod
而不是:
hit.isMethod()
这一组最容易踩坑:callMethod / callStaticMethod / invoke 的参数匹配并不宽松
这是整页里最值得你记住的一个点。
上面那层“解包再转字符串”的兼容,只针对 方法名这个位置。
真正传给 Java 方法的 ...args 仍然按原来的类型匹配规则处理,不会因为 methodName 更宽松了,就顺手帮你把 "1" 变成 int、把 "true" 变成 boolean。
当前实现的匹配规则,尽量按这 4 条理解
- 参数个数必须对上
null不能传给基本类型参数- 基本类型主要接受对应包装类型
- 不会帮你自动把字符串
"1"当成int,也不会自动把"true"当成boolean
错误例子:你以为能自动转,其实不一定
// 如果 Java 方法签名是 setLevel(int)
callMethod(target, "setLevel", "1");
这类写法很容易匹配失败。
正确例子:直接传对的 JS 类型
callMethod(target, "setLevel", 1);
callMethod(target, "setVip", true);
callMethod(target, "setRatio", 0.75);
再举一个容易忽略的例子
// 假设 Java 签名是 setEnabled(boolean)
callMethod(target, "setEnabled", "true"); // 容易失败
callMethod(target, "setEnabled", true); // 更稳
看到重载多的时候,不要死猜
比如同名方法很多、而且参数类型接近时,最稳的做法不是一遍遍换参数试,而是直接拿精确 Method:
const StringBuilderClass = findClass("java.lang.StringBuilder");
const IntegerClass = importClass("java.lang.Integer");
const appendInt = StringBuilderClass.getDeclaredMethod(
"append",
IntegerClass.TYPE
);
appendInt.setAccessible(true);
const sb = this["new"](StringBuilderClass);
appendInt.invoke(sb, 123);
log(sb.toString());
getField(obj, fieldName) / setField(obj, fieldName, value)
这组 API 是读写实例字段的。
先记住它的真实行为
- 能找私有字段
- 会沿继承链往父类找
- 第一参数如果传的是
it,会自动取it.thisObject fieldName会先做一次“解包 -> 转字符串”,静态字段版本也是同一套规则
读取实例字段
const profile = callMethod(manager, "getCurrentProfile");
const name = getField(profile, "name");
log(`name=${name}`);
修改实例字段
setField(profile, "vip", true);
setField(profile, "loginCount", 99);
在 Hook 里直接改参数对象
hook({
class: "com.example.target.ProfileManager",
classloader: lpparam.classLoader,
method: "updateProfile",
params: ["com.example.target.Profile"],
before(it) {
const profile = it.args[0];
log(`before.name=${getField(profile, "name")}`);
log(`before.vip=${getField(profile, "vip")}`);
setField(profile, "vip", true);
setField(profile, "name", "patched-by-jsxhook");
log(`after.name=${getField(profile, "name")}`);
log(`after.vip=${getField(profile, "vip")}`);
}
});
传 it 也能工作
const title = getField(it, "mTitle");
setField(it, "mResumed", true);
但和 callMethod() 一样,平时更建议你把目标写清楚:
const title = getField(it.thisObject, "mTitle");
连续下钻一个对象
const manager = it.thisObject;
const profile = getField(manager, "mProfile");
const token = getField(profile, "token");
log(`token=${token}`);
fieldName 可以直接吃 Java 返回值
这点在和 DexKit、反射扫描结果配合时特别实用。
现在 getField()、setField()、getStaticField()、setStaticField() 处理字段名时,也会先解包再转字符串,所以不一定非得自己再手动 String(...) 一遍。
const fieldName = fieldData.name;
log(getField(profile, fieldName));
setField(profile, fieldName, "patched-by-jsxhook");
常见来源包括:
fieldData.namefieldHit.fieldName- 其他 Java 返回对象里的字符串类字段名
getStaticField(clazzOrName, fieldName) / setStaticField(clazzOrName, fieldName, value)
这一组是读写静态字段的。
第二参数 fieldName 的行为和上面实例字段一致,也支持先解包再转字符串。
第一参数可传值
| 形式 | 例子 |
|---|---|
| 字符串类名 | "android.os.Build$VERSION" |
Class | findClass("com.example.target.DebugFlags", lpparam.classLoader) |
| 类代理 | importClass("android.os.Build$VERSION") |
读取静态字段
log(`sdk=${getStaticField("android.os.Build$VERSION", "SDK_INT")}`);
用类代理读静态字段
const BuildVersion = importClass("android.os.Build$VERSION");
log(`sdk=${getStaticField(BuildVersion, "SDK_INT")}`);
log(`release=${BuildVersion.RELEASE}`);
修改静态字段
const DebugFlags = findClass(
"com.example.target.DebugFlags",
lpparam.classLoader
);
setStaticField(DebugFlags, "ENABLE_LOG", true);
log(`ENABLE_LOG=${getStaticField(DebugFlags, "ENABLE_LOG")}`);
直接用类代理改静态字段
const DebugFlags = importClass("com.example.target.DebugFlags");
DebugFlags.ENABLE_LOG = true;
log(`ENABLE_LOG=${DebugFlags.ENABLE_LOG}`);
printFields(target, separator?)
这个函数特别适合反射阶段“先看清楚对象长什么样”。
参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
target | object / Class | 是 | - | 传对象时打印实例字段;传类时打印静态字段 |
separator | string | 否 | " " | 字段之间怎么分隔 |
传对象时会发生什么
- 只打印实例字段
- 会沿继承链向上找
- 输出走日志,不是函数返回值
hook({
class: "com.example.target.ProfileManager",
classloader: lpparam.classLoader,
method: "getProfile",
after(it) {
printFields(it.thisObject, "\n");
}
});
传类时会发生什么
- 只打印静态字段
- 同样会沿继承链向上看
const BuildVersion = importClass("android.os.Build$VERSION");
printFields(BuildVersion, "\n");
最推荐的用法:陌生对象先 dump,再决定下一步
hook({
class: "com.example.target.EntryActivity",
classloader: lpparam.classLoader,
method: "onResume",
after(it) {
printFields(it.thisObject, "\n");
}
});
这样你很快就能知道字段到底叫:
mProfileprofilecurrentProfile- 还是别的名字
构造对象:其实你有 3 条路
路线 1:new 导入后的类(...)
这是最像平时写代码的方式,适合公开构造器、参数也比较普通的类。
const ArrayList = imports("java.util.ArrayList");
const list = new ArrayList();
list.add("a");
list.add("b");
log(list.size());
路线 2:this["new"](clazzOrName, ...args)
这是全局的构造辅助函数。
它比类代理构造更强,适合:
- 你手上拿的是
Class - 你要调
declaredConstructors - 参数类型比较复杂
- 你想利用当前实现里更宽松的构造参数转换
先记住它支持的几种写法
| 写法 | 什么时候用 |
|---|---|
this["new"](FileClass, arg1, arg2) | 你手上已经是 Class |
this["new"](FileProxy, arg1, arg2) | 你手上是 imports() / importClass() 返回的类代理 |
this["new"]("java.io.File", arg1, arg2) | 默认 loader 就能找到这个类 |
this["new"]("java.io.File", classLoader, arg1, arg2) | 你要显式指定查类用的 ClassLoader |
const FileClass = findClass("java.io.File", lpparam.classLoader);
const file = this["new"](FileClass, "/sdcard/Download/demo.txt");
log(`exists=${file.exists()}`);
log(`path=${file.getPath()}`);
第一参数是字符串类名时,第二参数可以显式传 classLoader
这是当前实现里一个很实用、但很容易被忽略的细节。
只有同时满足下面两件事时,第二参数才会被当成“查类用的 ClassLoader”:
- 第一参数是字符串类名
- 第二参数本身是一个真正的 Java
ClassLoader
一旦满足这两个条件:
- 第二参数只参与“先把类找出来”
- 真正的构造参数会从第三个位置开始算
const sendImage = this["new"](
"com.example.mobileqq.SendImage",
classLoader,
imagePath,
peerUin
);
这类写法最适合:
- 类名现在只有字符串
- 这个类不在默认 loader 里
- 你已经拿到了一个专门的 loader,比如插件 loader、宿主 loader、动态 dex loader
sendImageClass 如果已经不是字符串,这条规则就不再生效
如果 sendImageClass 已经是下面这些形式之一:
Class- 类代理
那 JSXHook 会直接拿它当“已经解析好的类”去构造。
这时你再把第二参数写成 classLoader,它不会再被当成“查类 loader”,而会继续参与构造器参数匹配。
const sendImageClass = findClass(
"com.example.mobileqq.SendImage",
classLoader
);
// 这里推荐只传真正的构造参数
const sendImage = this["new"](sendImageClass, imagePath, peerUin);
如果目标构造器本身第一参数就是 ClassLoader,那当然也可以传:
const sendImage = this["new"](sendImageClass, classLoader, imagePath);
但这里的 classLoader 含义已经变了,它是构造器参数,不是“帮你查类的 loader”。
第一参数是字符串,但第二参数不是 ClassLoader 时
那它就只是普通构造参数,不会被特殊处理。
const url = this["new"]("java.net.URL", "https://example.com/image.jpg");
路线 3:精确 Constructor
当一个类构造器很多、签名很接近、你不想碰运气时,直接拿 Constructor。
const FileClass = findClass("java.io.File", lpparam.classLoader);
const StringClass = findClass("java.lang.String");
const ctor = FileClass.getDeclaredConstructor(StringClass, StringClass);
ctor.setAccessible(true);
const file = ctor.newInstance("/sdcard", "Download/demo.txt");
log(`path=${file.getPath()}`);
log(`exists=${file.exists()}`);
new 导入后的类(...) 和 this["new"](...) 的差别一定要知道
这点很关键。
new 导入后的类(...)
优点:
- 写法最短
- 最像普通 Java / JS
- 日常最顺手
限制:
- 更适合公开构造器
- 重载复杂时不够透明
this["new"](...)
优点:
- 可以直接传字符串类名、类代理、
Class - 第一参数是字符串类名时,第二参数还能显式传
ClassLoader - 走的是
declaredConstructors - 会尝试更积极地匹配和转换参数
- 出错时会把可用构造器列表带出来,排查更容易
一个非常实用的结论
如果你遇到下面这些情况:
new SomeClass(...)一直不稳- 构造器重载很多
- 你怀疑它用的是私有 / protected 构造器
那就别硬扛,直接改成:
const SomeClass = findClass("com.example.target.SomeClass", lpparam.classLoader);
const obj = this["new"](SomeClass, arg1, arg2);
或者更进一步,直接拿精确 Constructor。
this["new"](...) 的参数转换比方法调用更宽松
这也是一个很容易忽略的细节。
和 callMethod() 相比,它能多帮你做什么
当前实现对构造参数会尝试做这些转换:
CharSequence -> String- 单字符字符串 / 数字 ->
char - 数字 /
"true"/"false"/"1"/"0"/"yes"/"no"->boolean - 数字 / 数字字符串 / 字符 -> 数值类型
- 字符串 -> 枚举名
List/ JS 数组 / Java 数组 -> 目标数组类型
例子 1:单字符字符串转 char
const CharacterClass = findClass("java.lang.Character");
const ch = this["new"](CharacterClass, "A");
log(`${ch}`);
例子 2:JS 数组转 Java 数组参数
如果某个构造器参数本身是数组类型,this["new"](...) 会比你手动拼更轻松。
const ArrayListClass = findClass("java.util.ArrayList");
const list = this["new"](ArrayListClass);
list.add("alpha");
list.add("beta");
log(list.size());
上面这个例子没用到数组参数,但你可以先记住结论:
构造对象时,this["new"](...) 往往比 callMethod() 更愿意帮你做类型转换。
newArray(typeName, size)
这是专门创建 Java 数组的。
参数
| 参数 | 类型 | 必填 | 可填值 | 说明 |
|---|---|---|---|---|
typeName | string / Class / 类代理 | 是 | 基本类型名、String、完整类名、数组类型名 | 数组元素类型 |
size | number | 否 | 非负整数 | 长度;不传时等价于 0 |
支持的常见类型名
intlongbooleandoublefloatcharbyteshortStringjava.io.Filejava.lang.Stringint[]java.lang.String[]
创建基础类型数组
const ints = newArray("int", 4);
ints[0] = 10;
ints[1] = 20;
ints[2] = 30;
ints[3] = 40;
log(`len=${ints.length}`);
log(`first=${ints[0]}`);
创建对象数组
const files = newArray("java.io.File", 2);
const FileClass = findClass("java.io.File", lpparam.classLoader);
files[0] = this["new"](FileClass, "/sdcard/Download/a.txt");
files[1] = this["new"](FileClass, "/sdcard/Download/b.txt");
log(`files[0]=${files[0].getPath()}`);
log(`files[1]=${files[1].getPath()}`);
直接传类代理 / Class
const FileClass = findClass("java.io.File", lpparam.classLoader);
const files = newArray(FileClass, 3);
log(`len=${files.length}`);
一个容易忽略的小细节
如果长度传的是负数,当前实现会把它压成 0,不会真的创建负长度数组。
const arr = newArray("int", -5);
log(arr.length); // 0
ByteArray(size)
这是 byte[] 的快捷创建函数。
最基础例子
const bytes = ByteArray(8);
bytes[0] = 65;
bytes[1] = 66;
bytes[2] = 67;
log(`len=${bytes.length}`);
log(`${bytes[0]},${bytes[1]},${bytes[2]}`);
什么时候比 newArray("byte", size) 更适合
通常就 3 种情况:
- 你就是要
byte[] - 你在写加密、文件、网络字节处理
- 你不想每次都重复写
"byte"
例子:配合 String(byte[]) 构造字符串
const bytes = ByteArray(3);
bytes[0] = 65;
bytes[1] = 66;
bytes[2] = 67;
const StringClass = findClass("java.lang.String");
const text = this["new"](StringClass, bytes);
log(`${text}`);
难点场景:重载多、签名复杂时,直接上原生反射
这部分很重要,因为很多人卡住不是因为“不知道 API 名字”,而是因为“明明 API 名字都知道,但就是调不中”。
场景 1:我要精确拿某个重载方法
const StringBuilderClass = findClass("java.lang.StringBuilder");
const IntegerClass = importClass("java.lang.Integer");
const appendInt = StringBuilderClass.getDeclaredMethod(
"append",
IntegerClass.TYPE
);
appendInt.setAccessible(true);
const sb = this["new"](StringBuilderClass);
appendInt.invoke(sb, 123);
log(sb.toString());
场景 2:我要拿私有字段
const clazz = findClass("com.example.target.DebugFlags", lpparam.classLoader);
const field = clazz.getDeclaredField("ENABLE_LOG");
field.setAccessible(true);
field.set(null, true);
log(field.get(null));
场景 3:我要用精确构造器
const FileClass = findClass("java.io.File", lpparam.classLoader);
const StringClass = findClass("java.lang.String");
const ctor = FileClass.getDeclaredConstructor(StringClass, StringClass);
ctor.setAccessible(true);
const file = ctor.newInstance("/sdcard", "Download/demo.txt");
log(file.getPath());
一个很实战的结论
如果你遇到这些现象:
callMethod()总是NoSuchMethodinvoke()看起来参数差不多,但就是调不到new SomeClass(...)重载太多,一会儿能用一会儿不能用
那就立刻切到:
findClass()getDeclaredMethod()/getDeclaredConstructor()/getDeclaredField()setAccessible(true)invoke()/newInstance()/field.get()/field.set()
别再靠猜。
3 个完整操作例子
例子 1:查类 -> 拿单例 -> 调实例方法
const ActivityThread = findClass(
"android.app.ActivityThread",
lpparam.classLoader
);
const currentApplication = ActivityThread.getDeclaredMethod("currentApplication");
currentApplication.setAccessible(true);
const app = currentApplication.invoke(null);
const packageName = app.getPackageName();
log(`package=${packageName}`);
例子 2:在 Hook 回调里读字段、改字段、再调方法
hook({
class: "com.example.target.ProfileManager",
classloader: lpparam.classLoader,
method: "updateProfile",
params: ["com.example.target.Profile"],
before(it) {
const profile = it.args[0];
log(`before.name=${getField(profile, "name")}`);
log(`before.vip=${getField(profile, "vip")}`);
setField(profile, "vip", true);
setField(profile, "name", "patched-user");
log(`after.name=${getField(profile, "name")}`);
log(`after.vip=${getField(profile, "vip")}`);
},
after(it) {
const result = callMethod(it.thisObject, "getCurrentProfile");
log(`current=${result}`);
}
});
例子 3:先看字段,再决定读哪个
hook({
class: "com.example.target.EntryActivity",
classloader: lpparam.classLoader,
method: "onResume",
after(it) {
printFields(it.thisObject, "\n");
// 观察日志后,假设你发现有个字段叫 mProfile
const profile = getField(it.thisObject, "mProfile");
printFields(profile, "\n");
}
});
常见误区
误区 1:宿主类也用系统类那种查法
不推荐:
findClass("com.example.target.ProfileManager");
更稳:
findClass("com.example.target.ProfileManager", lpparam.classLoader);
误区 2:把字符串当数字 / 布尔硬塞给 callMethod
不稳:
callMethod(target, "setLevel", "1");
callMethod(target, "setEnabled", "true");
更稳:
callMethod(target, "setLevel", 1);
callMethod(target, "setEnabled", true);
误区 3:实例字段和静态字段不分
实例字段用:
getField(obj, "name");
静态字段用:
getStaticField(clazz, "SDK_INT");
误区 4:对象长什么样没看清就直接猜字段名
先:
printFields(obj, "\n");
再决定:
getField(obj, "mProfile");
误区 5:重载太多还坚持只用简写 helper
一旦重载多、签名复杂,直接切原生反射。
最后给你一个选择口诀
可以直接照这个顺序选:
- 要反复用同一个类:先
imports()/importClass() - 要查宿主类:
findClass(name, lpparam.classLoader) - 已知就是实例方法:
callMethod() - 已知就是静态方法:
callStaticMethod() - 想偷个懒写通用入口:
invoke() - 要读写实例字段 / 静态字段:
getField()/getStaticField() - 要先摸清对象结构:
printFields() - 普通构造:
new 导入后的类(...) - 构造器复杂 / 想更稳:
this["new"](...) - 重载、私有成员、签名必须精准:直接
getDeclaredMethod()/getDeclaredConstructor()/getDeclaredField()
如果你把这页吃透,后面无论是改字段、抓单例、造对象、还是在 Hook 里追调用链,基本都能自己往下推了。
