# 宿主资源注入扩展 | Yuki Hook API
一个使用 Kotlin 重构的轻量、高效、稳定的 Xposed Hook API

# # 宿主资源注入扩展

> 这是一个将模块资源、`Activity` 组件以及 `Context` 主题注入到宿主的扩展功能。

在使用以下功能之前，为防止资源 ID 互相冲突，你需要在当前 Xposed 模块项目的 `build.gradle` 中修改资源 ID。

- Kotlin Gradle DSL

    android {
        androidResources.additionalParameters("--allow-reserved-package-id", "--package-id", "0x64")
    }

- Groovy

    android {
        aaptOptions.additionalParameters '--allow-reserved-package-id', '--package-id', '0x64'
    }

注意

提供的示例资源 ID 值仅供参考，不可使用 0x7f，默认为 0x64，为了防止当前宿主存在多个 Xposed 模块，建议自定义你自己的资源 ID。

## # 注入模块资源 (Resources)

在 Hook 宿主之后，我们可以直接在 Hooker 中得到的 `Context` 注入当前模块资源。

> 示例如下

    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` 中注入当前模块资源。

> 示例如下

    onAppLifecycle {
        onCreate {
            // 全局注入模块资源，但仅限于全局生命周期
            // 类似 ImageView.setImageResource 这样的方法在 Activity 中需要单独注入
            // <方案1> 通过 Context 注入模块资源
            injectModuleAppResources()
            // <方案2> 直接得到宿主 Resources 注入模块资源
            resources.injectModuleAppResources()
            // 直接使用模块资源 ID
            getString(R.id.app_name)
        }
    }

小提示

更多功能请参考 Context+Resources.injectModuleAppResources 方法。

## # 注册模块 Activity

在 Android 系统中所有应用的 `Activity` 启动时，都需要在 `AndroidManifest.xml` 中进行注册，在 Hook 过程中，如果我们想通过宿主来直接启动模块中未注册的 `Activity` 要怎么做呢？

在 Hook 宿主之后，我们可以直接在 Hooker 中得到的 `Context` 注册当前模块的 `Activity` 代理。

> 示例如下

    injectMember {
        method {
            name = "onCreate"
            param(BundleClass)
        }
        afterHook {
            instance<Activity>().registerModuleAppActivities()
        }
    }

你还可以直接在 `AppLifecycle` 中注册当前模块的 `Activity` 代理。

> 示例如下

    onAppLifecycle {
        onCreate {
            registerModuleAppActivities()
        }
    }

如果没有填写 `proxy` 参数，API 将会根据当前 `Context` 自动获取当前宿主的启动入口 `Activity` 进行代理。

通常情况下，它是有效的，但是以上情况在一些 APP 中会失效，例如一些 `Activity` 会在注册清单上加入启动参数，那么我们就需要使用另一种解决方案。

若未注册的 `Activity` 不能被正确启动，我们可以手动拿到宿主的 `AndroidManifest.xml` 进行分析，来得到一个注册过的 `Activity` 标签，获取其中的 `name`。

你需要选择一个当前宿主可能用不到的、不需要的 `Activity` 作为一个“傀儡”将其进行代理，通常是有效的。

比如我们已经找到了能够被代理的合适 `Activity`。

> 示例如下

    <activity
        android:name="com.demo.test.activity.TestActivity"
        ...>

根据其中的 `name`，我们只需要在方法中加入这个参数进行注册即可。

> 示例如下

    registerModuleAppActivities(proxy = "com.demo.test.activity.TestActivity")

另一种情况，如果你对宿主的类编写了一个 `stub`，那么你可以直接通过 `Class` 对象来进行注册。

> 示例如下

    registerModuleAppActivities(TestActivity::class.java)

注册完成后，请将你需要使用宿主启动的模块中的 `Activity` 继承于 `ModuleAppActivity` 或 `ModuleAppCompatActivity`。

这些 `Activity` 现在无需注册即可无缝存活于宿主中。

> 示例如下

    class HostTestActivity : ModuleAppActivity() {

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            // 模块资源已被自动注入，可以直接使用 xml 装载布局
            setContentView(R.layout.activity_main)
        }
    }

若你需要继承于 `ModuleAppCompatActivity`，你需要手动设置 AppCompat 主题。

> 示例如下

    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` 了。

> 示例如下

    val context: Context = ... // 假设这就是你的 Context
    context.startActivity(context, HostTestActivity::class.java)

小提示

更多功能请参考 Context.registerModuleAppActivities 方法。

## # 创建 ContextThemeWrapper 代理

有时候，我们需要使用 `MaterialAlertDialogBuilder` 来美化自己在宿主中的对话框，但是拿不到 AppCompat 主题就无法创建。

- 会得到如下异常

    The style on this component requires your app theme to be Theme.AppCompat (or a descendant).

这时，我们想在宿主被 Hook 的当前 `Activity` 中使用 `MaterialAlertDialogBuilder` 来创建对话框，就可以有如下方法。

> 示例如下

    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 及以上系统版本支持且当前主题包含夜间模式相关元素。

> 示例如下

    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` 创建对话框了。

可能存在的问题

由于一些 APP 自身使用的 androidx 依赖库或自定义主题可能会对当前 MaterialAlertDialog 实际样式造成干扰，例如对话框的按钮样式，这种情况你可以参考 模块 Demo 中 这里的示例代码
在新窗口中打开 来修复这个问题。

某些 APP 在创建时可能会发生 ClassCastException 异常，请手动指定新的 Configuration 实例来进行修复。

小提示

更多功能请参考 Context.applyModuleTheme 方法。

## # ClassLoader 冲突问题

本页面所介绍的内容都是直接将模块的资源注入到了宿主，由于模块与宿主不在同一个进程 (同一个 APK) 中，其可能存在 `ClassLoader` 冲突的问题。

若发生了 `ClassLoader` 冲突，你可能会遇到 `ClassCastException` 异常。

`YukiHookAPI` 默认已解决了可能冲突的问题，其余情况需要你自行配置排除列表。

排除列表决定了这些 `Class` 需要被模块还是宿主的 `ClassLoader` 进行装载。

> 示例如下

    // 排除属于宿主的 Class 类名
    // 它们将会被宿主的 ClassLoader 装载
    // ❗以下内容仅供演示，不要直接使用，请以你的实际情况为准
    ModuleClassLoader.excludeHostClasses(
        "androidx.core.app.ActivityCompat",
        "com.demo.Test"
    )
    // 排除属于模块的 Class 类名
    // 它们将会被模块 (当前 Hook 进程) 的 ClassLoader 装载
    // ❗以下内容仅供演示，不要直接使用，请以你的实际情况为准
    ModuleClassLoader.excludeModuleClasses(
        "com.demo.entry.HookEntry",
        "com.demo.controller.ModuleController"
    )

你需要在向宿主注入模块资源的方法执行之前进行设置才能生效。

此功能仅为解决宿主与模块中可能存在同名的 `Class` 情况，例如共用的 SDK 以及依赖，在大部分情况下你不会用到此功能。

小提示

更多功能请参考 ModuleClassLoader。