DexKit 静态检索
DexKit 静态检索
这一页专门讲 JSXHook 里已经能直接用的 DexKit 检索 API,也就是:
- 怎么打开
DexKitBridge FindClass/FindMethod/FindField到底怎么写StringMatchType、UsingType、OpCodeMatchType这些值到底能填什么- 查出来的
ClassData/MethodData/FieldData后面怎么继续用
这页不重复讲 APK 工作区、smali/ 浏览、Manifest 读取那套静态分析流程。
那一部分已经放在 DexKit 与 APK 分析。
先分清这页和模块页
很多人第一次接触 DexKit,会把“运行时检索 API”和“APK 工作区分析”混成一件事。
先用这张表切开,后面会省很多时间:
| 你现在要做的事 | 更该看哪里 |
|---|---|
| 在脚本里直接查类、查方法、查字段 | 这页 |
想知道 openDexKit() 和 openDexKitByClassLoader() 有什么区别 | 这页 |
想知道 StringMatchType 有哪些值、每个值是什么意思 | 这页 |
想看 smali/、AndroidManifest.xml、引用关系、工作区路径规则 | DexKit 与 APK 分析 |
| 想用 MCP 工具读 APK 工作区 | MCP APK 工具 |
先建立正确心智模型
DexKit 这块最容易误解的地方,不是语法,而是“它到底在干嘛”。
你可以先把它理解成下面这 4 步:
- 打开一个
DexKitBridge - 用查询对象描述“我要找什么”
- 拿到
ClassData/MethodData/FieldData - 需要真正落到运行时
Class/Method/Field时,再用classLoader转回去
也就是说,DexKit 更像:
- 先帮你在 dex / apk 里定位目标
- 再把结果交给反射 / Hook 去继续处理
它不是:
- Frida 那种“直接盯着 live object”
Java.perform(...)/Java.use(...)那套运行时对象脚本模型
如果你只记一句话:
- DexKit 负责“找”
- 反射和 Hook 负责“用”
入口怎么选
JSXHook 当前给了你 4 条常见入口。它们都能拿到 DexKitBridge,但适合场景不一样。
openDexKit(apkPath?)
这是 JSXHook 包装过的最直接入口。
const bridge = openDexKit();
try {
log(`dexCount=${bridge.getDexNum()}`);
} finally {
bridge.close();
}
参数规则
| 参数 | 类型 | 必填 | 可填值 | 默认行为 |
|---|---|---|---|---|
apkPath | string | 否 | APK 真实路径,如 lpparam.appInfo.sourceDir、/data/app/.../base.apk | 省略参数或传空字符串时,内部会退回到当前应用的 lpparam.appInfo.sourceDir |
这一层有个很容易踩的细节
当前包装代码的逻辑是:
- 先把第一个参数转成字符串
- 再判断是不是空串
- 空串才会回退到
lpparam.appInfo.sourceDir
这意味着:
openDexKit():稳openDexKit(""):也稳openDexKit("/data/app/xxx/base.apk"):稳openDexKit(undefined):不建议赌openDexKit({}):更不要传
因为后两种情况在 JS 里很可能先变成 "undefined" 或 "[object Object]",那就不是你想要的路径了。
最稳的习惯只有两个:
- 要么完全不传
- 要么只传明确的字符串路径
什么时候优先用它
优先用 openDexKit() 的典型场景:
- 你就是要查当前目标 App 自己的 apk / dex
- 你不关心自定义
ClassLoader - 你想先把 DexKit 跑通,再慢慢缩小范围
openDexKitByClassLoader(useMemoryDexFile = false)
这个入口更贴近“当前运行时类加载链实际能看到什么”。
const bridge = openDexKitByClassLoader(false);
try {
log(`valid=${bridge.isValid()}`);
} finally {
bridge.close();
}
参数规则
| 参数 | 类型 | 必填 | 可填值 | 默认值 |
|---|---|---|---|---|
useMemoryDexFile | boolean | 否 | true、false | false |
真正会认哪些值
JSXHook 这一层当前是把第一个参数做 Boolean 提取:
- 你传
true-> 真正是true - 你传
false-> 真正是false - 你不传 -> 回落到
false - 你传
"true"、1、"1"这种 -> 不会帮你转布尔
所以这里也别猜,老老实实只传:
openDexKitByClassLoader(true);
openDexKitByClassLoader(false);
useMemoryDexFile 到底是什么意思
从 DexKit 2.0.7 源码说明看:
false:更保守,优先按常规加载链处理true:会尝试按内存 dex 方式去取;如果遇到不能解析的 OatDex,会再回退到 apk 路径模式
对新手来说,先按这个规则选就够了:
- 第一次跑通:先用
false - 你很怀疑目标类来自内存 dex / 特殊加载链:再试
true
什么时候优先用它
- 你怀疑目标类不是老老实实从
base.apk里常规加载出来的 - 你想让结果更贴近当前进程真实的
ClassLoader视角 - 你后面准备直接把结果转回
lpparam.classLoader里的Class/Method
这一层的限制
openDexKitByClassLoader(...) 当前包装死的是:
lpparam.classLoader
也就是说,它没有让你传自定义 loader。
如果你已经拿到了别的 ClassLoader,比如:
const loader = it.thisObject.getClass().getClassLoader();
那就别用这个包装函数了,直接走下一节的 DexKitBridge.create(loader, false)。
DexFinder.INSTANCE.create(apkPath) + DexFinder.getDexKitBridge()
JSXHook 还把 DexFinder 暴露出来了。
DexFinder.INSTANCE.create(lpparam.appInfo.sourceDir);
const bridge = DexFinder.getDexKitBridge();
try {
log(`valid=${bridge != null && bridge.isValid()}`);
} finally {
DexFinder.INSTANCE.close();
}
参数规则
| 步骤 | 作用 | 要点 |
|---|---|---|
DexFinder.INSTANCE.create(apkPath) | 先按 APK 路径建桥 | apkPath 请传字符串路径 |
DexFinder.getDexKitBridge() | 拿桥对象 | 可能为 null,先判空 |
DexFinder.INSTANCE.close() | 释放资源 | 这一套更推荐关 DexFinder 自己 |
什么时候用
- 你本来就已经在用
DexFinder - 你想复用 JSXHook 现成桥接逻辑
- 你拿到的是 apk 路径,不是自定义
ClassLoader
DexKitBridge.create(...)
这是更底层、也更灵活的入口。
JSXHook 已经把 DexKitBridge 这个类直接挂到全局了。
当前可用的 3 个重载
| 写法 | 作用 | 适合场景 |
|---|---|---|
DexKitBridge.create(apkPath) | 通过 APK 路径打开 | 你手里明确有 apk 路径 |
DexKitBridge.create(loader, useMemoryDexFile) | 通过指定 ClassLoader 打开 | 你就是想自己控 loader |
DexKitBridge.create(dexBytesArray) | 通过 byte[] 数组打开 | 很进阶,日常 JSXHook 脚本几乎用不到 |
最实用的一种高级写法
const loader = it.thisObject.getClass().getClassLoader();
const bridge = DexKitBridge.create(loader, false);
try {
log(`dexCount=${bridge.getDexNum()}`);
} finally {
bridge.close();
}
如果你已经知道:
- 当前要沿着哪个对象的 loader 继续查
那这条路会比 openDexKitByClassLoader(...) 更自由。
Bridge 基础操作
拿到 bridge 以后,不是只能 findClass / findMethod / findField。
还有一批非常实用的实例方法,最好一开始就知道。
bridge.isValid() / bridge.close()
这是最基础、也最该养成习惯的一组。
bridge.isValid()
- 返回值只有
true/false true表示当前桥还没被关闭false表示桥已失效,或者初始化就没成功
const bridge = openDexKit();
log(`valid=${bridge.isValid()}`);
bridge.close();
log(`validAfterClose=${bridge.isValid()}`);
bridge.close()
- 无参数
- 用来释放 native 资源
- 重复调用不会再重复释放,当前源码是“已经无效就不再处理”
最稳写法永远是:
const bridge = openDexKit();
try {
// do search
} finally {
bridge.close();
}
bridge.getDexNum()
返回当前桥实际解析到的 dex 数量。
const bridge = openDexKit();
try {
const dexCount = bridge.getDexNum();
log(`dexCount=${dexCount}`);
} finally {
bridge.close();
}
什么时候它特别有用
- 判断桥有没有正常打开
- 判断
openDexKit()和openDexKitByClassLoader(...)看到的 dex 规模是不是差很多 - 排查“为什么明明有类却查不到”
bridge.setThreadNum(num)
设置 DexKit 工作线程数。
const bridge = openDexKit();
try {
bridge.setThreadNum(2);
log(`dexCount=${bridge.getDexNum()}`);
} finally {
bridge.close();
}
num 到底能填什么
从 JSXHook 和 DexKit 这层源码看:
- 这里只接收一个
Int - JS 侧没有帮你做范围兜底
所以实战里请只传:
124
这种明确的正整数。
不建议传:
0- 负数
- 字符串
"2"
因为包装层不会替你兜。
bridge.initFullCache()
初始化全量缓存。
const bridge = openDexKit();
try {
bridge.initFullCache();
log("full cache ready");
} finally {
bridge.close();
}
这一步要不要默认加
不要默认加。
DexKit 源码自己的说明已经写得很直接:
- 会占更多时间
- 会占更多内存
- 更适合性能测试或高频重复检索
所以普通脚本建议:
- 先别加
- 只有你确定要反复跑大量查询时,再考虑加
bridge.exportDexFile(outPath)
把当前桥里解析到的 dex 导出到指定输出路径。
const bridge = openDexKit();
try {
bridge.exportDexFile("/sdcard/Download/jsxhook-dex-export");
} finally {
bridge.close();
}
outPath 怎么填更稳
- 只传字符串路径
- 尽量准备一个明确可写的目录
- 不要传相对路径去赌当前工作目录
如果你是第一次用,最省心的就是传一眼能认出的绝对路径。
bridge.getClassData(...)
这个方法的价值在于:
- 你已经知道类名或类描述符
- 你不想重新写一整套
FindClass - 你只是想把“一个已知类”直接转成
ClassData
可传值
| 写法 | 说明 |
|---|---|
bridge.getClassData("com.example.LoginManager") | 传完整类名 |
bridge.getClassData("Lcom/example/LoginManager;") | 传类描述符 |
bridge.getClassData(SomeJavaClass) | 传已经拿到的 Java Class |
当前实现的真实规则
如果你传的是字符串:
- 首字符是
L且末尾是;-> 当成类描述符 - 否则 -> 当成类名,再内部转成描述符
const bridge = openDexKit();
try {
const classData = bridge.getClassData("com.example.LoginManager");
if (classData) {
log(classData.descriptor);
log(classData.name);
}
} finally {
bridge.close();
}
bridge.getMethodData(...)
这个和 getClassData(...) 不一样,它的字符串版本只认方法描述符。
可传值
| 写法 | 说明 |
|---|---|
bridge.getMethodData("Lcom/example/LoginManager;->login(Ljava/lang/String;)Z") | 传方法描述符 |
bridge.getMethodData(methodObject) | 传 java.lang.reflect.Method |
bridge.getMethodData(constructorObject) | 传 java.lang.reflect.Constructor |
不能偷懒传什么
下面这些都不对:
bridge.getMethodData("login");
bridge.getMethodData("com.example.LoginManager.login");
因为它不是按名字查,它是按完整方法描述符或反射对象转。
一个很实用的写法
const bridge = openDexKit();
try {
const TargetClass = findClass("com.example.LoginManager", lpparam.classLoader);
const method = TargetClass.getDeclaredMethod(
"login",
findClass("java.lang.String"),
findClass("java.lang.String")
);
method.setAccessible(true);
const methodData = bridge.getMethodData(method);
if (methodData) {
log(methodData.descriptor);
log(methodData.returnTypeName);
}
} finally {
bridge.close();
}
bridge.getFieldData(...)
字段版本也一样,字符串版本只认完整字段描述符。
可传值
| 写法 | 说明 |
|---|---|
bridge.getFieldData("Lcom/example/LoginManager;->token:Ljava/lang/String;") | 传字段描述符 |
bridge.getFieldData(fieldObject) | 传 java.lang.reflect.Field |
例子
const bridge = openDexKit();
try {
const fieldData = bridge.getFieldData(
"Lcom/example/LoginManager;->token:Ljava/lang/String;"
);
if (fieldData) {
log(fieldData.name);
log(fieldData.typeName);
}
} finally {
bridge.close();
}
先记住 4 种描述格式
DexKit 后面很多 API 会同时出现:
- 类名
- 类描述符
- 方法描述符
- 字段描述符
如果这 4 种你分不清,后面几乎一定会越写越乱。
类名
这是你平时最熟的那种形式:
com.example.LoginManager
java.lang.String
android.view.View
ClassMatcher.className(...) 这类 API 用它最顺手。
DexKit 源码文档里也允许斜杠形式:
com/example/LoginManager
但日常还是推荐你统一用点号类名,读起来最直观。
类描述符
格式长这样:
Lcom/example/LoginManager;
Ljava/lang/String;
Landroid/view/View;
特征是:
- 以
L开头 - 以
;结尾 - 包名分隔符是
/
方法描述符
格式长这样:
Lcom/example/LoginManager;->login(Ljava/lang/String;Ljava/lang/String;)Z
你可以把它拆成 3 块看:
Lcom/example/LoginManager;:声明这个方法的类login:方法名(Ljava/lang/String;Ljava/lang/String;)Z:参数和返回值
字段描述符
格式长这样:
Lcom/example/LoginManager;->token:Ljava/lang/String;
你可以把它拆成 3 块看:
Lcom/example/LoginManager;:声明这个字段的类token:字段名Ljava/lang/String;:字段类型
常见类型缩写
Dex 描述符里最常见的基础类型缩写如下:
| 缩写 | 含义 |
|---|---|
V | void |
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long |
F | float |
D | double |
Ljava/lang/String; | java.lang.String |
[I | int[] |
[Ljava/lang/String; | java.lang.String[] |
必须先认清的枚举值
这部分你最好别跳过。
很多“我明明写了匹配条件却查不到”的问题,根子就是这里的值没选对。
StringMatchType
当前 DexKit 2.0.7 里只有下面 5 个值:
| 值 | 作用 | 典型场景 |
|---|---|---|
StringMatchType.Contains | 包含匹配 | 关键字宽搜、混淆环境先缩小范围 |
StringMatchType.StartsWith | 前缀匹配 | 包名前缀、类名前缀、方法名前缀 |
StringMatchType.EndsWith | 后缀匹配 | Activity、Fragment、Manager 这类结尾 |
StringMatchType.SimilarRegex | 类正则匹配 | 字符串模式比较复杂时 |
StringMatchType.Equals | 完全相等 | 你已经知道精确类名、精确字段类型、精确方法名 |
一个很重要的默认值提醒
不是所有字符串 API 的默认匹配方式都一样。
最常见的是这两类:
| API | 默认匹配方式 |
|---|---|
className(...) / name(...) / type(...) / declaredClass(...) / source(...) | 默认 Equals |
usingStrings("a", "b") 这种字符串列表快捷写法 | 默认 Contains |
这就是为什么同样是“不显式传 StringMatchType”:
name("login")往往是等值匹配usingStrings("login")往往是包含匹配
别把这两个默认值混了。
UsingType
当前只有 3 个值:
| 值 | 含义 | 最常见用法 |
|---|---|---|
UsingType.Any | 只要这个方法和字段发生过关系就算,不区分读还是写 | 第一轮先宽搜 |
UsingType.Read | 只匹配“读字段”的方法 | 你关心 getter / 读取点 |
UsingType.Write | 只匹配“写字段”的方法 | 你关心 setter / 赋值点 |
它最常见出现在:
MethodMatcher.create().addUsingField(fieldDescriptor, UsingType.Write)
OpCodeMatchType
当前只有 4 个值:
| 值 | 作用 |
|---|---|
OpCodeMatchType.Contains | 指令序列出现在方法体任何位置 |
OpCodeMatchType.StartsWith | 指令序列出现在方法体开头 |
OpCodeMatchType.EndsWith | 指令序列出现在方法体结尾 |
OpCodeMatchType.Equals | 指令序列整体相等 |
它用于:
opNames(...)opCodes(...)
MatchType
这个枚举主要给修饰符匹配用,当前只有 2 个值:
| 值 | 作用 |
|---|---|
MatchType.Contains | 修饰符位掩码包含这些位就算命中 |
MatchType.Equals | 修饰符位掩码必须完全相等 |
最常见场景是:
const Modifier = importClass("java.lang.reflect.Modifier");
const MatchType = imports("org.luckypray.dexkit.query.enums.MatchType");
ClassMatcher.create().modifiers(
Modifier.PUBLIC | Modifier.FINAL,
MatchType.Contains
);
查类:FindClass.create()
最小模板
const FindClass = imports("org.luckypray.dexkit.query.FindClass");
const ClassMatcher = imports("org.luckypray.dexkit.query.matchers.ClassMatcher");
const StringMatchType = imports("org.luckypray.dexkit.query.enums.StringMatchType");
const bridge = openDexKit();
try {
const query = FindClass.create()
.matcher(
ClassMatcher.create()
.className("login", StringMatchType.Contains)
);
const matches = bridge.findClass(query);
log(`matches=${matches.size()}`);
} finally {
bridge.close();
}
FindClass 这一层参数
FindClass 自己主要负责“搜索范围”和“什么时候停”。
| API | 可填值 | 默认值 | 说明 |
|---|---|---|---|
searchPackages(...) | 一个或多个包名前缀字符串 | 不限制 | 只在这些包里搜 |
excludePackages(...) | 一个或多个包名前缀字符串 | 不排除 | 从范围里再剔除这些包 |
ignorePackagesCase(true/false) | 只能填 true 或 false | false | 只影响 searchPackages / excludePackages |
searchIn(classHits) | Collection<ClassData> / ClassDataList | 全 APK | 只在上一轮类结果里继续搜 |
findFirst = true/false | 只能填 true 或 false | false | 找到第一个就停 |
matcher(ClassMatcher) | ClassMatcher | 无 | 真正描述“类长什么样” |
searchPackages(...) 和 className(...) 不是一回事
很多新手会把这两个混掉:
searchPackages("com.example.feature"):先把搜索范围限制在这个包className("LoginManager", StringMatchType.Contains):匹配类名内容本身
实际很常见的组合是:
FindClass.create()
.searchPackages("com.example.feature")
.matcher(
ClassMatcher.create().className("login", StringMatchType.Contains)
);
ClassMatcher 常用条件
ClassMatcher 负责描述“这个类本身像什么”。
descriptor(...)
当你已经知道类描述符时,直接走精确匹配最稳。
ClassMatcher.create().descriptor("Lcom/example/LoginManager;");
适合什么时候用
- 你从 Smali、日志、反射里已经拿到了精确 descriptor
- 你不想再猜包名大小写、点号斜杠这些细节
className(...)
这是最常见的类名匹配入口。
ClassMatcher.create().className(
"com.example.LoginManager",
StringMatchType.Equals,
false
);
参数
| 参数 | 可填值 | 默认值 |
|---|---|---|
className | 点号类名或斜杠类名 | 无 |
matchType | Contains / StartsWith / EndsWith / SimilarRegex / Equals | Equals |
ignoreCase | true / false | false |
最实用的 3 种写法
// 已知完整类名
ClassMatcher.create().className("com.example.LoginManager");
// 先宽搜
ClassMatcher.create().className("login", StringMatchType.Contains);
// 只看某类后缀
ClassMatcher.create().className("Activity", StringMatchType.EndsWith);
source(...)
按 .source 文件名去匹配类。
ClassMatcher.create().source("MainActivity.java");
什么时候它特别好用
- 混淆类名已经很难看,但
.source还残留可读信息 - 你从崩溃栈、日志或反编译结果里看到了源文件名
参数规则
| 参数 | 可填值 | 默认值 |
|---|---|---|
source | 纯文件名,如 MainActivity.java | 无 |
matchType | 同 StringMatchType 那 5 个值 | Equals |
ignoreCase | true / false | false |
superClass(...)
按父类去筛类。
ClassMatcher.create().superClass(
"android.app.Activity",
StringMatchType.Equals,
false
);
可传形式
- 字符串类名
ClassMatcher
新手平时直接传字符串就够了:
ClassMatcher.create().superClass("androidx.fragment.app.Fragment");
addInterface(...) / interfaceCount(...)
这组用来按“实现了什么接口”或“实现了几个接口”继续压范围。
ClassMatcher.create()
.addInterface("java.lang.Runnable")
.interfaceCount(1, 3);
addInterface(...) 可传什么
| 写法 | 说明 |
|---|---|
addInterface("java.lang.Runnable") | 最常用 |
addInterface("android.view.View", StringMatchType.Equals, false) | 显式控制匹配方式 |
addInterface(ClassMatcher.create()...) | 更复杂的接口条件 |
interfaceCount(...) 怎么填最省事
DexKit 源码有多种重载:
- 精确数量:
interfaceCount(2) IntRange- Kotlin
1..3 min, max
但在 JS 里最省事的是:
interfaceCount(1, 3)
因为你不用再去手动构 IntRange 对象。
modifiers(...)
按修饰符位做匹配。
const Modifier = importClass("java.lang.reflect.Modifier");
const MatchType = imports("org.luckypray.dexkit.query.enums.MatchType");
ClassMatcher.create().modifiers(
Modifier.PUBLIC | Modifier.FINAL,
MatchType.Contains
);
这里能填什么
| 参数 | 可填值 | 默认值 |
|---|---|---|
modifiers | java.lang.reflect.Modifier 位掩码组合值 | 无 |
matchType | MatchType.Contains、MatchType.Equals | Contains |
怎么理解 Contains 和 Equals
Contains:只要包含这些修饰符位就行Equals:修饰符位必须完全一样
所以你第一次写时,通常先用 Contains,别一上来就 Equals 把自己卡死。
fieldCount(...) / methodCount(...)
这组条件很适合在混淆环境里继续缩范围。
ClassMatcher.create()
.fieldCount(2, 8)
.methodCount(5, 30);
这里有两个很关键的点
- JS 里最省事的仍然是传
min, max methodCount(...)统计里包含:- 构造函数
<init> - 静态初始化块
<clinit>
- 构造函数
所以如果你觉得“这个类明明就 3 个普通方法,为什么方法数不是 3”,先把这点想起来。
addFieldForName(...) / addFieldForType(...)
这组条件很适合你已经知道“这个类里大概有某个字段”的时候。
ClassMatcher.create()
.addFieldForName("token", StringMatchType.Contains)
.addFieldForType("java.lang.String");
addFieldForName(...)
- 按字段名筛
- 默认字符串匹配是
Equals - 宽搜时显式传
StringMatchType.Contains
addFieldForType(...)
- 按字段类型筛
- 最常传的是
java.lang.String、int、boolean
addMethod(...)
按“类里声明了什么方法”去筛类。
ClassMatcher.create().addMethod(
MethodMatcher.create()
.name("login", StringMatchType.Contains)
.paramCount(1)
);
这里要注意
ClassMatcher 里的“方法”范围包含:
- 普通方法
- 构造函数
<init> - 静态初始化块
<clinit>
如果你就是想找构造函数,也可以明确写:
MethodMatcher.create().name("<init>");
usingStrings(...) / usingEqStrings(...) / addUsingString(...)
这是类查询里非常实用的一组。
ClassMatcher.create().usingStrings(
["login", "token"],
StringMatchType.Contains,
false
);
这 3 种写法的区别
| 写法 | 默认行为 | 适合场景 |
|---|---|---|
usingStrings("a", "b") | 默认 Contains | 快速宽搜 |
usingStrings(list, matchType, ignoreCase) | 你自己定匹配方式 | 想显式控值 |
usingEqStrings("a", "b") | 强制 Equals | 你知道字符串必须精确出现 |
addUsingString("a", matchType, ignoreCase) | 一个一个加 | 条件是动态拼出来的 |
一个很好用的经验
第一次别太贪精确,通常先这样:
ClassMatcher.create().usingStrings("login", "token");
等命中太多了,再切到:
ClassMatcher.create().usingEqStrings("login_success");
类查询例子
例子 1:先按包名缩范围,再按类名宽搜
const query = FindClass.create()
.searchPackages("com.example.feature")
.matcher(
ClassMatcher.create()
.className("login", StringMatchType.Contains)
);
例子 2:按字符串和字段类型找可疑类
const query = FindClass.create()
.matcher(
ClassMatcher.create()
.usingStrings("token", "refresh")
.addFieldForType("java.lang.String")
.fieldCount(2, 10)
);
例子 3:已知 descriptor 直接精确命中
const query = FindClass.create()
.matcher(
ClassMatcher.create().descriptor("Lcom/example/LoginManager;")
);
查方法:FindMethod.create()
最小模板
const FindMethod = imports("org.luckypray.dexkit.query.FindMethod");
const MethodMatcher = imports("org.luckypray.dexkit.query.matchers.MethodMatcher");
const bridge = openDexKit();
try {
const query = FindMethod.create()
.matcher(
MethodMatcher.create()
.name("onClick")
.paramCount(1)
.addParamType("android.view.View")
);
const matches = bridge.findMethod(query);
log(`matches=${matches.size()}`);
} finally {
bridge.close();
}
FindMethod 这一层参数
| API | 可填值 | 默认值 | 说明 |
|---|---|---|---|
searchPackages(...) | 一个或多个包名前缀字符串 | 不限制 | 只在这些包里搜 |
excludePackages(...) | 一个或多个包名前缀字符串 | 不排除 | 排掉这些包 |
ignorePackagesCase(true/false) | true / false | false | 只影响包过滤 |
searchInClass(classHits) | Collection<ClassData> | 全 APK | 只在这些类的方法里搜 |
searchInMethod(methodHits) | Collection<MethodData> | 全 APK | 只在上一轮方法结果里继续筛 |
findFirst = true/false | true / false | false | 找到一个就停 |
matcher(MethodMatcher) | MethodMatcher | 无 | 真正描述方法条件 |
searchInClass(...) 和 searchInMethod(...) 怎么选
- 你上一轮拿到的是类结果 -> 用
searchInClass(classHits) - 你上一轮拿到的已经是方法结果 -> 用
searchInMethod(methodHits)
这也是 DexKit 缩范围最好用的地方:
前一轮结果,不用手工拆,直接喂给下一轮。
MethodMatcher 常用条件
descriptor(...)
你已经知道完整方法描述符时,直接用它最稳。
MethodMatcher.create().descriptor(
"Lcom/example/LoginManager;->login(Ljava/lang/String;Ljava/lang/String;)Z"
);
它会顺手帮你补哪些信息
从 DexKit 源码看,descriptor(...) 内部会自动拆出并设置:
namedeclaredClassreturnTypeparamTypes
所以已知 descriptor 时,不要再自己重复补一遍。
name(...) / declaredClass(...)
这是最常见的一组。
MethodMatcher.create()
.name("login", StringMatchType.Contains)
.declaredClass("com.example.LoginManager");
默认值
| API | 默认 StringMatchType | 默认 ignoreCase |
|---|---|---|
name(...) | Equals | false |
declaredClass(...) | Equals | false |
所以你想宽搜时,一定要显式写:
name("login", StringMatchType.Contains)
returnType(...)
按返回值类型筛方法。
MethodMatcher.create().returnType("boolean");
MethodMatcher.create().returnType("java.lang.String");
可传形式
| 写法 | 说明 |
|---|---|
returnType("java.lang.String") | 最常用 |
returnType(SomeClass) | 你已经拿到 Java Class |
returnType(ClassMatcher.create()...) | 想写更复杂条件 |
paramTypes(...) / addParamType(...) / paramCount(...)
这一组是方法查询里最容易写错、也最该写明白的一组。
paramTypes()
无参数重载,表示“这个方法没有参数”:
MethodMatcher.create().paramTypes();
paramTypes("a", "b")
精确写出参数类型顺序:
MethodMatcher.create().paramTypes(
"java.lang.String",
"java.lang.String"
);
paramTypes(null, "java.lang.String")
这里的 null 很有用,意思是:
- 这一位参数可以是任意类型
MethodMatcher.create().paramTypes(
null,
"java.lang.String"
);
这相当于:
- 第 1 个参数随便
- 第 2 个参数必须是
java.lang.String
addParamType(...)
适合你是“边想边拼条件”的场景:
MethodMatcher.create()
.addParamType("java.lang.String")
.addParamType("int");
paramCount(...)
这组可以精确数量,也可以范围:
MethodMatcher.create().paramCount(1);
MethodMatcher.create().paramCount(1, 3);
最容易踩的坑
下面这种写法很容易把自己卡死:
MethodMatcher.create()
.paramTypes("java.lang.String")
.paramCount(2);
因为:
paramTypes("java.lang.String")已经暗含“只有 1 个参数”- 你后面又要求
paramCount(2)
这两个条件是互相打架的。
protoShorty(...)
这是方法原型的紧凑写法。
如果你第一次见,会觉得它很怪;但一旦看懂,查重载时非常利索。
可用字符
| 字符 | 含义 |
|---|---|
V | void |
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long |
F | float |
D | double |
L | 对象或对象数组 |
读法规则
- 第 1 个字符:返回值类型
- 后面的字符:参数类型顺序
例子
| 写法 | 含义 |
|---|---|
protoShorty("V") | 无参数 void 方法 |
protoShorty("VL") | 返回 void,参数 1 个对象 |
protoShorty("ZLL") | 返回 boolean,参数 2 个对象 |
protoShorty("ILI") | 返回 int,参数是对象 + int |
什么时候用它更值
- 重载特别多
- 你不想把完整 descriptor 整串抄出来
- 你只关心“返回值 + 参数轮廓”
usingStrings(...) / usingEqStrings(...)
方法体里出现的字符串匹配。
MethodMatcher.create().usingStrings("login", "token");
MethodMatcher.create().usingEqStrings("login_success");
规则和类查询基本一致
| 写法 | 默认行为 |
|---|---|
usingStrings("a", "b") | 默认 Contains |
usingStrings(list, matchType, ignoreCase) | 你自己指定 |
usingEqStrings("a") | 强制精确相等 |
addUsingString("a", matchType, ignoreCase) | 一个一个加 |
addUsingField(..., UsingType)
这个条件非常适合找:
- 某个方法有没有读某个字段
- 某个方法有没有写某个字段
可传形式
| 写法 | 说明 |
|---|---|
addUsingField(fieldMatcher, UsingType.Any) | 传 FieldMatcher |
addUsingField(fieldDescriptor, UsingType.Write) | 传字段描述符 |
UsingType 只能填什么
UsingType.AnyUsingType.ReadUsingType.Write
例子
const UsingType = imports("org.luckypray.dexkit.query.enums.UsingType");
MethodMatcher.create().addUsingField(
"Lcom/example/LoginManager;->token:Ljava/lang/String;",
UsingType.Write
);
这表示:
- 只关心“会写这个字段”的方法
addCaller(...) / addInvoke(...)
这组特别容易写反。
addInvoke(...)
表示:
- “这个目标方法内部又调用了谁”
MethodMatcher.create().addInvoke(
"Ljava/lang/String;->length()I"
);
addCaller(...)
表示:
- “这个目标方法是被谁调起来的”
MethodMatcher.create().addCaller(
"Lcom/example/EntryActivity;->onClick(Landroid/view/View;)V"
);
最简单的记忆法
invoke:往外看,它调了谁caller:往回看,谁调了它
opNames(...) / opCodes(...)
这是按方法体指令序列筛方法,属于比较硬核但很有杀伤力的一组。
opNames(...)
按 smali 指令名匹配,例如:
const-stringconst-string/jumboinvoke-virtualinvoke-static
opCodes(...)
按 opcode 数值匹配。
DexKit MethodData.opCodes 返回的就是 0 到 255 的整型列表。
OpCodeMatchType 可填值
ContainsStartsWithEndsWithEquals
一个实用提醒
这类 API 底层接收的是 Collection<String> / Collection<Int>。
如果你所在脚本环境里 JS 数组没有自动桥成 Collection,就先手工装一个 ArrayList 再传。
usingNumbers(...)
这是很容易被忽略,但对混淆环境非常好用的一个条件。
它按“方法里出现过的数字常量”匹配。
MethodMatcher.create().usingNumbers(114514, 10086);
它能填什么
- 一个或多个
Number - 可以是整数
- 也可以是浮点数
一个特别细的行为
DexKit 2.0.7 源码里明确写了:
- 浮点比较会容忍
1e-6以内的误差
也就是说,写浮点筛选时,它不是死板的逐位相等。
方法查询例子
例子 1:手册里的 onClick(View) 版本
const query = FindMethod.create()
.matcher(
MethodMatcher.create()
.name("onClick")
.paramCount(1)
.addParamType("android.view.View")
);
例子 2:在上一轮类结果里继续缩方法
const classHits = bridge.findClass(
FindClass.create().matcher(
ClassMatcher.create().className("login", StringMatchType.Contains)
)
);
const methodHits = classHits.findMethod(
FindMethod.create().matcher(
MethodMatcher.create()
.name("login", StringMatchType.Contains)
.paramCount(2)
.usingStrings("token")
)
);
例子 3:找“会写 token 字段”的方法
const UsingType = imports("org.luckypray.dexkit.query.enums.UsingType");
const query = FindMethod.create()
.matcher(
MethodMatcher.create()
.addUsingField(
"Lcom/example/LoginManager;->token:Ljava/lang/String;",
UsingType.Write
)
);
查字段:FindField.create()
最小模板
const FindField = imports("org.luckypray.dexkit.query.FindField");
const FieldMatcher = imports("org.luckypray.dexkit.query.matchers.FieldMatcher");
const StringMatchType = imports("org.luckypray.dexkit.query.enums.StringMatchType");
const bridge = openDexKit();
try {
const query = FindField.create()
.matcher(
FieldMatcher.create()
.declaredClass("com.example.target.Profile", StringMatchType.Contains)
.name("token", StringMatchType.Contains)
.type("java.lang.String", StringMatchType.Equals)
);
const matches = bridge.findField(query);
log(`matches=${matches.size()}`);
} finally {
bridge.close();
}
FindField 这一层参数
| API | 可填值 | 默认值 | 说明 |
|---|---|---|---|
searchPackages(...) | 一个或多个包名前缀字符串 | 不限制 | 只在这些包里搜 |
excludePackages(...) | 一个或多个包名前缀字符串 | 不排除 | 排掉这些包 |
ignorePackagesCase(true/false) | true / false | false | 只影响包过滤 |
searchInClass(classHits) | Collection<ClassData> | 全 APK | 只在这些类的字段里搜 |
searchInField(fieldHits) | Collection<FieldData> | 全 APK | 只在上一轮字段结果里继续筛 |
findFirst = true/false | true / false | false | 找到一个就停 |
matcher(FieldMatcher) | FieldMatcher | 无 | 真正描述字段条件 |
FieldMatcher 常用条件
descriptor(...)
已知完整字段描述符时,直接精确匹配最稳。
FieldMatcher.create().descriptor(
"Lcom/example/LoginManager;->token:Ljava/lang/String;"
);
name(...) / declaredClass(...)
这是字段查询里最常用的第一轮条件。
FieldMatcher.create()
.declaredClass("com.example.LoginManager")
.name("token");
默认值
| API | 默认 StringMatchType | 默认 ignoreCase |
|---|---|---|
name(...) | Equals | false |
declaredClass(...) | Equals | false |
如果你就是在混淆环境里宽搜,记得自己改成:
name("token", StringMatchType.Contains)
type(...)
按字段类型继续压范围。
FieldMatcher.create().type("java.lang.String");
FieldMatcher.create().type("boolean");
FieldMatcher.create().type("int");
可传形式
| 写法 | 说明 |
|---|---|
type("java.lang.String") | 最常用 |
type(SomeJavaClass) | 已有 Java Class |
type(ClassMatcher.create()...) | 更复杂的类型条件 |
addReadMethod(...) / addWriteMethod(...)
这组条件特别适合“通过读写点反推字段”。
addReadMethod(...)
表示:
- 这个字段会被哪些方法读取
addWriteMethod(...)
表示:
- 这个字段会被哪些方法写入
可传形式
| 写法 | 说明 |
|---|---|
addReadMethod(methodDescriptor) | 传方法描述符 |
addReadMethod(MethodMatcher.create()...) | 传方法匹配器 |
addWriteMethod(methodDescriptor) | 传方法描述符 |
addWriteMethod(MethodMatcher.create()...) | 传方法匹配器 |
例子
FieldMatcher.create()
.type("java.lang.String")
.addWriteMethod("Lcom/example/LoginManager;->saveToken(Ljava/lang/String;)V");
modifiers(...) / annotations(...)
字段这边也支持:
modifiers(...)annotations(...)addAnnotation(...)annotationCount(...)
如果你已经明显知道:
- 它是
public static final - 或者它挂了某个注解
这些条件都能继续压范围。
字段查询例子
例子 1:按声明类 + 字段名 + 字段类型
const query = FindField.create()
.matcher(
FieldMatcher.create()
.declaredClass("com.example.LoginManager")
.name("token")
.type("java.lang.String")
);
例子 2:按写入方法反推字段
const query = FindField.create()
.matcher(
FieldMatcher.create()
.type("java.lang.String")
.addWriteMethod("Lcom/example/LoginManager;->saveToken(Ljava/lang/String;)V")
);
例子 3:在上一轮类结果里继续筛字段
const classHits = bridge.findClass(
FindClass.create().matcher(
ClassMatcher.create().usingStrings("login", "token")
)
);
const fieldHits = classHits.findField(
FindField.create().matcher(
FieldMatcher.create()
.name("token", StringMatchType.Contains)
.type("java.lang.String")
)
);
批量分组:BatchFindClassUsingStrings / BatchFindMethodUsingStrings
什么时候用
批量分组不是为了替代 FindClass / FindMethod,而是为了让你:
- 一次跑多组关键词
- 每组都有自己的组名
- 最后按组拿结果
它特别适合这种场景:
- 你在猜“登录流”“个人资料流”“支付流”分别落在哪些地方
- 你不想一次只试一组关键词
BatchFindClassUsingStrings.create()
const BatchFindClassUsingStrings = imports(
"org.luckypray.dexkit.query.BatchFindClassUsingStrings"
);
const bridge = openDexKit();
try {
const query = BatchFindClassUsingStrings.create()
.searchPackages("com.example.feature")
.addSearchGroup("loginFlow", ["login", "signIn", "auth"])
.addSearchGroup("profileFlow", ["profile", "avatar", "nickname"]);
const groups = bridge.batchFindClassUsingStrings(query);
const loginHits = groups.get("loginFlow");
log(`loginFlow=${loginHits ? loginHits.size() : 0}`);
} finally {
bridge.close();
}
这一层能填什么
| API | 可填值 | 说明 |
|---|---|---|
searchPackages(...) | 包名前缀字符串 | 只在这些包里搜 |
excludePackages(...) | 包名前缀字符串 | 排掉这些包 |
ignorePackagesCase(true/false) | true / false | 只影响包过滤 |
searchIn(classHits) | Collection<ClassData> | 只在指定类集合里搜 |
groups(map, matchType, ignoreCase) | Map<String, Collection<String>> | 一次性塞整组 |
addSearchGroup(name, strings, matchType, ignoreCase) | 组名 + 字符串集合 | 一组一组加 |
BatchFindMethodUsingStrings.create()
手册里给的是方法版例子,这个在实战里也最常见。
const FindClass = imports("org.luckypray.dexkit.query.FindClass");
const BatchFindMethodUsingStrings = imports(
"org.luckypray.dexkit.query.BatchFindMethodUsingStrings"
);
const ClassMatcher = imports("org.luckypray.dexkit.query.matchers.ClassMatcher");
const StringMatchType = imports("org.luckypray.dexkit.query.enums.StringMatchType");
const bridge = openDexKit();
try {
const classHits = bridge.findClass(
FindClass.create().matcher(
ClassMatcher.create().className("login", StringMatchType.Contains)
)
);
const query = BatchFindMethodUsingStrings.create()
.searchInClasses(classHits)
.addSearchGroup("loginFlow", ["login", "signIn", "auth"])
.addSearchGroup("profileFlow", ["profile", "avatar", "nickname"]);
const groups = bridge.batchFindMethodUsingStrings(query);
const loginHits = groups.get("loginFlow");
log(`loginFlow=${loginHits ? loginHits.size() : 0}`);
} finally {
bridge.close();
}
这一层多出来的两个范围入口
| API | 说明 |
|---|---|
searchInClasses(classHits) | 只在这些类的方法里搜 |
searchInMethods(methodHits) | 只在这些方法结果里继续筛 |
groups(...) / addSearchGroup(...) 规则
这部分一定要看,因为这里有源码级约束。
规则 1:组名必须唯一
同一次查询里,下面这种写法会出问题:
addSearchGroup("loginFlow", ["login"])
addSearchGroup("loginFlow", ["auth"])
因为 groupName 不能重复。
规则 2:至少要有一组
空分组不能跑。
也就是说,你不能只写:
BatchFindMethodUsingStrings.create().searchPackages("com.example")
而不加任何 group。
规则 3:拿结果时按组名取
这两个 API 返回的不是单个列表,而是:
Map<String, ClassDataList>Map<String, MethodDataList>
所以日常拿结果是这样:
const groups = bridge.batchFindMethodUsingStrings(query);
const loginHits = groups.get("loginFlow");
const profileHits = groups.get("profileFlow");
结果对象怎么用
很多人卡住不是因为“查不到”,而是因为“查到了以后不知道下一步干嘛”。
这部分就是专门讲这个的。
ClassData
bridge.findClass(...) 返回的是 ClassDataList,里面每一项都是 ClassData。
最常用字段和属性
| 名字 | 含义 |
|---|---|
descriptor | 类描述符 |
name | 完整类名,如 com.example.LoginManager |
simpleName | 简单类名,如 LoginManager |
isArray | 是否数组类型,例如 java.lang.String[]、int[] 这类会是 true |
sourceFile | 源文件名,如 LoginManager.java |
modifiers | 修饰符位 |
superClass | 父类对应的 ClassData,没有时可能是 null |
interfaces | 接口列表 |
interfaceCount | 接口数量 |
methods | 类里声明的方法列表,包含 <init> 和 <clinit> |
methodCount | 声明的方法数量 |
fields | 类里声明的字段列表 |
fieldCount | 声明的字段数量 |
annotations | 类注解列表 |
ClassData.isArray 到底看什么
isArray 看的不是“这个类里有没有数组字段”,而是:
这个 ClassData 自己代表的是不是数组类型。
也就是说:
ClassData.name 可能长什么样 | isArray |
|---|---|
java.lang.String | false |
com.example.LoginManager | false |
java.lang.String[] | true |
int[] | true |
如果你拿到的是数组类,后面做反射或打印结果时,通常就要意识到这不是普通对象类了。
const bridge = openDexKit();
try {
const classHit = bridge.getClassData("java.lang.String[]");
if (classHit) {
log(`name=${classHit.name}`);
log(`descriptor=${classHit.descriptor}`);
log(`isArray=${classHit.isArray}`);
}
} finally {
bridge.close();
}
这里也顺手提醒一个容易写错的点:
classHit.isArray是布尔属性- 不是
classHit.isArray()
最常用实例方法
const clazz = classHit.getInstance(lpparam.classLoader);
getInstance(classLoader) 的作用是:
- 把
ClassData转回运行时真正的 JavaClass
这通常是你后面接反射或 Hook 的关键一步。
MethodData
bridge.findMethod(...) 返回的是 MethodDataList。
最常用字段和属性
| 名字 | 含义 |
|---|---|
descriptor | 完整方法描述符 |
methodSign | 只有“参数 + 返回值”那一段的签名,例如 (Ljava/lang/String;)V |
name / methodName | 方法名 |
className / declaredClassName | 声明该方法的类名 |
declaredClass | 声明该方法的 ClassData |
paramTypeNames | 参数类型名列表 |
paramCount | 参数数量 |
paramTypes | 参数类型对应的 ClassDataList |
returnTypeName | 返回值类型名 |
returnType | 返回值对应的 ClassData |
isConstructor | 是否构造方法 |
isStaticInitializer | 是否静态初始化块 |
isMethod | 是否普通方法,也就是既不是 <init> 也不是 <clinit> |
annotations | 方法注解 |
paramNames | 参数名列表(如果有) |
paramAnnotations | 参数注解 |
opCodes | opcode 数值列表 |
opNames | smali 指令名列表 |
callers | 谁调用了这个方法 |
invokes | 这个方法又调用了谁 |
usingStrings | 方法里出现的字符串 |
usingFields | 方法里用到的字段列表 |
isConstructor / isStaticInitializer / isMethod 三个值怎么理解
这 3 个字段是拿来判断“当前这条 MethodData 到底属于哪一类成员”的。
源码里的判断规则很直接:
isConstructor:方法名是不是<init>isStaticInitializer:方法名是不是<clinit>isMethod:只要前两个都不是,它就是普通方法
你可以直接按下面这张表记:
| 实际成员 | isConstructor | isStaticInitializer | isMethod |
|---|---|---|---|
构造方法 <init> | true | false | false |
静态初始化块 <clinit> | false | true | false |
普通方法 login / onCreate / getToken | false | false | true |
这组字段很适合在你把一批方法结果转回反射对象前,先做分流:
const hit = methodHits.firstOrNull();
if (hit) {
if (hit.isConstructor) {
log("这是构造方法");
} else if (hit.isStaticInitializer) {
log("这是静态初始化块");
} else if (hit.isMethod) {
log("这是普通方法");
}
}
这几个 isXxx 现在在 JS 里按真正的布尔属性工作
这一点是最近桥接层专门修过的,值得单独记住。
像下面这批字段:
MethodData.isConstructorMethodData.isStaticInitializerMethodData.isMethodClassData.isArray
现在在 JSXHook 的 JS 运行时里,直接读出来就是 true / false。
也就是说你应该这样写:
if (hit.isMethod) {
const method = hit.getMethodInstance(lpparam.classLoader);
method.setAccessible(true);
log(method);
}
而不是写成:
hit.isMethod()
如果你以前见过“明明不是构造方法,但 if (hit.isConstructor) 还总是进分支”的怪现象,那就是旧版本 Rhino 包装把 Kotlin 的 isXxx 属性当成了可调用对象。现在这批已经按布尔属性处理了。
name / methodName 现在可以直接喂给反射与 Hook 入口
新版桥接层在处理 callMethod()、callStaticMethod()、invoke()、hook()、hookAll()、replace() 这类入口里的“方法名”时,会先做一次“解包 -> 转字符串”。
所以像 hit.name、methodData.name 这种从 DexKit 返回的值,现在通常可以直接传,不用你先手动包一层 String(...)。
const hit = methodHits.firstOrNull();
if (hit) {
const clazz = hit.getClassInstance(lpparam.classLoader);
hookAll(clazz, hit.name, function(it) {
log(`hooked ${hit.name}`);
});
}
usingFields 里每项是什么
usingFields 不是直接给你一个字段名数组,它里面每一项都是:
fieldusingType
其中 usingType 当前只会是:
ReadWrite
也就是说你不仅能知道“这个方法用到了哪个字段”,还能知道它是读还是写。
最常用实例方法
| 方法 | 什么时候用 |
|---|---|
getMethodInstance(classLoader) | 当前结果是普通方法 |
getConstructorInstance(classLoader) | 当前结果是构造方法 |
getClassInstance(classLoader) | 想拿到声明这个方法的 Class |
例子
const hit = methodHits.firstOrNull();
if (hit) {
if (hit.isConstructor) {
const ctor = hit.getConstructorInstance(lpparam.classLoader);
ctor.setAccessible(true);
log(`ctor=${ctor}`);
} else {
const method = hit.getMethodInstance(lpparam.classLoader);
method.setAccessible(true);
log(`method=${method}`);
}
}
FieldData
bridge.findField(...) 返回的是 FieldDataList。
最常用字段和属性
| 名字 | 含义 |
|---|---|
descriptor | 完整字段描述符 |
name / fieldName | 字段名 |
className / declaredClassName | 声明该字段的类名 |
typeName | 字段类型名 |
declaredClass | 定义字段的 ClassData |
type | 字段类型对应的 ClassData |
annotations | 字段注解 |
readers | 会读这个字段的方法列表 |
writers | 会写这个字段的方法列表 |
name / fieldName 现在可以直接喂给字段 API
新版桥接层在处理 getField()、setField()、getStaticField()、setStaticField() 的字段名时,也会先做一次“解包 -> 转字符串”。
所以你从 DexKit 拿到 fieldHit.name、fieldData.name 之后,可以直接继续做字段读写:
const fieldHit = fieldHits.firstOrNull();
if (fieldHit) {
const fieldName = fieldHit.name;
log(getField(profile, fieldName));
setField(profile, fieldName, "patched-by-jsxhook");
}
最常用实例方法
const field = fieldHit.getFieldInstance(lpparam.classLoader);
field.setAccessible(true);
ClassDataList / MethodDataList / FieldDataList
这 3 个返回列表不是纯 JS 数组,而是 DexKit 自己的列表类型。
但它们底层都继承自 ArrayList,日常很好用。
最常用的列表操作
| API | 作用 |
|---|---|
.size() | 看命中数量 |
.get(index) | 取某一项 |
.firstOrNull() | 取第一项,空时返回 null |
.single() | 期望只有唯一结果;空或多条都会抛异常 |
一个非常实用的继续筛选能力
它们自己还能继续接查询:
| 当前列表 | 能继续做什么 |
|---|---|
ClassDataList | .findClass(...)、.findMethod(...)、.findField(...) |
MethodDataList | .findMethod(...) |
FieldDataList | .findField(...) |
例子
const classHits = bridge.findClass(
FindClass.create().matcher(
ClassMatcher.create().usingStrings("login", "token")
)
);
const methodHits = classHits.findMethod(
FindMethod.create().matcher(
MethodMatcher.create().name("login", StringMatchType.Contains)
)
);
这比你自己手工再 searchInClass(classHits) 更顺手,尤其是连续缩范围时。
新手为什么别太早用 .single()
因为 .single() 的要求非常苛刻:
- 0 条不行
- 多条也不行
所以刚开始调查询时,更稳的是:
const hit = methodHits.firstOrNull();
等你把条件收得非常准了,再改成 .single()。
3 个完整实战例子
例子 1:从字符串宽搜到真正的 Method
const FindClass = imports("org.luckypray.dexkit.query.FindClass");
const FindMethod = imports("org.luckypray.dexkit.query.FindMethod");
const ClassMatcher = imports("org.luckypray.dexkit.query.matchers.ClassMatcher");
const MethodMatcher = imports("org.luckypray.dexkit.query.matchers.MethodMatcher");
const StringMatchType = imports("org.luckypray.dexkit.query.enums.StringMatchType");
const bridge = openDexKitByClassLoader(false);
try {
const classHits = bridge.findClass(
FindClass.create()
.searchPackages("com.example")
.matcher(
ClassMatcher.create()
.usingStrings("login", "token")
.className("manager", StringMatchType.Contains)
)
);
const methodHits = classHits.findMethod(
FindMethod.create().matcher(
MethodMatcher.create()
.name("login", StringMatchType.Contains)
.paramCount(2)
.usingStrings("token")
)
);
const hit = methodHits.firstOrNull();
if (!hit) {
log("no method hit");
return;
}
log(`descriptor=${hit.descriptor}`);
const method = hit.getMethodInstance(lpparam.classLoader);
method.setAccessible(true);
log(`method=${method}`);
} finally {
bridge.close();
}
例子 2:按“写字段的方法”反推 token 字段
const FindField = imports("org.luckypray.dexkit.query.FindField");
const FieldMatcher = imports("org.luckypray.dexkit.query.matchers.FieldMatcher");
const bridge = openDexKit();
try {
const fieldHits = bridge.findField(
FindField.create().matcher(
FieldMatcher.create()
.type("java.lang.String")
.addWriteMethod("Lcom/example/LoginManager;->saveToken(Ljava/lang/String;)V")
)
);
const hit = fieldHits.firstOrNull();
if (!hit) {
log("no field hit");
return;
}
log(`field=${hit.descriptor}`);
log(`readers=${hit.readers.size()}`);
log(`writers=${hit.writers.size()}`);
} finally {
bridge.close();
}
例子 3:一次跑多组关键词,按组取方法结果
const FindClass = imports("org.luckypray.dexkit.query.FindClass");
const BatchFindMethodUsingStrings = imports(
"org.luckypray.dexkit.query.BatchFindMethodUsingStrings"
);
const ClassMatcher = imports("org.luckypray.dexkit.query.matchers.ClassMatcher");
const StringMatchType = imports("org.luckypray.dexkit.query.enums.StringMatchType");
const bridge = openDexKit();
try {
const classHits = bridge.findClass(
FindClass.create().matcher(
ClassMatcher.create().className("service", StringMatchType.Contains)
)
);
const query = BatchFindMethodUsingStrings.create()
.searchInClasses(classHits)
.addSearchGroup("loginFlow", ["login", "signIn", "auth"])
.addSearchGroup("profileFlow", ["profile", "avatar", "nickname"])
.addSearchGroup("payFlow", ["pay", "order", "trade"]);
const groups = bridge.batchFindMethodUsingStrings(query);
const loginHits = groups.get("loginFlow");
const profileHits = groups.get("profileFlow");
const payHits = groups.get("payFlow");
log(`login=${loginHits ? loginHits.size() : 0}`);
log(`profile=${profileHits ? profileHits.size() : 0}`);
log(`pay=${payHits ? payHits.size() : 0}`);
} finally {
bridge.close();
}
常见误区和排错
误区 1:把 DexKit 当成 live object 脚本引擎
如果你写的是:
Java.perform(...)
Java.use(...)
那你走的已经不是这套心智模型了。
DexKit 更像“先定位静态结构,再转回反射 / Hook”。
误区 2:openDexKit(undefined) 也会自动回退
不建议这么赌。
当前包装层是先 toString() 再判空,最稳还是:
- 完全不传
- 或明确传字符串路径
误区 3:openDexKitByClassLoader("true") 也能当布尔
不行。
这里稳定可用的只有:
openDexKitByClassLoader(true);
openDexKitByClassLoader(false);
误区 4:类名 / 方法描述符 / 字段描述符混着传
尤其这 3 个地方最容易错:
getClassData(...):字符串可传类名,也可传类描述符getMethodData(...):字符串版本只传方法描述符getFieldData(...):字符串版本只传字段描述符
如果你把:
"login"
喂给 getMethodData(...),那不是“模糊查”,而是参数形式就不对。
误区 5:默认匹配方式都一样
不是。
name(...)/className(...)默认更偏精确usingStrings("...")默认更偏包含
写到宽搜逻辑时,尽量把 StringMatchType 明确写出来。
误区 6:第一轮就把条件叠太死
典型表现:
- 类名精确
- 包名精确
- 参数个数写死
- 返回值写死
- 还顺手加了 3 个
usingStrings
最后 0 命中,再开始怀疑 DexKit 不工作。
更稳的顺序永远是:
- 先宽搜
- 看命中长什么样
- 再加限制
误区 7:拿到结果就直接 .single()
新手调查询时更推荐:
log(matches.size());
const hit = matches.firstOrNull();
等你已经能稳定命中唯一结果,再上 .single()。
误区 8:查完不 close()
DexKitBridge 底下有 native 资源。
不养成 try/finally 习惯,脚本越写越长时很容易把自己坑进去。
最后给你一条最实用的选路口诀
你可以直接照这个顺序选:
- 就查当前 App 自己 apk:先
openDexKit() - 更在意运行时真实加载链:先
openDexKitByClassLoader(false) - 要自己控 loader:直接
DexKitBridge.create(loader, false) - 已知精确 descriptor:优先
descriptor(...) - 还在摸索阶段:优先
Contains - 已经很确定目标:再切
Equals - 上一轮已经有类结果:直接
classHits.findMethod(...)/classHits.findField(...) - 查完要接反射或 Hook:把结果转回
getInstance(...)/getMethodInstance(...)/getFieldInstance(...)
把这页吃透以后,DexKit 这块就不会再停留在“知道有这个功能”,而是真的能自己一层一层把目标筛出来了。
后面如果你要把查到的结果继续接到反射调用,可以直接接着看 反射与类操作。
