Added new documentations files

This commit is contained in:
2022-09-17 14:46:07 +08:00
parent dbd5a74a5c
commit 237d5cbed0
99 changed files with 20686 additions and 0 deletions

View File

@@ -0,0 +1,275 @@
# 宿主资源注入扩展
> 这是一个将模块资源、`Activity` 组件以及 `Context` 主题注入到宿主的扩展功能。
在使用以下功能之前,为防止资源 ID 互相冲突,你需要在当前 Xposed 模块项目的 `build.gradle` 中修改资源 ID。
- Kotlin Gradle DSL
```kotlin
android {
androidResources.additionalParameters("--allow-reserved-package-id", "--package-id", "0x64")
}
```
- Groovy
```groovy
android {
aaptOptions.additionalParameters '--allow-reserved-package-id', '--package-id', '0x64'
}
```
::: warning
提供的示例资源 ID 值仅供参考,不可使用 **0x7f**,默认为 **0x64**,为了防止当前宿主存在多个 Xposed 模块,建议自定义你自己的资源 ID。
:::
## 注入模块资源 (Resources)
在 Hook 宿主之后,我们可以直接在 Hooker 中得到的 `Context` 注入当前模块资源。
> 示例如下
```kotlin
injectMember {
method {
name = "onCreate"
param(BundleClass)
}
afterHook {
instance<Activity>().also {
// <方案1> 通过 Context 注入模块资源
it.injectModuleAppResources()
// <方案2> 直接得到宿主 Resources 注入模块资源
it.resources.injectModuleAppResources()
// 直接使用模块资源 ID
it.getString(R.id.app_name)
}
}
}
```
你还可以直接在 `AppLifecycle` 中注入当前模块资源。
> 示例如下
```kotlin
onAppLifecycle {
onCreate {
// 全局注入模块资源,但仅限于全局生命周期
// 类似 ImageView.setImageResource 这样的方法在 Activity 中需要单独注入
// <方案1> 通过 Context 注入模块资源
injectModuleAppResources()
// <方案2> 直接得到宿主 Resources 注入模块资源
resources.injectModuleAppResources()
// 直接使用模块资源 ID
getString(R.id.app_name)
}
}
```
::: tip
更多功能请参考 [Context+Resources.injectModuleAppResources](../public/com/highcapable/yukihookapi/hook/factory/YukiHookFactory#context-resources-injectmoduleappresources-ext-method) 方法。
:::
## 注册模块 Activity
在 Android 系统中所有应用的 `Activity` 启动时,都需要在 `AndroidManifest.xml` 中进行注册,在 Hook 过程中,如果我们想通过宿主来直接启动模块中未注册的 `Activity` 要怎么做呢?
在 Hook 宿主之后,我们可以直接在 Hooker 中得到的 `Context` 注册当前模块的 `Activity` 代理。
> 示例如下
```kotlin
injectMember {
method {
name = "onCreate"
param(BundleClass)
}
afterHook {
instance<Activity>().registerModuleAppActivities()
}
}
```
你还可以直接在 `AppLifecycle` 中注册当前模块的 `Activity` 代理。
> 示例如下
```kotlin
onAppLifecycle {
onCreate {
registerModuleAppActivities()
}
}
```
如果没有填写 `proxy` 参数API 将会根据当前 `Context` 自动获取当前宿主的启动入口 `Activity` 进行代理。
通常情况下,它是有效的,但是以上情况在一些 APP 中会失效,例如一些 `Activity` 会在注册清单上加入启动参数,那么我们就需要使用另一种解决方案。
若未注册的 `Activity` 不能被正确启动,我们可以手动拿到宿主的 `AndroidManifest.xml` 进行分析,来得到一个注册过的 `Activity` 标签,获取其中的 `name`
你需要选择一个当前宿主可能用不到的、不需要的 `Activity` 作为一个“傀儡”将其进行代理,通常是有效的。
比如我们已经找到了能够被代理的合适 `Activity`
> 示例如下
```xml
<activity
android:name="com.demo.test.activity.TestActivity"
...>
```
根据其中的 `name`,我们只需要在方法中加入这个参数进行注册即可。
> 示例如下
```kotlin
registerModuleAppActivities(proxy = "com.demo.test.activity.TestActivity")
```
另一种情况,如果你对宿主的类编写了一个 `stub`,那么你可以直接通过 `Class` 对象来进行注册。
> 示例如下
```kotlin
registerModuleAppActivities(TestActivity::class.java)
```
注册完成后,请将你需要使用宿主启动的模块中的 `Activity` 继承于 `ModuleAppActivity``ModuleAppCompatActivity`
这些 `Activity` 现在无需注册即可无缝存活于宿主中。
> 示例如下
```kotlin
class HostTestActivity : ModuleAppActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 模块资源已被自动注入,可以直接使用 xml 装载布局
setContentView(R.layout.activity_main)
}
}
```
若你需要继承于 `ModuleAppCompatActivity`,你需要手动设置 AppCompat 主题。
> 示例如下
```kotlin
class HostTestActivity : ModuleAppCompatActivity() {
// 这里的主题名称仅供参考,请填写你模块中已有的主题名称
override val moduleTheme get() = R.style.Theme_AppCompat
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 模块资源已被自动注入,可以直接使用 xml 装载布局
setContentView(R.layout.activity_main)
}
}
```
以上步骤全部完成后,你就可以在 (Xposed) 宿主环境任意存在 `Context` 的地方愉快地调用 `startActivity` 了。
> 示例如下
```kotlin
val context: Context = ... // 假设这就是你的 Context
context.startActivity(context, HostTestActivity::class.java)
```
::: tip
更多功能请参考 [Context.registerModuleAppActivities](../public/com/highcapable/yukihookapi/hook/factory/YukiHookFactory#context-registermoduleappactivities-ext-method) 方法。
:::
## 创建 ContextThemeWrapper 代理
有时候,我们需要使用 `MaterialAlertDialogBuilder` 来美化自己在宿主中的对话框,但是拿不到 AppCompat 主题就无法创建。
- 会得到如下异常
```:no-line-numbers
The style on this component requires your app theme to be Theme.AppCompat (or a descendant).
```
这时,我们想在宿主被 Hook 的当前 `Activity` 中使用 `MaterialAlertDialogBuilder` 来创建对话框,就可以有如下方法。
> 示例如下
```kotlin
injectMember {
method {
name = "onCreate"
param(BundleClass)
}
afterHook {
// 使用 applyModuleTheme 创建一个当前模块中的主题资源
val appCompatContext = instance<Activity>().applyModuleTheme(R.style.Theme_AppCompat)
// 直接使用这个包装了模块主题后的 Context 创建对话框
MaterialAlertDialogBuilder(appCompatContext)
.setTitle("AppCompat 主题对话框")
.setMessage("我是一个在宿主中显示的 AppCompat 主题对话框。")
.setPositiveButton("确定", null)
.show()
}
}
```
你还可以对当前 `Context` 通过 `uiMode` 设置原生的夜间模式和日间模式,至少需要 Android 10 及以上系统版本支持且当前主题包含夜间模式相关元素。
> 示例如下
```kotlin
injectMember {
method {
name = "onCreate"
param(BundleClass)
}
afterHook {
// 定义当前模块中的主题资源
var appCompatContext: ModuleContextThemeWrapper
// <方案1> 直接得到 Configuration 对象进行设置
appCompatContext = instance<Activity>()
.applyModuleTheme(R.style.Theme_AppCompat)
.applyConfiguration { uiMode = Configuration.UI_MODE_NIGHT_YES }
// <方案2> 创建一个新的 Configuration 对象
// 此方案会破坏当前宿主中原有的字体缩放大小等设置,你需要手动重新传递 densityDpi 等参数
appCompatContext = instance<Activity>().applyModuleTheme(
theme = R.style.Theme_AppCompat,
configuration = Configuration().apply { uiMode = Configuration.UI_MODE_NIGHT_YES }
)
// 直接使用这个包装了模块主题后的 Context 创建对话框
MaterialAlertDialogBuilder(appCompatContext)
.setTitle("AppCompat 主题对话框")
.setMessage("我是一个在宿主中显示的 AppCompat 主题对话框。")
.setPositiveButton("确定", null)
.show()
}
}
```
这样,我们就可以在宿主中非常简单地使用 `MaterialAlertDialogBuilder` 创建对话框了。
::: warning 可能存在的问题
由于一些 APP 自身使用的 **androidx** 依赖库或自定义主题可能会对当前 **MaterialAlertDialog** 实际样式造成干扰,例如对话框的按钮样式,这种情况你可以参考 **模块 Demo** 中 [这里的示例代码](https://github.com/fankes/YukiHookAPI/tree/master/demo-module/src/main/java/com/highcapable/yukihookapi/demo_module/hook/factory/ComponentCompatFactory.kt) 来修复这个问题。
某些 APP 在创建时可能会发生 **ClassCastException** 异常,请手动指定新的 **Configuration** 实例来进行修复。
:::
::: tip
更多功能请参考 [Context.applyModuleTheme](../public/com/highcapable/yukihookapi/hook/factory/YukiHookFactory#context-applymoduletheme-ext-method) 方法。
:::

View File

@@ -0,0 +1,82 @@
# 宿主生命周期扩展
> 这是一个自动 Hook 宿主 APP 生命周期的扩展功能。
## 监听生命周期
> 通过自动化 Hook 宿主 APP 的生命周期方法,来实现监听功能。
我们需要监听宿主 `Application` 的启动和生命周期方法,只需要使用以下方式实现。
> 示例如下
```kotlin
loadApp(name = "com.example.demo") {
// 注册生命周期监听
onAppLifecycle {
// 你可以在这里实现 Application 中的生命周期方法监听
attachBaseContext { baseContext, hasCalledSuper ->
// 通过判断 hasCalledSuper 来确定是否已执行 super.attachBaseContext(base) 方法
// ...
}
onCreate {
// 通过 this 得到当前 Application 实例
// ...
}
onTerminate {
// 通过 this 得到当前 Application 实例
// ...
}
onLowMemory {
// 通过 this 得到当前 Application 实例
// ...
}
onTrimMemory { self, level ->
// 可在这里判断 APP 是否已切换到后台
if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// ...
}
// ...
}
onConfigurationChanged { self, config ->
// ...
}
}
}
```
::: tip
更多功能请参考 [AppLifecycle](../public/com/highcapable/yukihookapi/hook/param/PackageParam#applifecycle-class)。
:::
## 注册系统广播
> 通过 `Application.onCreate` 方法注册系统广播,来实现对系统广播的监听。
我们还可以在宿主 `Application` 中注册系统广播。
> 示例如下
```kotlin
loadApp(name = "com.example.demo") {
// 注册生命周期监听
onAppLifecycle {
// 注册用户解锁时的广播监听
registerReceiver(Intent.ACTION_USER_PRESENT) { context, intent ->
// ...
}
// 注册多个广播监听 - 会同时回调多次
registerReceiver(Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_TIME_TICK) { context, intent ->
// ...
}
}
}
```
::: tip
更多功能请参考 [AppLifecycle](../public/com/highcapable/yukihookapi/hook/param/PackageParam#applifecycle-class)。
:::

View File

@@ -0,0 +1,124 @@
# 调试日志
> 日志是调试过程最重要的一环,`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")
```
打印的结果为如下所示。
> 示例如下
```:no-line-numbers
[YukiHookAPI][D][宿主包名]--> This is a log
```
你还可以使用 `LoggerType` 自定义日志打印的类型,可选择使用 `android.util.Log` 还是 `XposedBridge.log` 来打印日志。
默认类型为 `LoggerType.BOTH`,含义为同时使用这两个方法来打印日志。
比如我们仅使用 `android.util.Log` 来打印日志。
> 示例如下
```kotlin
loggerD(tag = "YukiHookAPI", msg = "This is a log", type = LoggerType.LOGD)
```
或又仅使用 `XposedBridge.log` 来打印日志,此方法仅可在 (Xposed) 宿主环境使用。
> 示例如下
```kotlin
loggerD(tag = "YukiHookAPI", msg = "This is a log", type = LoggerType.XPOSEDBRIDGE)
```
若你想智能区分 (Xposed) 宿主环境与模块环境,可以写为如下形式。
> 示例如下
```kotlin
loggerD(tag = "YukiHookAPI", msg = "This is a log", type = LoggerType.SCOPE)
```
这样 API 就会在不同环境智能选择指定的方法类型去打印这条日志。
::: tip
更多功能请参考 [loggerD](../public/com/highcapable/yukihookapi/hook/log/LoggerFactory#loggerd-method)、[loggerI](../public/com/highcapable/yukihookapi/hook/log/LoggerFactory#loggeri-method) 及 [loggerW](../public/com/highcapable/yukihookapi/hook/log/LoggerFactory#loggerw-method) 方法。
:::
## 错误日志
你可以调用 `loggerE` 来向控制台打印 `E` 级别的日志。
使用方法如下所示。
> 示例如下
```kotlin
loggerE(msg = "This is an error")
```
错误日志的级别是最高的,无论你有没有过滤仅为 `E` 级别的日志。
对于错误级别的日志,你还可以在后面加上一个异常堆栈。
```kotlin
// 假设这就是被抛出的异常
val throwable = Throwable(...)
// 打印日志
loggerE(msg = "This is an error", e = throwable)
```
打印的结果为如下所示。
> 示例如下
```:no-line-numbers
[YukiHookAPI][E][宿主包名]--> This is an error
```
同时,日志会帮你打印整个异常堆栈。
> 示例如下
```:no-line-numbers
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
```
在错误日志中,你同样也可以使用 `LoggerType` 来指定当前打印日志所用到的方法类型。
::: tip
更多功能请参考 [loggerE](../public/com/highcapable/yukihookapi/hook/log/LoggerFactory#loggere-method) 方法。
:::

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,181 @@
# Xposed 模块与宿主通讯桥
> 这是一个使用系统无序广播在模块与宿主之间发送和接收数据的解决方案。
::: danger 需要满足的条件
模块与宿主需要保持存活状态,否则无法建立通讯。
:::
## 基本用法
> 这里描述了 `wait` 与 `put` 方法的基本使用方法。
通过使用 `dataChannel` 来实现模块与宿主之间的通讯桥,原理为发送接收系统无序广播。
> 模块示例如下
```kotlin
// 从指定包名的宿主获取
dataChannel(packageName = "com.example.demo").wait<String>(key = "key_from_host") { value ->
// Your code here.
}
// 发送给指定包名的宿主
dataChannel(packageName = "com.example.demo").put(key = "key_from_module", value = "I am module")
```
> 宿主示例如下
```kotlin
// 从模块获取
dataChannel.wait<String>(key = "key_from_module") { value ->
// Your code here.
}
// 发送给模块
dataChannel.put(key = "key_from_host", value = "I am host")
```
你可以不设置 `dataChannel``value` 来达到仅通知模块或宿主回调 `wait` 方法。
> 模块示例如下
```kotlin
// 从指定包名的宿主获取
dataChannel(packageName = "com.example.demo").wait(key = "listener_from_host") {
// Your code here.
}
// 发送给指定包名的宿主
dataChannel(packageName = "com.example.demo").put(key = "listener_from_module")
```
> 宿主示例如下
```kotlin
// 从模块获取
dataChannel.wait(key = "listener_from_module") {
// Your code here.
}
// 发送给模块
dataChannel.put(key = "listener_from_host")
```
::: danger
接收方需要保持存活状态才能收到通讯数据。
:::
::: tip
更多功能请参考 [YukiHookDataChannel](../public/com/highcapable/yukihookapi/hook/xposed/channel/YukiHookDataChannel)。
:::
## 判断模块与宿主版本是否匹配
> 通过通讯桥功能,`YukiHookAPI` 还为你提供了在用户更新模块后,判断模块是否与宿主版本匹配的解决方案。
我们只需要调用 `checkingVersionEquals` 方法,即可实现这个功能。
在模块与宿主中可进行双向判断。
你可以在模块中判断指定包名的宿主是否与当前模块的版本匹配。
> 示例如下
```kotlin
// 从指定包名的宿主获取
dataChannel(packageName = "com.example.demo").checkingVersionEquals { isEquals ->
// Your code here.
}
```
你还可以在宿主中判断是否自身与当前模块的版本匹配。
> 示例如下
```kotlin
// 从模块获取
dataChannel.checkingVersionEquals { isEquals ->
// Your code here.
}
```
::: warning 方法回调的条件
宿主、模块保持存活状态,并在激活模块后重启了作用域中的 Hook 目标宿主对象。
:::
::: tip
更多功能请参考 [YukiHookDataChannel](../public/com/highcapable/yukihookapi/hook/xposed/channel/YukiHookDataChannel)。
:::
## 回调事件响应的规则
这里只列出了在模块中使用的例子,在宿主中相同的 `key` 始终不允许重复创建。
::: danger
在模块和宿主中,每一个 **dataChannel** 对应的 **key** 的回调事件**都不允许重复创建**,若重复,之前的回调事件会被新增加的回调事件替换,若在模块中使用,在同一个 **Activity** 中不可以重复,不同的 **Activity** 中相同的 **key** 允许重复。
:::
> 示例如下
```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 回调事件 A
dataChannel(packageName = "com.example.demo").wait(key = "test_key") {
// Your code here.
}
// 回调事件 B
dataChannel(packageName = "com.example.demo").wait(key = "test_key") {
// Your code here.
}
// 回调事件 C
dataChannel(packageName = "com.example.demo").wait(key = "other_test_key") {
// Your code here.
}
}
}
class OtherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 回调事件 D
dataChannel(packageName = "com.example.demo").wait(key = "test_key") {
// Your code here.
}
}
}
```
在上述示例中,回调事件 A 会被回调事件 B 替换掉,回调事件 C 的 `key` 不与其它重复,回调事件 D 在另一个 Activity 中,所以最终回调事件 B、C、D 都可被创建成功。
::: danger
一个相同 **key** 的回调事件只会回调当前模块正在显示的 **Activity** 中注册的回调事件,例如上述中的 **test_key**,如果 **OtherActivity** 正在显示,那么 **MainActivity** 中的 **test_key** 就不会被回调。
相同的 **key** 在同一个 **Activity** 不同的 **Fragment** 中注册 **dataChannel**,它们依然会在当前 **Activity** 中同时被回调。
在模块中,你只能使用 **Activity****Context** 注册 **dataChannel**,你不能在 **Application** 以及 **Service** 等地方使用 **dataChannel**,若要在 **Fragment** 中使用 **dataChannel**,请使用 **activity?.dataChannel(...)**
:::
## 安全性说明
在模块环境中,你只能接收<u>**指定包名的宿主**</u>发送的通讯数据且只能发送给<u>**指定包名的宿主**</u>
::: danger
为了进一步防止广播滥用,通讯数据中 API 会自动指定宿主和模块的包名,防止其它 APP 监听并利用广播做出超限行为。
:::

View File

@@ -0,0 +1,89 @@
# Xposed 模块数据存储
> 这是一个自动对接 `SharedPreferences` 和 `XSharedPreferences` 的高效模块数据存储解决方案。
我们需要存储模块的数据,以供宿主调用,这个时候会遇到原生 `Sp` 存储的数据互通阻碍。
原生的 `Xposed` 给我们提供了一个 `XSharedPreferences` 用于读取模块的 `Sp` 数据。
## 在 Activity 中使用
> 这里描述了在 `Activity` 中装载 `YukiHookModulePrefs` 的场景。
通常情况下我们可以这样在 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` 来创建模板。
::: tip
更多功能请参考 [YukiHookModulePrefs](../public/com/highcapable/yukihookapi/hook/xposed/prefs/YukiHookModulePrefs)、[PrefsData](../public/com/highcapable/yukihookapi/hook/xposed/prefs/data/PrefsData)。
:::
## 在 PreferenceFragment 中使用
> 这里描述了在 `PreferenceFragment` 中装载 `YukiHookModulePrefs` 的场景。
若你的模块使用了 `PreferenceFragmentCompat`,你现在可以将其继承类开始迁移到 `ModulePreferenceFragment`
::: danger
你必须继承 **ModulePreferenceFragment** 才能实现 **YukiHookModulePrefs** 的模块存储功能。
:::
::: tip
更多功能请参考 [ModulePreferenceFragment](../public/com/highcapable/yukihookapi/hook/xposed/prefs/ui/ModulePreferenceFragment)。
:::