Merge new documentation

This commit is contained in:
2022-04-09 01:37:12 +08:00
parent c3fbaf553f
commit a455eec5de
52 changed files with 8270 additions and 104 deletions

347
docs/guide/example.md Normal file
View File

@@ -0,0 +1,347 @@
# 用法示例
> 这里介绍了 `YukiHookAPI` 的基本工作方式以及列举了简单的 Hook 例子和常用功能。
## 结构图解
> 下方的结构描述了 `YukiHookAPI` 的基本工作方式和原理。
```
Host Environment
└── YukiHookCreater
└── Class
└── MemberHookCreater
└── Member
├── Before
└── After
MemberHookCreater
└── Member
├── Before
└── After
...
```
> 上方的结构换做代码将可写为如下形式。
```kotlin
TargetClass.hook {
injectMember {
method {
// Your code here.
}
beforeHook {
// Your code here.
}
afterHook {
// Your code here.
}
}
}
```
## Demo
> 你可以在下方找到 API 提供的 Demo 来学习 `YukiHookAPI` 的使用方法。
- 宿主 APP Demo [点击这里查看](https://github.com/fankes/YukiHookAPI/tree/master/demo-app)
- 模块 APP Demo [点击这里查看](https://github.com/fankes/YukiHookAPI/tree/master/demo-module)
同时安装宿主和模块 Demo通过激活模块来测试宿主中被 Hook 的功能。
## 一个简单的 Hook 例子
假设,我们要 Hook `com.android.browser` 中的 `onCreate` 方法并弹出一个对话框。
`encase` 方法体中添加代码。
> 示例如下
```kotlin
loadApp(name = "com.android.browser") {
ActivityClass.hook {
injectMember {
method {
name = "onCreate"
param(BundleClass)
returnType = UnitType
}
afterHook {
AlertDialog.Builder(instance())
.setTitle("Hooked")
.setMessage("I am hook!")
.setPositiveButton("OK", null)
.show()
}
}
}
}
```
至此,`onCreate` 方法将被成功 Hook 并在 `com.android.browser` 中的每个 `Activity` 启动时弹出此对话框。
那么,我想继续 Hook `onStart` 方法要怎么做呢?
在刚刚的代码中,继续插入一个 `injectMember` 方法体即可。
> 示例如下
```kotlin
loadApp(name = "com.android.browser") {
ActivityClass.hook {
injectMember {
method {
name = "onCreate"
param(BundleClass)
returnType = UnitType
}
afterHook {
AlertDialog.Builder(instance())
.setTitle("Hooked")
.setMessage("I am hook!")
.setPositiveButton("OK", null)
.show()
}
}
injectMember {
method {
name = "onStart"
returnType = UnitType
}
afterHook {
// Your code here.
}
}
}
}
```
对于当前项目下没有的 `Class`,你可以使用 `stub` 方式或 `findClass` 方法来得到需要 Hook 的类。
比如,我要得到 `com.example.demo.TestClass`
> 示例如下
```kotlin
findClass(name = "com.example.demo.TestClass").hook {
injectMember {
// Your code here.
}
}
```
`com.example.demo` 是你要 Hook 的 APP那么写法可以更简单。
> 示例如下
```kotlin
findClass(name = "$packageName.TestClass").hook {
injectMember {
// Your code here.
}
}
```
到这里有些同学可能就开始说了,在某些场景下 `findClass` 显得有些繁琐。
因为可能有些同学有如下需求。
> 示例如下
```kotlin
const val TestClass = "com.example.demo.TestClass"
TestClass.hook {
injectMember {
// Your code here.
}
}
```
没关系,你还可以使用字符串类名直接创建一个 Hook。
> 示例如下
```kotlin
("$packageName.TestClass").hook {
injectMember {
// Your code here.
}
}
```
## 异常处理
> `YukiHookAPI` 重新设计了对异常的监听,任何异常都不会在 Hook 过程中抛出,避免打断下一个 Hook 流程导致 Hook 进程“死掉”。
你可以处理 Hook 方法过程发生的异常。
> 示例如下
```kotlin
injectMember {
// Your code here.
}.result {
// 处理 Hook 开始时的异常
onHookingFailure {}
// 处理 Hook 过程中的异常
onConductFailure { param, throwable -> }
// 处理全部异常
onAllFailure {}
// ...
}
```
你还可以处理 Hook 的 `Class` 不存在时发生的异常。
> 示例如下
```kotlin
TargetClass.hook {
injectMember {
// Your code here.
}
}.onHookClassNotFoundFailure {
// Your code here.
}
```
你还可以处理查找方法时的异常。
> 示例如下
```kotlin
method {
// Your code here.
}.onNoSuchMethod {
// Your code here.
}
```
这里介绍了可能发生的常见异常,若要了解更多请参考 [API 异常处理](config/api-exception.md)。
## 状态监听
在使用 `XposedHelper` 的同学往往会在 Hook 后打印 `UnHook` 的方法确定是否 Hook 成功。
`YukiHookAPI` 中,你可以用以下方法方便地重新实现这个功能。
首先我们可以监听 Hook 已经准备开始。
> 示例如下
```kotlin
YourClass.hook {
// Your code here.
}.onPrepareHook {
loggerD(msg = "$instanceClass hook start")
}
```
!> 请注意 `instanceClass` 建议只在 `onPrepareHook` 中使用,万一被 Hook 的 `Class` 不存在将会抛出无法拦截的异常导致 Hook 进程“死掉”。
然后,我们还可以对 Hook 的方法结果进行监听是否成功。
> 示例如下
```kotlin
injectMember {
// Your code here.
}.onHooked { member ->
loggerD(msg = "$member has hooked")
}
```
## 扩展用法
> 你可以在 Hook 过程中使用下面的方法方便地实现各种判断和功能。
### 多个宿主
如果你的模块需要同时处理多个 APP 的 Hook 事件,你可以使用 `loadApp` 方法体来区分你要 Hook 的 APP。
> 示例如下
```kotlin
loadApp(name = "com.android.browser") {
// Your code here.
}
loadApp(name = "com.android.phone") {
// Your code here.
}
```
详细用法可 [点击这里](api/document?id=loadapp-method) 进行查看。
### 多个进程
如果你 Hook 的宿主 APP 有多个进程,你可以使用 `withProcess` 方法体来对它们分别进行 Hook。
> 示例如下
```kotlin
withProcess(mainProcessName) {
// Your code here.
}
withProcess(name = "$packageName:tool") {
// Your code here.
}
```
详细用法可 [点击这里](api/document?id=withprocess-method) 进行查看。
## 写法优化
为了使代码更加简洁,你可以删去 `YukiHookAPI` 的名称,将你的 `onHook` 入口写作 `lambda` 形式。
> 示例如下
```kotlin
override fun onHook() = encase {
// Your code here.
}
```
## Xposed 模块判断自身激活状态
通常情况下,我们会选择写一个方法,使其返回 `false`,然后 Hook 掉这个方法使其返回 `true` 来证明 Hook 已经生效。
`YukiHookAPI` 中你完全不需要再这么做了,`YukiHookAPI` 已经帮你封装好了这个操作,你可以直接进行使用。
现在,你可以直接使用 `isXposedModuleActive` 在模块中判断自身是否被激活。
> 示例如下
```kotlin
if(isXposedModuleActive) {
// Your code here.
}
```
由于一些特殊原因,在太极、无极中的模块无法使用标准方法检测激活状态。
此时你可以在 `Activity` 中使用 `isTaiChiModuleActive` 判断自身是否被激活。
> 示例如下
```kotlin
if(isTaiChiModuleActive) {
// Your code here.
}
```
若你想使用两者得兼的判断方案,`YukiHookAPI` 同样为你封装了便捷的方式。
此时你可以在 `Activity` 中使用 `isModuleActive` 判断自身是否在 Xposed 或太极、无极中被激活。
> 示例如下
```kotlin
if(isModuleActive) {
// Your code here.
}
```
若要了解更多可 [点击这里](api/document?id=ismoduleactive-field) 进行查看。
!> 除了提供标准 API 的 Hook 框架之外,其它情况下模块可能都将无法判断自己是否被激活。

142
docs/guide/home.md Normal file
View File

@@ -0,0 +1,142 @@
# 介绍
> 这是一个 Hook API 框架,本身不提供任何 Hook 功能,需要 Xposed 基础 API 的支持。
## 背景
这是一个使用 `Kotlin` 重新构建的高效 Xposed Hook API。
名称取自 [《ももくり》女主 栗原 雪(Yuki)](https://www.bilibili.com/bangumi/play/ss5016)。
前身为 [开发学习项目](https://github.com/fankes/TMore) 中使用的 Innocent Xposed API现在重新命名并开源。
## 用途
`YukiHookAPI` 完全采用 `Kotlin` `lambda` 语法构建。
抛弃原始不太友好的 `XposedHelper`,你可以使用它来轻松创建 Xposed 模块以及轻松实现自定义 Hook API。
## 语言要求
请使用 `Kotlin`,框架部分代码构成同样兼容 `Java` 但基础 Hook 场景的实现<b>可能完全无法使用</b>
文档全部的 Demo 示例代码都将使用 `Kotlin` 进行描述,如果你完全不会使用 `Kotlin` 那你将有可能无法使用 `YukiHookAPI`
## 功能特性
- <b>Xposed 模块开发</b>
自动构建程序可以帮你快速创建一个 Xposed 模块,完全省去配置入口类和 `xposed_init` 文件。
- <b>轻量优雅</b>
拥有一套强大、优雅和人性化的 `Kotlin Lambda Hook API`,可以帮你快速实现 `Method``Constructor``Field` 的查找以及 Hook。
- <b>高效调试</b>
拥有丰富的调试日志功能,细到每个 Hook 方法的名称、所在类以及查找耗时,可进行快速调试和排错。
- <b>方便移植</b>
原生支持 Xposed API 用法,并原生对接 Xposed API拥有 Xposed API 的 Hook 框架都能快速对接 Yuki Hook API。
- <b>支持混淆</b>
使用 `YukiHookAPI` 构建的 Xposed 模块原生支持 R8 压缩优化混淆,混淆不会破坏 Hook 入口点R8 下无需任何其它配置。
- <b>快速上手</b>
简单易用,不需要繁琐的配置,不需要十足的开发经验,搭建环境集成依赖即可立即开始使用。
## 灵感来源
以前,我们在构建 Xposed 模块的时候,首先需要在 `assets` 下创建 `xposed_init` 文件。
然后,将自己的入口类名手动填入文件中,使用 `XposedHelper` 去实现我们的 Hook 逻辑。
`Kotlin` 作为 Android 主要开发语言以来,这套 API 用起来确实已经不是很优雅了。
有没有什么 <b>好用、轻量、优雅</b> 的解决办法呢?
本着这样的想法,`YukiHookAPI` 诞生了。
现在,我们只需要编写少量的代码,一切时间开销和花费交给自动化处理。
> 示例如下
<!-- tabs:start -->
#### **Yuki Hook API**
```kotlin
@InjectYukiHookWithXposed
class MainHook : YukiHookXposedInitProxy {
override fun onHook() = encase {
loadApp(name = "com.android.browser") {
ActivityClass.hook {
injectMember {
method {
name = "onCreate"
param(BundleClass)
}
beforeHook {
// Your code here.
}
afterHook {
// Your code here.
}
}
}
}
}
}
```
#### **Xposed API**
```kotlin
class MainHook : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName == "com.android.browser")
XposedHelpers.findAndHookMethod(
Activity::class.java.name,
lpparam.classLoader,
"onCreate",
Bundle::class.java,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam?) {
// Your code here.
}
override fun afterHookedMethod(param: MethodHookParam?) {
// Your code here.
}
})
}
}
```
<!-- tabs:end -->
是的,你没有看错,仅仅就需要这几行代码,就一切安排妥当。
代码量少,逻辑清晰,借助高效强大的 `YukiHookAPI`,你就可以实现一个非常简单的 Xposed 模块。
## 支持的 Hook 框架
以下是 `YukiHookAPI` 支持的 `Hook Framework` 以及 Xposed 框架。
| Hook Framework | ST | Describe |
| --------------------------------------------------------- | --- | ----------------------------------------------------------------------------------------- |
| [LSPosed](https://github.com/LSPosed/LSPosed) | ✅ | 多场景下稳定使用 |
| [EdXposed](https://github.com/ElderDrivers/EdXposed) | ✅ | 部分兼容 |
| [Pine](https://github.com/canyie/pine) | ⭕ | 可以使用 |
| [SandHook](https://github.com/asLody/SandHook) | ⭕ | 可以使用 |
| [Whale](https://github.com/asLody/whale) | ⭕ | 需要 [xposed-hook-based-on-whale](https://github.com/WindySha/xposed-hook-based-on-whale) |
| [YAHFA](https://github.com/PAGalaxyLab/YAHFA) | ❗ | 需要自行实现 Xposed API |
| [FastHook](https://github.com/turing-technician/FastHook) | ❗ | 需要自行实现 Xposed API |
| [Epic](https://github.com/tiann/epic) | ❗ | 需要自行对接 [Dexposed](https://github.com/alibaba/dexposed) |
| [TaiChi](https://github.com/taichi-framework/TaiChi) | ⭕ | 可以作为模块使用 |
| [Xposed](https://github.com/rovo89/Xposed) | ❎ | 未测试,不再推荐使用 |

63
docs/guide/knowledge.md Normal file
View File

@@ -0,0 +1,63 @@
# 基础知识
> 这里介绍了 Xposed 以及 Hook 的工作原理,已经了解的同学可以略过。
## Xposed 是什么
> Xposed 框架(Xposed Framework)是一套开源的、在 Android 高权限模式下运行的框架服务,可以在不修改 APK 文件的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。
上述内容复制自百度百科。
## Xposed 能做什么
> 下方的结构描述了 Xposed 的基本工作方式和原理。
```
Xposed Framework
└── App's Environment
└── Hooker (Hooked)
...
App's Environment
└── Hooker (Hooked)
...
...
```
我们可以在宿主(APP)运行时通过注入宿主(APP)来达到控制其行为的最终目的。
Xposed 的这种运行方式被称为<b>寄生</b>Xposed 模块跟随宿主的生命周期,在宿主的生命周期内完成自己的生命历程。
我们可以通过反射的方式调用宿主的方法、变量、构造方法,以及使用 `XposedBridge` 所提供的 Hook 操作动态地在宿主(APP)要执行的方法前后插入自己的代码,或完全替换目标,甚至是拦截。
## 发展过程
如今的 Xposed 管理器已完全被其衍生作品替代,而 <b>SuperSU</b> 的时代也已经落幕了,现在,借助 <b>Magisk</b> 使后面的一切又成为了可能。
> 其发展史大致可分为 <b>Xposed(Dalvik)</b> → <b>Xposed(ART)</b> → <b>Xposed(Magisk)</b> → <b>EdXposed(Riru)</b>/<b>LSPosed(Riru/Zygisk)</b>
## 衍生产品
> 下方的结构描述了类似 Xposed 的 Hook Framework 的工作方式和原理。
```
App's Environment
└── Hook Framework
└── Hooker (Hooked)
...
```
通过 Xposed 的运行原理,从而衍生了很多同类型框架,随着当今时代的移动设备获取 Root 权限甚至刷机越来越困难且不是刚需的时候,一些免 Root 框架也随之产生,例如<b>太极</b>
这些在 ART 层面上的 Hook 框架同样也可不借助 Xposed API 完成其和 Xposed 原理一样的 Hook 流程,免 Root 的运行原理为修改 APK 并将 Hook 进程注入宿主,通过外部模块对其进行控制。
另外一种产品就是利用 Android 运行环境现有的功能虚拟出一个完全与当前设备系统一样的环境,并在其中运行 APP这个就是虚拟 APP 技术 <b>VirtualApp</b>,后来衍生为 <b>VirtualXposed</b>
上述提到的免 Root 框架分别为<b>太极/无极</b><b>VirtualXposed/SandVXposed</b>
## YukiHookAPI 做了什么
自从 Xposed 出现到现在为止,除了开发者人人皆知的 `XposedHelper`,依然没有一套针对 `Kotlin` 打造的语法糖以及用法封装十分完善的 API。
本 API 框架的诞生就是希望在 Xposed 的如今时代,能让更多有动手能力的 Xposed 模块开发者少走弯路,更容易、更简单地完成整个开发流程。
未来,`YukiHookAPI` 将在使用 Xposed API 的目标基础上适配更多第三方 Hook 框架,使得整个生态得到完善,并帮助更多开发者让 Xposed 模块开发变得更加简单和易懂。

145
docs/guide/quick-start.md Normal file
View File

@@ -0,0 +1,145 @@
# 快速开始
> 集成 `YukiHookAPI` 到你的项目中。
## 环境要求
- Windows 7 及以上/macOS 10.14 及以上/Linux 发行版(Arch/Debian)
- Android Studio 4.1 及以上
- IntelliJ IDEA 2021.01 及以上
- Kotlin 1.6.0 及以上
- Android Gradle Plugin 7.0 及以上
- Gradle 7.0 及以上
## 集成依赖
在你的项目 `build.gradle` 中添加依赖。
> 示例如下
```gradle
repositories {
google()
mavenCentral()
// ❗若你的 Plugin 版本过低,作为 Xposed 模块使用务必添加,其它情况可选
maven { url "https://dl.bintray.com/kotlin/kotlin-eap" }
// ❗作为 Xposed 模块使用务必添加,其它情况可选
maven { url "https://api.xposed.info/" }
// MavenCentral 有 2 小时缓存,若无法集成最新版本请添加此地址
maven { url "https://s01.oss.sonatype.org/content/repositories/releases" }
}
```
在你的 app `build.gradle` 中添加 `plugin`
> 示例如下
```gradle
plugins {
// ❗作为 Xposed 模块使用务必添加,其它情况可选
id 'com.google.devtools.ksp' version '<version>'
}
```
在你的 app `build.gradle` 中添加依赖。
> 示例如下
```gradle
dependencies {
// 基础依赖
implementation 'com.highcapable.yukihookapi:api:<version>'
// ❗作为 Xposed 模块使用务必添加,其它情况可选
compileOnly 'de.robv.android.xposed:api:82'
// ❗作为 Xposed 模块使用务必添加,其它情况可选
ksp 'com.highcapable.yukihookapi:ksp-xposed:<version>'
}
```
请将 <b>&lt;version&gt;</b> 修改为 [这里](about/changelog) 的最新版本。
!> `YukiHookAPI``api``ksp-xposed` 依赖的版本必须一一对应,否则将会造成版本不匹配错误。
## 作为 Xposed 模块使用
在你的 `AndroidManifest.xml` 中添加基础代码。
> 示例如下
```xml
<!-- 设置为 Xposed 模块 -->
<meta-data
android:name="xposedmodule"
android:value="true" />
<!-- 设置你的模块描述 -->
<meta-data
android:name="xposeddescription"
android:value="填写你的 Xposed 模块描述" />
<!-- 最低 Xposed 版本号,若你正在使用 EdXposed/LSPosed建议最低为 93 -->
<meta-data
android:name="xposedminversion"
android:value="93" />
```
在你的项目中创建一个 Hook 入口类,继承于 `YukiHookXposedInitProxy` 并加入注释 `InjectYukiHookWithXposed`
!> 在默认配置情况下,你的入口类需要建立在你的包名的 hook 子包名下,假设你的包名为 `com.example.demo`,入口类应为 `com.example.demo.hook.你的入口类名称`
> 示例如下
```kotlin
@InjectYukiHookWithXposed
class MainHook : YukiHookXposedInitProxy {
override fun onHook() = YukiHookAPI.encase {
// Your code here.
}
}
```
然后,你就可以开始编写 Hook 代码了。
有关作为 Xposed 模块使用的相关配置详细内容,你可以 [点击这里](config/xposed-using) 继续阅读。
## 作为 Hook API 使用
### 集成方式
创建你的自定义 `Application`
!> 无论使用任何 `Hook Framework`,你都需要加入其对接的 Xposed 依赖支持。
!> 若目标 `Hook Framework` 没有集成 Xposed API 你需要自行实现并对接 `XposedBridge`
`attachBaseContext` 中添加 `YukiHookAPI.encase` 方法。
> 示例如下
```kotlin
override fun attachBaseContext(base: Context?) {
// 装载 Hook Framework
//
// Your code here.
//
// 装载 YukiHookAPI
YukiHookAPI.encase(base) {
// Your code here.
}
super.attachBaseContext(base)
}
```
然后,你就可以开始编写 Hook 代码了,方式与作为 Xposed 模块使用基本一致。
有关作为 Hook API 使用的相关配置详细内容,你可以 [点击这里](config/api-using) 继续阅读。
### 特别说明
!> 由于你使用了自定义的 Hook 框架而并非模块,~~`YukiHookModuleStatus`~~ ~~`YukiHookModulePrefs`~~ 功能将失效。

View File

@@ -0,0 +1,799 @@
# 特色功能
> 除了基本的 Hook 功能之外,`YukiHookAPI` 还为开发者提供了大量的语法糖和扩展用法。
## 字节码扩展功能
假设有一个这样的 `Class`
> 示例如下
```java
package com.demo;
public class Test {
public Test() {
// ...
}
public Test(boolean isInit) {
// ...
}
private static TAG = "Test";
private String a;
private boolean a;
private boolean isTaskRunning = false;
private static void init() {
// ...
}
private void doTask(String taskName) {
// ...
}
private void release(Release release, Function<boolean, String> function, Task task) {
// ...
}
private void stop() {
// ...
}
private String getName() {
// ...
}
private void b() {
// ...
}
private void b(String a) {
// ...
}
}
```
### 查询与反射调用
假设我们要得到 `doTask` 方法并执行,通常情况下,我们可以使用标准的反射 API 去查询这个方法。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用反射 API 调用并执行
Test::class.java.getDeclaredMethod("doTask", String::class.java).apply { isAccessible = true }.invoke(instance, "task_name")
```
这种写法大概不是很友好,此时 `YukiHookAPI` 就为你提供了一个可在任意地方使用的语法糖。
以上写法换做 `YukiHookAPI` 可写作如下形式。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "doTask"
param(StringType)
}.get(instance).call("task_name")
```
更多用法可参考 [MethodFinder](api/document?id=methodfinder-class)。
同样地,我们需要得到 `isTaskRunning` 变量也可以写作如下形式。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.field {
name = "isTaskRunning"
type = BooleanType
}.get(instance).self // self 为 Field 的实例对象
```
更多用法可参考 [FieldFinder](api/document?id=fieldfinder-class)。
也许你还想得到当前 `Class` 的构造方法,同样可以实现。
> 示例如下
```kotlin
Test::class.java.constructor {
param(BooleanType)
}.get().call(true) // 可创建一个新的实例
```
若想得到的是 `Class` 的无参构造方法,可写作如下形式。
> 示例如下
```kotlin
Test::class.java.constructor().get().call() // 可创建一个新的实例
```
更多用法可参考 [ConstructorFinder](api/document?id=constructorfinder-class)。
### 可选的查询条件
假设我们要得到 `Class` 中的 `getName` 方法,可以使用如下实现。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "getName"
returnType = StringType
}.get(instance).string() // 得到方法的结果
```
通过观察发现,这个 `Class` 中只有一个名为 `getName` 的方法,那我们可不可以再简单一点呢?
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "getName"
}.get(instance).string() // 得到方法的结果
```
是的,对于确切不会变化的方法,你可以精简查询条件,<b>`YukiHookAPI` 会默认按照字节码顺序匹配第一个查询到的结果</b>
问题又来了,这个 `Class` 中有一个 `release` 方法,但是它的方法参数好长,而且很多的类型都无法直接得到。
通常情况下我们会使用 `param(...)` 来查询这个方法,但是有没有更简单的方法呢。
此时,在确定方法唯一性后,你可以使用 `paramCount` 来查询到这个方法。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "release"
// 此时我们不必确定方法参数具体类型,写个数就好
paramCount = 3
}.get(instance) // 得到这个方法
```
### 静态字节码
有些方法和变量在类中是静态的实现,这个时候,我们不需要传入实例就可以调用它们。
假设我们这次要得到静态变量 `TAG` 的内容。
> 示例如下
```kotlin
Test::class.java.field {
name = "TAG"
type = StringType
}.get().string() // Field 的类型是字符串,可直接进行 cast
```
假设类中存在同名的非静态 `TAG` 变量,这个时候怎么办呢?
加入一个筛选条件即可。
> 示例如下
```kotlin
Test::class.java.field {
name = "TAG"
type = StringType
modifiers {
// 标识查询的这个变量需要是静态
asStatic()
}
}.get().string() // Field 的类型是字符串,可直接进行 cast
```
更多用法可参考 [ModifierRules](api/document?id=modifierrules-class)。
我们还可以调用名为 `init` 的静态方法。
> 示例如下
```kotlin
Test::class.java.method {
name = "init"
}.get().call()
```
同样地,你可以标识它是一个静态。
> 示例如下
```kotlin
Test::class.java.method {
name = "init"
modifiers {
// 标识查询的这个方法需要是静态
asStatic()
}
}.get().call()
```
### 混淆的字节码
你可能已经注意到了,这里给出的示例 `Class` 中有两个混淆的变量名称,它们都是 `a`,这个时候我们要怎么得到它们呢?
有两种方案。
第一种方案,确定变量的名称和类型。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.field {
name = "a"
type = BooleanType
}.get(instance).self // 得到名称为 a 类型为 Boolean 的变量
```
第二种方案,确定变量的类型所在的位置。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.field {
type(BooleanType).index().first()
}.get(instance).self // 得到第一个类型为 Boolean 的变量
```
以上两种情况均可得到对应的变量 `private boolean a`
同样地,这个 `Class` 中也有两个混淆的方法名称,它们都是 `b`
你也可以有两种方案来得到它们。
第一种方案,确定方法的名称和方法参数。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "b"
param(StringType)
}.get(instance).call("test_string") // 得到名称为 b 方法参数为 [String] 的方法
```
第二种方案,确定方法的参数所在的位置。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
param(StringType).index().first()
}.get(instance).call("test_string") // 得到第一个方法参数为 [String] 的方法
```
由于观察到这个方法在 `Class` 的最后一个,那我们还有一个备选方案。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
order().index().last()
}.get(instance).call("test_string") // 得到当前 Class 的最后一个方法
```
!> 请尽量不要使用 `order` 来筛选字节码的下标,它们可能是不确定的,除非你确定它在这个 `Class` 中的位置一定不会变。
### 直接调用
上面介绍的调用字节码的方法都需要使用 `get(instance)` 才能调用对应的方法,有没有简单一点的办法呢?
此时,你可以在任意实例上使用 `current` 方法来创建一个调用空间。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 假设这个 Class 是不能被直接得到的
instance.current {
// 执行 doTask 方法
method {
name = "doTask"
param(StringType)
}.call("task_name")
// 执行 stop 方法
method {
name = "stop"
}.call()
// 得到 name
val name = method { name = "getName" }.string()
}
```
问题又来了,我想使用反射的方式创建如下的实例并调用其中的方法,该怎么做呢?
> 示例如下
```kotlin
Test(true).doTask("task_name")
```
通常情况下,我们可以使用标准的反射 API 来调用。
> 示例如下
```kotlin
classOf("com.demo.Test")
.getDeclaredConstructor(Boolean::class.java)
.apply { isAccessible = true }
.newInstance(true)
.apply {
javaClass
.getDeclaredMethod("doTask", String::class.java)
.apply { isAccessible = true }
.invoke(this, "task_name")
}
```
但是感觉这种做法好麻烦,有没有更简洁的调用方法呢?
这个时候,我们还可以借助 `buildOf``buildOfAny` 方法来创建一个实例。
> 示例如下
```kotlin
classOf("com.demo.Test").buildOfAny(true) { param(BooleanType) }?.current {
method {
name = "doTask"
param(StringType)
}.call("task_name")
}
```
更多用法可参考 [CurrentClass](api/document?id=currentclass-class) 以及 [buildOf](api/document?id=buildof-method) 方法。
### 再次查询
假设有三个不同版本的 `Class`,它们都是这个宿主不同版本相同的 `Class`
这里面同样都有一个方法 `doTask`,假设它们的功能是一样的。
> 版本 A 示例如下
```java
public class Test {
public void doTask() {
// ...
}
}
```
> 版本 B 示例如下
```java
public class Test {
public void doTask(String taskName) {
// ...
}
}
```
> 版本 C 示例如下
```java
public class Test {
public void doTask(String taskName, int type) {
// ...
}
}
```
我们需要在不同的版本中得到这个相同功能的 `doTask` 方法,要怎么做呢?
此时,你可以使用 `RemedyPlan` 完成你的需求。
> 示例如下
```kotlin
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "doTask"
}.remedys {
method {
name = "doTask"
param(StringType)
}.onFind {
// 可在这里实现找到的逻辑
}
method {
name = "doTask"
param(StringType, IntType)
}.onFind {
// 可在这里实现找到的逻辑
}
}.wait(instance) {
// 得到方法的结果
}
```
!> 特别注意使用了 `RemedyPlan` 的方法查询结果不能再使用 `get` 的方式得到方法实例,应当使用 `wait` 方法。
更多用法可参考 [Method RemedyPlan](api/document?id=remedyplan-class) 以及 [Constructor RemedyPlan](api/document?id=remedyplan-class-1)。
### 相对匹配
假设宿主中不同版本中存在功能相同的 `Class` 但仅有 `Class` 的名称不一样。
> 版本 A 示例如下
```java
public class ATest {
public static void doTask() {
// ...
}
}
```
> 版本 B 示例如下
```java
public class BTest {
public static void doTask() {
// ...
}
}
```
这个时候我们想在每个版本都调用这个 `Class` 里的 `doTask` 方法该怎么做呢?
通常做法是判断 `Class` 是否存在。
> 示例如下
```kotlin
// 首先查询到这个 Class
val currentClass = if("com.demo.ATest".hasClass) classOf("com.demo.ATest") else classOf("com.demo.BTest")
// 然后再查询这个方法并调用
currentClass.method {
name = "doTask"
}.get().call()
```
感觉这种方案非常的不优雅且繁琐,那么此时 `YukiHookAPI` 就为你提供了一个非常方便的 `VariousClass` 专门来解决这个问题。
现在,你可以直接使用以下方式获取到这个 `Class`
> 示例如下
```kotlin
VariousClass("com.demo.ATest", "com.demo.BTest").get().method {
name = "doTask"
}.get().call()
```
更多用法可参考 [VariousClass](api/document?id=variousclass-class)。
若在创建 Hook 的时候使用,可以更加方便,还可以自动拦截找不到 `Class` 的异常。
> 示例如下
```kotlin
findClass("com.demo.ATest", "com.demo.BTest").hook {
// Your code here.
}
```
你还可以把这个 `Class` 定义为一个常量类型来使用。
> 示例如下
```kotlin
// 定义常量类型
val ABTestClass = VariousClass("com.demo.ATest", "com.demo.BTest")
// 直接使用
ABTestClass.hook {
// Your code here.
}
```
更多用法可参考 [findClass](api/document?id=findclass-method) 方法。
### 注意误区
> 这里列举了使用时可能会遇到的误区部分,可供参考。
#### 限制性查询条件
!> 在查询条件中,除了 `order` 你只能使用一次 `index` 功能。
> 示例如下
```kotlin
method {
name = "test"
param(BooleanType).index(num = 2)
// ❗错误的使用方法,请仅保留一个 index 方法
returnType(StringType).index(num = 1)
}
```
以下查询条件的使用是没有任何问题的。
> 示例如下
```kotlin
method {
name = "test"
param(BooleanType).index(num = 2)
order().index(num = 1)
}
```
#### 字节码类型
!> 在字节码调用结果中,`cast` 方法只能指定字节码对应的类型。
例如我们想得到一个 `Boolean` 类型的变量,把他转换为 `String`
以下是错误的使用方法。
> 示例如下
```kotlin
field {
name = "test"
type = BooleanType
}.get().string() // ❗错误的使用方法,必须 cast 为字节码目标类型
```
以下是正确的使用方法。
> 示例如下
```kotlin
field {
name = "test"
type = BooleanType
}.get().boolean().toString() // ✅ 正确的使用方法,得到类型后再进行转换
```
## 常用类型扩展功能
在查询方法、变量的时候我们通常需要指定所查询的类型。
> 示例如下
```kotlin
field {
name = "test"
type = Boolean::class.java
}
```
`Kotlin` 中表达出 `Boolean::class.java` 这个类型的写法很长,感觉并不方便。
因此,`YukiHookAPI` 为开发者封装了常见的类型调用,其中包含了 Android 的基本类型和 Java 的基本类型。
这个时候上面的类型就可以写作如下形式了。
> 示例如下
```kotlin
field {
name = "test"
type = BooleanType
}
```
在 Java 中常见的基本类型都已被封装为 <b>类型 + Type</b> 的方式,例如 `IntType``FloatType`
相应地,数组类型也有方便的使用方法,假设我们要获得 `String[]` 类型的数组。
需要写做 `java.lang.reflect.Array.newInstance(String::class.java, 0).javaClass` 才能得到这个类型。
感觉是不是很麻烦,这个时候我们可以使用扩展方法 `ArrayClass(StringType)` 来得到这个类型。
同时由于 `String` 是常见类型,所以还可以直接使用 `StringArrayClass` 来得到这个类型。
一些常见的 Hook 中查询的方法,都有其对应的封装类型以供使用,格式为 <b>类型 + Class</b>
例如 Hook `onCreate` 方法需要查询 `Bundle::class.java` 类型。
> 示例如下
```kotlin
method {
name = "onCreate"
param(BundleClass)
}
```
更多类型请 [点击这里](api/document?id=graphicstypefactory-kt) 前往查看,也欢迎你能贡献更多的常用类型。
## 调试日志功能
> 日志是调试过程最重要的一环,`YukiHookAPI` 为开发者封装了一套稳定高效的调试日志功能。
### 普通日志
你可以调用 `loggerD``loggerI``loggerW` 来向控制台打印普通日志。
使用方法如下所示。
> 示例如下
```kotlin
loggerD(msg = "This is a log")
```
此时,`YukiHookAPI` 会调用 `android.util.Log``XposedBridge.log` 同时打印这条日志。
日志默认的 `TAG` 为你在 `YukiHookAPI.Configs.debugTag` 中设置的值。
你也可以动态自定义这个值,但是不建议轻易修改 `TAG` 防止过滤不到日志。
> 示例如下
```kotlin
loggerD(tag = "YukiHookAPI", msg = "This is a log")
```
打印的结果为如下所示。
> 示例如下
```
[YukiHookAPI][D]--> This is a log
```
更多用法可参考 [loggerD](api/document?id=loggerd-method)、[loggerI](api/document?id=loggeri-method) 及 [loggerW](api/document?id=loggerw-method) 方法。
### 错误日志
你可以调用 `loggerE` 来向控制台打印 `E` 级别的日志。
使用方法如下所示。
> 示例如下
```kotlin
loggerE(msg = "This is an error")
```
错误级别的日志是最高的,无论你有没有过滤仅为 `E` 级别的日志。
对于错误级别的日志,你还可以在后面加上一个异常堆栈。
```kotlin
// 假设这就是被抛出的异常
val e = Throwable(...)
// 打印日志
loggerE(msg = "This is an error", throwable = e)
```
打印的结果为如下所示。
> 示例如下
```
[YukiHookAPI][E]--> This is an error
```
同时,日志会帮你打印整个异常堆栈。
> 示例如下
```
java.lang.Throwable
at com.demo.Test.<init>(...)
at com.demo.Test.doTask(...)
at com.demo.Test.stop(...)
at com.demo.Test.init(...)
at a.a.a(...)
... 3 more
```
更多用法可参考 [loggerE](api/document?id=loggere-method) 方法。
## Xposed 模块数据存储功能
> 这是一个自动对接 `SharedPreferences` 和 `XSharedPreferences` 的高效模块数据存储解决方案。
我们需要存储模块的数据,以供宿主调用,这个时候会遇到原生 `Sp` 存储的数据互通阻碍。
原生的 `Xposed` 给我们提供了一个 `XSharedPreferences` 用于读取模块的 `Sp` 数据。
通常情况下我们可以这样在 Hook 内对其进行初始化。
> 示例如下
```kotlin
XSharedPreferences(BuildConfig.APPLICATION_ID)
```
有没有方便快捷的解决方案呢,此时你就可以使用 `YukiHookAPI` 的扩展能力快速实现这个功能。
当你在模块中存储数据的时候,若当前处于 `Activity` 内,可以使用如下方法。
> 示例如下
```kotlin
modulePrefs.putString("test_name", "saved_value")
```
当你在 Hook 中读取数据时,可以使用如下方法。
> 示例如下
```kotlin
val testName = prefs.getString("test_name", "default_value")
```
你不需要考虑传入模块的包名以及一系列复杂的权限配置,一切都交给 `YukiHookModulePrefs` 来处理。
若要实现存储的区域划分,你可以指定每个 `prefs` 文件的名称。
在模块的 `Activity` 中这样使用。
> 示例如下
```kotlin
// 推荐用法
modulePrefs("specify_file_name").putString("test_name", "saved_value")
// 也可以这样用
modulePrefs.name("specify_file_name").putString("test_name", "saved_value")
```
在 Hook 中这样读取。
> 示例如下
```kotlin
// 推荐用法
val testName = prefs("specify_file_name").getString("test_name", "default_value")
// 也可以这样用
val testName = prefs.name("specify_file_name").getString("test_name", "default_value")
```
若你的项目中有大量的固定数据需要存储和读取,推荐使用 `PrefsData` 来创建模板,详细用法可参考 [PrefsData](api/document?id=prefsdata-class)。
更多用法可参考 [YukiHookModulePrefs](api/document?id=yukihookmoduleprefs-class)。