From 20663f54d8373383605491ede713ed4d3041a79c Mon Sep 17 00:00:00 2001 From: fankesyooni Date: Wed, 21 Sep 2022 14:31:27 +0800 Subject: [PATCH] Added DexClassFinder and toClass, hasClass function using in reflection documentation --- .../zh-cn/api/special-features/reflection.md | 352 +++++++++++++++++- 1 file changed, 348 insertions(+), 4 deletions(-) diff --git a/docs-source/src/zh-cn/api/special-features/reflection.md b/docs-source/src/zh-cn/api/special-features/reflection.md index abfb6bb0..13556b89 100644 --- a/docs-source/src/zh-cn/api/special-features/reflection.md +++ b/docs-source/src/zh-cn/api/special-features/reflection.md @@ -8,15 +8,359 @@ ### 对象转换 -> 敬请期待。 +假设我们要得到一个不能直接调用的 `Class`,通常情况下,我们可以使用标准的反射 API 去查找这个 `Class`。 + +> 示例如下 + +```kotlin +// 默认 ClassLoader 环境下的 Class +var instance = Class.forName("com.demo.Test") +// 指定 ClassLoader 环境下的 Class +val customClassLoader: ClassLoader? = ... // 假设这个就是你的 ClassLoader +var instance = customClassLoader?.loadClass("com.demo.Test") +``` + +这种写法大概不是很友好,此时 `YukiHookAPI` 就为你提供了一个可在任意地方使用的语法糖。 + +以上写法换做 `YukiHookAPI` 可写作如下形式。 + +> 示例如下 + +```kotlin +// 直接得到这个 Class +// 如果当前正处于 PackageParam 环境,那么你可以不需要考虑 ClassLoader +var instance = "com.demo.Test".toClass() +// 自定义 Class 所在的 ClassLoader +val customClassLoader: ClassLoader? = ... // 假设这个就是你的 ClassLoader +var instance = "com.demo.Test".toClass(customClassLoader) +``` + +我们还可以通过映射来得到一个存在的 `Class` 对象。 + +> 示例如下 + +```kotlin +// 假设这个 Class 是能够被直接得到的 +var instance = classOf() +// 我们同样可以自定义 Class 所在的 ClassLoader,这对于 stub 来说非常有效 +val customClassLoader: ClassLoader? = ... // 假设这个就是你的 ClassLoader +var instance = classOf(customClassLoader) +``` + +::: tip + +更多功能请参考 [String.toClass](../public/com/highcapable/yukihookapi/hook/factory/ReflectionFactory#string-toclass-ext-method)、[PackageParam → String+VariousClass.toClass](../public/com/highcapable/yukihookapi/hook/param/PackageParam#string-variousclass-toclass-i-ext-method)、[classOf](../public/com/highcapable/yukihookapi/hook/factory/ReflectionFactory#classof-method) 方法。 + +::: ### 存在判断 -> 敬请期待。 +假设我们要判断一个 `Class` 是否存在,通常情况下,我们可以使用标准的反射 API 去查找这个 `Class` 通过异常来判断是否存在。 -### 模糊查询 +> 示例如下 -> 敬请期待。 +```kotlin +// 默认 ClassLoader 环境下的 Class +var isExist = try { + Class.forName("com.demo.Test") + true +} catch (_: Throwable) { + false +} +// 指定 ClassLoader 环境下的 Class +val customClassLoader: ClassLoader? = ... // 假设这个就是你的 ClassLoader +var isExist = try { + customClassLoader?.loadClass("com.demo.Test") + true +} catch (_: Throwable) { + false +} +``` + +这种写法大概不是很友好,此时 `YukiHookAPI` 就为你提供了一个可在任意地方使用的语法糖。 + +以上写法换做 `YukiHookAPI` 可写作如下形式。 + +> 示例如下 + +```kotlin +// 判断这个 Class 是否存在 +// 如果当前正处于 PackageParam 环境,那么你可以不需要考虑 ClassLoader +var isExist = "com.demo.Test".hasClass() +// 自定义 Class 所在的 ClassLoader +val customClassLoader: ClassLoader? = ... // 假设这个就是你的 ClassLoader +var isExist = "com.demo.Test".hasClass(customClassLoader) +``` + +::: tip + +更多功能请参考 [String.hasClass](../public/com/highcapable/yukihookapi/hook/factory/ReflectionFactory#string-hasclass-ext-method)、[PackageParam → String.hasClass](../public/com/highcapable/yukihookapi/hook/param/PackageParam#string-hasclass-i-ext-method) 方法。 + +::: + +### 模糊查找  + +在 R8 等工具混淆后的宿主 **Dex** 中的 `Class` 名称将会难以分辨,且不确定其正确位置,不能直接通过 [对象转换](#对象转换) 来得到。 + +此时就有了 `DexClassFinder`,它的作用是通过需要查找的 `Class` 中的字节码特征来确定这个 `Class` 的实例。 + +::: warning + +目前 **DexClassFinder** 的功能尚在试验阶段,由于仅通过 Java 层实现查找功能,在宿主 **Class** 过多时性能可能不能达到最佳水平,如果发生查找不到、定位有误的问题欢迎向我们反馈。 + +由于是反射层面的 API,目前它只能通过**类与成员**的特征来定位指定的 **Class**,不能通过指定字节码中的字符串和方法内容特征来进行定位。 + +查找 **Class** 的速度取决于当前设备的性能,目前主流的移动端处理器在 **10~15w** 数量的 **Class** 中条件不算复杂的情况下大概在 **3~10s** 区间,条件稍微复杂的情况下最快速度能达到 **25s** 以内,匹配到的同类型 **Class** 越多速度越慢。 + +::: + +#### 开始使用 + +下面是一个简单的用法示例。 + +假设下面这个 `Class` 是我们想要得到的,其中的名称经过了混淆,在每个版本可能都不一样。 + +> 示例如下 + +```java:no-line-numbers +package com.demo; + +public class a extends Activity implements Serializable { + + public a(String var1) { + // ... + } + + private String a; + + private String b; + + private boolean a; + + protected void onCreate(Bundle var1) { + // ... + } + + private static void a(String var1) { + // ... + } + + private String a(boolean var1, String var2) { + // ... + } + + private void a() { + // ... + } + + public void a(boolean var1, a var2, b var3, String var4) { + // ... + } +} +``` + +此时,我们想得到这个 `Class`,可以直接使用 `ClassLoader.searchClass` 方法。 + +在 `PackageParam` 中,你可以直接使用 `searchClass` 方法,它将自动指定 `appClassLoader`。 + +下方演示的条件中每一个都是可选的,条件越复杂定位越精确,同时性能也会越差。 + +> 示例如下 + +```kotlin +searchClass { + // 从指定的包名范围搜索,实际使用时,你可以同时指定多个包名范围 + from("com.demo") + // 指定当前 Class 的 getSimpleName 的结果,你可以直接对这个字符串进行逻辑判断 + // 这里我们不确定它的名称是不是 a,可以只判断字符串长度 + simpleName { it.length == 1 } + // 指定继承的父类对象,如果是存在的 stub,可以直接用泛型表示 + extends() + // 指定继承的父类对象,可以直接写为完整类名,你还可以同时指定多个 + extends("android.app.Activity") + // 指定实现的接口,如果是存在的 stub,可以直接用泛型表示 + implements() + // 指定实现的接口,可以直接写为完整类名,你还可以同时指定多个 + implements("java.io.Serializable") + // 指定构造方法的类型与样式,以及在当前类中存在的个数 count + constructor { param(StringType) }.count(num = 1) + // 指定变量的类型与样式,以及在当前类中存在的个数 count + field { type = StringType }.count(num = 2) + // 指定变量的类型与样式,以及在当前类中存在的个数 count + field { type = BooleanType }.count(num = 1) + // 直接指定所有变量在当前类中存在的个数 count + field().count(num = 3) + // 如果你认为变量的个数是不确定的,还可以使用如下自定义条件 + field().count(1..3) + field().count { it >= 3 } + // 指定方法的类型与样式,以及在当前类中存在的个数 count + method { + name = "onCreate" + param(BundleClass) + }.count(num = 1) + // 指定方法的类型与样式,同时指定修饰符,以及在当前类中存在的个数 count + method { + modifiers { isStatic && isPrivate } + param(StringType) + returnType = UnitType + }.count(num = 1) + // 指定方法的类型与样式,同时指定修饰符,以及在当前类中存在的个数 count + method { + modifiers { isPrivate && isStatic.not() } + param(BooleanType, StringType) + returnType = StringType + }.count(num = 1) + // 指定方法的类型与样式,同时指定修饰符,以及在当前类中存在的个数 count + method { + modifiers { isPrivate && isStatic.not() } + emptyParam() + returnType = UnitType + }.count(num = 1) + // 指定方法的类型与样式,同时指定修饰符和模糊类型 VagueType,以及在当前类中存在的个数 count + method { + modifiers { isPrivate && isStatic.not() } + param(BooleanType, VagueType, VagueType, StringType) + returnType = UnitType + }.count(num = 1) + // 直接指定所有方法在当前类中存在的个数 count + method().count(num = 5) + // 如果你认为方法的个数是不确定的,还可以使用如下自定义条件 + method().count(1..5) + method().count { it >= 5 } + // 直接指定所有成员 (Member) 在当前类中存在的个数 count + // 成员包括:Field (变量)、Method (方法)、Constructor (构造方法) + member().count(num = 9) + // 所有成员中一定存在一个 static 修饰符,可以这样加入此条件 + member { + modifiers { isStatic } + } +}.get() // 得到这个 Class 本身的实例,找不到会返回 null +``` + +::: tip + +上述用法中对于 **Field**、**Method**、**Constructor** 的条件用法与 [Member 扩展](#member-扩展) 中的相关用法是一致的,仅有小部分区别。 + +更多功能请参考 [MemberRules](../public/com/highcapable/yukihookapi/hook/core/finder/classes/rules/MemberRules)、[FieldRules](../public/com/highcapable/yukihookapi/hook/core/finder/classes/rules/FieldRules)、[MethodRules](../public/com/highcapable/yukihookapi/hook/core/finder/classes/rules/MethodRules)、[ConstructorRules](../public/com/highcapable/yukihookapi/hook/core/finder/classes/rules/ConstructorRules)。 + +::: + +#### 异步查找 + +默认情况下 `DexClassFinder` 会使用同步方式查找 `Class`,会阻塞当前线程直到找到或找不到发生异常为止,若查找消耗的时间过长,可能会导致宿主发生 **ANR** 问题。 + +针对上述问题,我们可以启用异步,只需要加入参数 `async = true`,这将不需要你再次启动一个线程,API 已帮你处理好相关问题。 + +::: warning + +对于异步情况下你需要使用 **wait** 方法来得到结果,**get** 方法将不再起作用。 + +::: + +> 示例如下 + +```kotlin +searchClass(async = true) { + // ... +}.wait { class1 -> + // 得到异步结果 +} +searchClass(async = true) { + // ... +}.wait { class2 -> + // 得到异步结果 +} +``` + +这样我们的查找过程就是异步运行了,它将不会阻塞主线程,每个查找都将在单独的线程同时进行,可达到并行任务的效果。 + +#### 本地缓存 + +由于每次重新打开宿主都会重新进行查找,在宿主版本不变的情况下这是一种重复性能浪费。 + +此时我们可以通过指定 `name` 参数来对当前宿主版本的查找结果进行本地缓存,下一次将直接从本地缓存中读取查找到的类名。 + +本地缓存使用的是 `SharedPreferences`,它将被保存到宿主的数据目录中,在宿主版本更新后会重新进行缓存。 + +启用本地缓存后,将同时设置 `async = true`,你可以不需要再手动进行设置。 + +> 示例如下 + +```kotlin +searchClass(name = "com.demo.class1") { + // ... +}.wait { class1 -> + // 得到异步结果 +} +searchClass(name = "com.demo.class2") { + // ... +}.wait { class2 -> + // 得到异步结果 +} +``` + +如果你想手动清除本地缓存,可以使用如下方法清除当前版本的宿主缓存。 + +> 示例如下 + +```kotlin +// 直接调用,在宿主的 appContext 为空时可能会失败,失败会打印警告信息 +DexClassFinder.clearCache() +// 监听宿主的生命周期后调用 +onAppLifecycle { + onCreate { + DexClassFinder.clearCache(context = this) + } +} +``` + +你还可以清除指定版本的宿主缓存。 + +> 示例如下 + +```kotlin +// 直接调用,在宿主的 appContext 为空时可能会失败,失败会打印警告信息 +DexClassFinder.clearCache(versionName = "1.0", versionCode = 1) +// 监听宿主的生命周期后调用 +onAppLifecycle { + onCreate { + DexClassFinder.clearCache(context = this, versionName = "1.0", versionCode = 1) + } +} +``` + +#### 多重查找 + +如果你需要使用固定的条件同时查找一组 `Class`,那么你只需要使用 `all` 或 `waitAll` 方法来得到结果。 + +```kotlin +// 同步查找,使用 all 得到条件全部查找到的结果 +searchClass { + // ... +}.all().forEach { clazz -> + // 得到每个结果 +} +// 同步查找,使用 all { ... } 遍历每个结果 +searchClass { + // ... +}.all { clazz -> + // 得到每个结果 +} +// 异步查找,使用 waitAll 得到条件全部查找到的结果 +searchClass(async = true) { + // ... +}.waitAll { classes -> + classes.forEach { + // 得到每个结果 + } +} +``` + +::: tip + +更多功能请参考 [ClassLoader.searchClass](../public/com/highcapable/yukihookapi/hook/factory/ReflectionFactory#classloader-searchclass-ext-method)、[PackageParam.searchClass](../public/com/highcapable/yukihookapi/hook/param/PackageParam#searchclass-method) 方法。 + +::: ## Member 扩展