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。
在 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 方法。
在 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 方法。
有时候,我们需要使用 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
创建对话框了。
小提示
更多功能请参考 Context.applyModuleTheme 方法。
本页面所介绍的内容都是直接将模块的资源注入到了宿主,由于模块与宿主不在同一个进程 (同一个 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。