import{_ as e,r as o,o as p,c,b as s,d as n,e as t,a}from"./app-BpUB8-Q8.js";const i={},r=a(`

宿主资源注入扩展

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

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

Kotlin DSL

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

Groovy DSL

android {
    androidResources.additionalParameters += ['--allow-reserved-package-id', '--package-id', '0x64']
}

注意

过往版本中的 aaptOptions.additionalParameters 已被作废,请参考上述写法并保持你的 Android Gradle Plugin 为最新版本。

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

注入模块资源 (Resources)

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

示例如下

resolve().firstMethod {
    name = "onCreate"
    parameters(Bundle::class)
}.hook {
    after {
        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 代理。

示例如下

resolve().firstMethod {
    name = "onCreate"
    parameters(Bundle::class)
}.hook {
    after {
        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 实现 ModuleActivity 接口。

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

我们推荐你创建 BaseActivity 作为所有模块 Activity 的基类。

示例如下

abstract class BaseActivity : AppCompatActivity(), ModuleActivity {

    // 设置 AppCompat 主题 (如果当前是 [AppCompatActivity])
    override val moduleTheme get() = R.style.YourAppTheme

    override fun getClassLoader() = delegate.getClassLoader()

    override fun onCreate(savedInstanceState: Bundle?) {
        delegate.onCreate(savedInstanceState)
        super.onCreate(savedInstanceState)
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        delegate.onConfigurationChanged(newConfig)
        super.onConfigurationChanged(newConfig)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        delegate.onRestoreInstanceState(savedInstanceState)
        super.onRestoreInstanceState(savedInstanceState)
    }
}

然后将需要实现的 Activity 继承于 BaseActivity

示例如下

class HostTestActivity : BaseActivity() {

    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)

上面我们在 registerModuleAppActivities 方法中设置的 proxy 参数为默认的全局代理 Activity

如果你需要指定某个代理的 Activity 使用另外的宿主 Activity 进行代理,你可以参考如下方法。

示例如下

class HostTestActivity : BaseActivity() {

    // 指定一个另外的代理 Activity 类名,其也必须存在于宿主的 AndroidManifest 中
    override val proxyClassName get() = "com.demo.test.activity.OtherActivity"

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

小提示

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

创建 ContextThemeWrapper 代理

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

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

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

示例如下

resolve().firstMethod {
    name = "onCreate"
    parameters(Bundle::class)
}.hook {
    after {
        // 使用 applyModuleTheme 创建一个当前模块中的主题资源
        val appCompatContext = instance<Activity>().applyModuleTheme(R.style.Theme_AppCompat)
        // 直接使用这个包装了模块主题后的 Context 创建对话框
        MaterialAlertDialogBuilder(appCompatContext)
            .setTitle("AppCompat 主题对话框")
            .setMessage("我是一个在宿主中显示的 AppCompat 主题对话框。")
            .setPositiveButton("确定", null)
            .show()
    }
}

你还可以对当前 Context 通过 uiMode 设置原生的夜间模式和日间模式,至少需要 Android 10 及以上系统版本支持且当前主题包含夜间模式相关元素。

示例如下

resolve().firstMethod {
    name = "onCreate"
    parameters(Bundle::class)
}.hook {
    after {
        // 定义当前模块中的主题资源
        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 创建对话框了。

`,64),d={class:"custom-container warning"},A=s("p",{class:"custom-container-title"},"可能存在的问题",-1),y=s("strong",null,"androidx",-1),D=s("strong",null,"MaterialAlertDialog",-1),B=s("strong",null,"模块 Demo",-1),C={href:"https://github.com/HighCapable/YukiHookAPI/tree/master/samples/demo-module/src/main/java/com/highcapable/yukihookapi/demo_module/hook/factory/ComponentCompatFactory.kt",target:"_blank",rel:"noopener noreferrer"},v=s("p",null,[n("某些 APP 在创建时可能会发生 "),s("strong",null,"ClassCastException"),n(" 异常,请手动指定新的 "),s("strong",null,"Configuration"),n(" 实例来进行修复。")],-1),u=a(`

小提示

更多功能请参考 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

`,11);function m(b,F){const l=o("ExternalLinkIcon");return p(),c("div",null,[r,s("div",d,[A,s("p",null,[n("由于一些 APP 自身使用的 "),y,n(" 依赖库或自定义主题可能会对当前 "),D,n(" 实际样式造成干扰,例如对话框的按钮样式,这种情况你可以参考 "),B,n(" 中 "),s("a",C,[n("这里的示例代码"),t(l)]),n(" 来修复这个问题。")]),v]),u])}const k=e(i,[["render",m],["__file","host-inject.html.vue"]]);export{k as default};