48 KiB
特色功能
除了基本的 Hook 功能之外,
YukiHookAPI
还为开发者提供了大量的语法糖和扩展用法。
字节码扩展功能
假设有一个这样的 Class
。
示例如下
package com.demo;
public class BaseTest {
public BaseTest() {
// ...
}
public BaseTest(boolean isInit) {
// ...
}
private void doBaseTask(String taskName) {
// ...
}
}
package com.demo;
public class Test extends BaseTest {
public Test() {
// ...
}
public Test(boolean isInit) {
// ...
}
private static TAG = "Test";
private BaseTest baseInstance;
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) {
// ...
}
}
查询与反射调用
假设我们要得到 Test
(以下统称“当前 Class
”)的 doTask
方法并执行,通常情况下,我们可以使用标准的反射 API 去查询这个方法。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用反射 API 调用并执行
Test::class.java.getDeclaredMethod("doTask", String::class.java).apply { isAccessible = true }.invoke(instance, "task_name")
这种写法大概不是很友好,此时 YukiHookAPI
就为你提供了一个可在任意地方使用的语法糖。
以上写法换做 YukiHookAPI
可写作如下形式。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "doTask"
param(StringType)
}.get(instance).call("task_name")
更多用法可参考 MethodFinder。
同样地,我们需要得到 isTaskRunning
变量也可以写作如下形式。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.field {
name = "isTaskRunning"
type = BooleanType
}.get(instance).any() // any 为 Field 的任意类型实例化对象
更多用法可参考 FieldFinder。
也许你还想得到当前 Class
的构造方法,同样可以实现。
示例如下
Test::class.java.constructor {
param(BooleanType)
}.get().call(true) // 可创建一个新的实例
若想得到的是 Class
的无参构造方法,可写作如下形式。
示例如下
Test::class.java.constructor().get().call() // 可创建一个新的实例
更多用法可参考 ConstructorFinder。
可选的查询条件
假设我们要得到 Class
中的 getName
方法,可以使用如下实现。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "getName"
emptyParam()
returnType = StringType
}.get(instance).string() // 得到方法的结果
通过观察发现,这个 Class
中只有一个名为 getName
的方法,那我们可不可以再简单一点呢?
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "getName"
emptyParam()
}.get(instance).string() // 得到方法的结果
是的,对于确切不会变化的方法,你可以精简查询条件。
在只使用 get
或 wait
方法得到结果时 YukiHookAPI
会默认按照字节码顺序匹配第一个查询到的结果。
问题又来了,这个 Class
中有一个 release
方法,但是它的方法参数好长,而且很多的类型都无法直接得到。
通常情况下我们会使用 param(...)
来查询这个方法,但是有没有更简单的方法呢。
此时,在确定方法唯一性后,你可以使用 paramCount
来查询到这个方法。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "release"
// 此时我们不必确定方法参数具体类型,写个数就好
paramCount = 3
}.get(instance) // 得到这个方法
在父类查询
你会注意到 Test
继承于 BaseTest
,现在我们想得到 BaseTest
的 doBaseTask
方法,在不知道父类名称的情况下,要怎么做呢?
参照上面的查询条件,我们只需要在查询条件中加入一个 superClass
即可实现这个功能。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "doBaseTask"
param(StringType)
// 只需要添加这个条件
superClass()
}.get(instance).call("task_name")
这个时候我们就可以在父类中取到这个方法了。
superClass
有一个参数为 isOnlySuperClass
,设置为 true
后,可以跳过当前 Class
仅查询当前 Class
的父类。
由于我们现在已知 doBaseTask
方法只存在于父类,可以加上这个条件节省查询时间。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "doBaseTask"
param(StringType)
// 加入一个查询条件
superClass(isOnlySuperClass = true)
}.get(instance).call("task_name")
这个时候我们同样可以得到父类中的这个方法。
superClass
一旦设置就会自动循环向后查找全部继承的父类中是否有这个方法,直到查询到目标没有父类(继承关系为 java.lang.Object
)为止。
更多用法可参考 superClass 方法。
!> 当前查询的 Method
除非指定 superClass
条件,否则只能查询到当前 Class
的 Method
。
模糊查询
如果我们想查询一个方法名称,但是又不确定它在每个版本中是否发生变化,此时我们就可以使用模糊查询功能。
假设我们要得到 Class
中的 doTask
方法,可以使用如下实现。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name {
// 设置名称不区分大小写
equalsOf(other = "dotask", isIgnoreCase = true)
}
param(StringType)
}.get(instance).call("task_name")
已知当前 Class
中仅有一个 doTask
方法,我们还可以判断方法名称仅包含其中指定的字符。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name {
// 仅包含 oTas
contains(other = "oTas")
}
param(StringType)
}.get(instance).call("task_name")
我们还可以根据首尾字符串进行判断。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name {
// 开头包含 do
startsWith(prefix = "do")
// 结尾包含 Task
endsWith(suffix = "Task")
}
param(StringType)
}.get(instance).call("task_name")
通过观察发现这个方法名称中只包含字母,我们还可以再增加一个精确的查询条件。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name {
// 开头包含 do
startsWith(prefix = "do")
// 结尾包含 Task
endsWith(suffix = "Task")
// 仅包含字母
onlyLetters()
}
param(StringType)
}.get(instance).call("task_name")
更多用法可参考 NameConditions。
多重查询
有些时候,我们可能需要查询一个 Class
中具有相同特征的一组方法、构造方法、变量,这个时候,我们就可以利用相对条件匹配来完成。
在查询条件结果的基础上,我们只需要把 get
换为 all
即可得到匹配条件的全部字节码。
假设这次我们要得到 Class
中方法参数个数范围在 1..3
的全部方法,可以使用如下实现。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
paramCount(1..3)
}.all(instance).forEach { instance ->
// 调用执行每个方法
instance.call(...)
}
上述示例可完美匹配到如下 3 个方法。
private void doTask(String taskName)
private void release(Release release, Function<boolean, String> function, Task task)
private void b(String a)
通过观察 Class
中有两个名称为 b
的方法,可以使用如下实现。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "b"
}.all(instance).forEach { instance ->
// 调用执行每个方法
instance.call(...)
}
上述示例可完美匹配到如下 2 个方法。
private void b()
private void b(String a)
静态字节码
有些方法和变量在 Class
中是静态的实现,这个时候,我们不需要传入实例就可以调用它们。
假设我们这次要得到静态变量 TAG
的内容。
示例如下
Test::class.java.field {
name = "TAG"
type = StringType
}.get().string() // Field 的类型是字符串,可直接进行 cast
假设 Class
中存在同名的非静态 TAG
变量,这个时候怎么办呢?
加入一个筛选条件即可。
示例如下
Test::class.java.field {
name = "TAG"
type = StringType
modifiers {
// 标识查询的这个变量需要是静态
asStatic()
}
}.get().string() // Field 的类型是字符串,可直接进行 cast
更多用法可参考 ModifierRules。
我们还可以调用名为 init
的静态方法。
示例如下
Test::class.java.method {
name = "init"
emptyParam()
}.get().call()
同样地,你可以标识它是一个静态。
示例如下
Test::class.java.method {
name = "init"
emptyParam()
modifiers {
// 标识查询的这个方法需要是静态
asStatic()
}
}.get().call()
混淆的字节码
你可能已经注意到了,这里给出的示例 Class
中有两个混淆的变量名称,它们都是 a
,这个时候我们要怎么得到它们呢?
有两种方案。
第一种方案,确定变量的名称和类型。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.field {
name = "a"
type = BooleanType
}.get(instance).any() // 得到名称为 a 类型为 Boolean 的变量
第二种方案,确定变量的类型所在的位置。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.field {
type(BooleanType).index().first()
}.get(instance).any() // 得到第一个类型为 Boolean 的变量
以上两种情况均可得到对应的变量 private boolean a
。
同样地,这个 Class
中也有两个混淆的方法名称,它们都是 b
。
你也可以有两种方案来得到它们。
第一种方案,确定方法的名称和方法参数。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "b"
param(StringType)
}.get(instance).call("test_string") // 得到名称为 b 方法参数为 [String] 的方法
第二种方案,确定方法的参数所在的位置。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
param(StringType).index().first()
}.get(instance).call("test_string") // 得到第一个方法参数为 [String] 的方法
由于观察到这个方法在 Class
的最后一个,那我们还有一个备选方案。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
order().index().last()
}.get(instance).call("test_string") // 得到当前 Class 的最后一个方法
!> 请尽量不要使用 order
来筛选字节码的下标,它们可能是不确定的,除非你确定它在这个 Class
中的位置一定不会变。
直接调用
上面介绍的调用字节码的方法都需要使用 get(instance)
才能调用对应的方法,有没有简单一点的办法呢?
此时,你可以在任意实例上使用 current
方法来创建一个调用空间。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 假设这个 Class 是不能被直接得到的
instance.current {
// 执行 doTask 方法
method {
name = "doTask"
param(StringType)
}.call("task_name")
// 执行 stop 方法
method {
name = "stop"
emptyParam()
}.call()
// 得到 name
val name = method { name = "getName" }.string()
}
我们还可以用 superClass
调用当前 Class
父类的方法。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 假设这个 Class 是不能被直接得到的
instance.current {
// 执行父类的 doBaseTask 方法
superClass().method {
name = "doBaseTask"
param(StringType)
}.call("task_name")
}
如果你不喜欢使用一个大括号的调用域来创建当前实例的命名空间,你可以直接使用 current()
方法。
示例如下
// 假设这就是这个 Class 的实例,这个 Class 是不能被直接得到的
val instance = Test()
// 执行 doTask 方法
instance
.current()
.method {
name = "doTask"
param(StringType)
}.call("task_name")
// 执行 stop 方法
instance
.current()
.method {
name = "stop"
emptyParam()
}.call()
// 得到 name
val name = instance.current().method { name = "getName" }.string()
同样地,它们之间可以连续调用,但不允许内联调用。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 假设这个 Class 是不能被直接得到的
instance.current {
method {
name = "doTask"
param(StringType)
}.call("task_name")
}.current()
.method {
name = "stop"
emptyParam()
}.call()
// ❗注意,因为 current() 返回的是 CurrentClass 自身对象,所以不能像下面这样调用
instance.current().current()
针对 Field
实例,还有一个便捷的方法,可以直接获取 Field
所在实例的对象。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 假设这个 Class 是不能被直接得到的
instance.current {
// <方案1>
field {
name = "baseInstance"
}.current {
method {
name = "doBaseTask"
param(StringType)
}.call("task_name")
}
// <方案2>
field {
name = "baseInstance"
}.current()
?.method {
name = "doBaseTask"
param(StringType)
}?.call("task_name")
}
上述 current
方法相当于帮你调用了 CurrentClass
中的 field { ... }.any()?.current()
方法。
!> 若不存在 CurrentClass
调用域,你需要使用 field { ... }.get(instance).current()
来进行调用。
问题又来了,我想使用反射的方式创建如下的实例并调用其中的方法,该怎么做呢?
示例如下
Test(true).doTask("task_name")
通常情况下,我们可以使用标准的反射 API 来调用。
示例如下
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
方法来创建一个实例。
示例如下
classOf("com.demo.Test").buildOfAny(true) { param(BooleanType) }?.current {
method {
name = "doTask"
param(StringType)
}.call("task_name")
}
更多用法可参考 CurrentClass 以及 Class.buildOf 方法。
再次查询
假设有三个不同版本的 Class
,它们都是这个宿主不同版本相同的 Class
。
这里面同样都有一个方法 doTask
,假设它们的功能是一样的。
版本 A 示例如下
public class Test {
public void doTask() {
// ...
}
}
版本 B 示例如下
public class Test {
public void doTask(String taskName) {
// ...
}
}
版本 C 示例如下
public class Test {
public void doTask(String taskName, int type) {
// ...
}
}
我们需要在不同的版本中得到这个相同功能的 doTask
方法,要怎么做呢?
此时,你可以使用 RemedyPlan
完成你的需求。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "doTask"
emptyParam()
}.remedys {
method {
name = "doTask"
param(StringType)
}.onFind {
// 可在这里实现找到的逻辑
}
method {
name = "doTask"
param(StringType, IntType)
}.onFind {
// 可在这里实现找到的逻辑
}
}.wait(instance) {
// 得到方法的结果
}
!> 特别注意使用了 RemedyPlan
的方法查询结果不能再使用 get
的方式得到方法实例,应当使用 wait
方法。
另外,你还可以在使用 多重查询 的情况下继续使用 RemedyPlan
。
示例如下
// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
name = "doTask"
emptyParam()
}.remedys {
method {
name = "doTask"
paramCount(0..1)
}.onFind {
// 可在这里实现找到的逻辑
}
method {
name = "doTask"
paramCount(1..2)
}.onFind {
// 可在这里实现找到的逻辑
}
}.waitAll(instance) {
// 得到方法的结果
}
以当前 Class
举例,若 多重查询 结合 RemedyPlan
在创建 Hook 的时候使用,你需要稍微改变一下用法。
示例如下
injectMember {
method {
name = "doTask"
emptyParam()
}.remedys {
method {
name = "doTask"
paramCount(0..1)
}
method {
name = "doTask"
paramCount(1..2)
}
}.all()
beforeHook {}
afterHook {}
}
更多用法可参考 Method RemedyPlan、Constructor RemedyPlan、Field RemedyPlan。
相对匹配
假设宿主中不同版本中存在功能相同的 Class
但仅有 Class
的名称不一样。
版本 A 示例如下
public class ATest {
public static void doTask() {
// ...
}
}
版本 B 示例如下
public class BTest {
public static void doTask() {
// ...
}
}
这个时候我们想在每个版本都调用这个 Class
里的 doTask
方法该怎么做呢?
通常做法是判断 Class
是否存在。
示例如下
// 首先查询到这个 Class
val currentClass = if("com.demo.ATest".hasClass) classOf("com.demo.ATest") else classOf("com.demo.BTest")
// 然后再查询这个方法并调用
currentClass.method {
name = "doTask"
emptyParam()
}.get().call()
感觉这种方案非常的不优雅且繁琐,那么此时 YukiHookAPI
就为你提供了一个非常方便的 VariousClass
专门来解决这个问题。
现在,你可以直接使用以下方式获取到这个 Class
。
示例如下
VariousClass("com.demo.ATest", "com.demo.BTest").get().method {
name = "doTask"
emptyParam()
}.get().call()
若当前 Class
在指定的 ClassLoader
中存在,你可以在 get
中填入你的 ClassLoader
。
示例如下
val customClassLoader: ClassLoader? = ... // 假设这个就是你的 ClassLoader
VariousClass("com.demo.ATest", "com.demo.BTest").get(customClassLoader).method {
name = "doTask"
emptyParam()
}.get().call()
若你正在 PackageParam
中操作 (Xposed) 宿主环境的 Class
,可以直接使用 clazz
进行设置。
示例如下
VariousClass("com.demo.ATest", "com.demo.BTest").clazz.method {
name = "doTask"
emptyParam()
}.get().call()
更多用法可参考 VariousClass。
若在创建 Hook 的时候使用,可以更加方便,还可以自动拦截找不到 Class
的异常。
示例如下
findClass("com.demo.ATest", "com.demo.BTest").hook {
// Your code here.
}
你还可以把这个 Class
定义为一个常量类型来使用。
示例如下
// 定义常量类型
val ABTestClass = VariousClass("com.demo.ATest", "com.demo.BTest")
// 直接使用
ABTestClass.hook {
// Your code here.
}
更多用法可参考 findClass 方法。
注意误区
这里列举了使用时可能会遇到的误区部分,可供参考。
限制性查询条件
!> 在查询条件中,除了 order
你只能使用一次 index
功能。
示例如下
method {
name = "test"
param(BooleanType).index(num = 2)
// ❗错误的使用方法,请仅保留一个 index 方法
returnType(StringType).index(num = 1)
}
以下查询条件的使用是没有任何问题的。
示例如下
method {
name = "test"
param(BooleanType).index(num = 2)
order().index(num = 1)
}
必要的查询条件
!> 在普通方法查询条件中,即使是无参的方法也需要设置查询条件。
假设我们有如下的 Class
。
示例如下
public class TestFoo {
public void foo(String string) {
// ...
}
public void foo() {
// ...
}
}
我们要得到其中的 public void foo()
方法,可以写作如下形式。
示例如下
TestFoo::class.java.method {
name = "foo"
}
但是,上面的例子是错误的。
你会发现这个 Class
中有两个 foo
方法,其中一个带有方法参数。
由于上述例子没有设置 param
的查询条件,得到的结果将会是匹配名称且匹配字节码顺序的第一个方法 public void foo(String string)
,而不是我们需要的最后一个方法。
这是一个经常会出现的错误,没有方法参数就会丢失方法参数查询条件的使用问题。
正确的使用方法如下。
示例如下
TestFoo::class.java.method {
name = "foo"
// ✅ 正确的使用方法,添加详细的筛选条件
emptyParam()
}
至此,上述的示例将可以完美地匹配到 public void foo()
方法。
PS:在较旧的 API 版本中是允许匹配不写默认匹配无参方法的做法的,但是最新版本更正了这一问题,请确保你使用的是最新的 API 版本。
可简写查询条件
在构造方法查询条件中,无参的构造方法可以不需要填写查询条件。
假设我们有如下的 Class
。
示例如下
public class TestFoo {
public TestFoo() {
// ...
}
}
我们要得到其中的 public TestFoo()
构造方法,可以写作如下形式。
示例如下
TestFoo::class.java.constructor { emptyParam() }
上面的例子可以成功获取到 public TestFoo()
构造方法,但是感觉有一些繁琐。
与普通方法不同,由于构造方法不需要考虑 name
名称,当构造方法没有参数的时候,我们可以省略 emptyParam
参数。
示例如下
TestFoo::class.java.constructor()
!> PS:在旧的 API 版本中构造方法不填写任何查询参数会直接找不到构造方法,这是一个 BUG,最新版本已经进行修复,请确保你使用的是最新的 API 版本。
字节码类型
!> 在字节码调用结果中,cast
方法只能指定字节码对应的类型。
例如我们想得到一个 Boolean
类型的变量,把他转换为 String
。
以下是错误的使用方法。
示例如下
field {
name = "test"
type = BooleanType
}.get().string() // ❗错误的使用方法,必须 cast 为字节码目标类型
以下是正确的使用方法。
示例如下
field {
name = "test"
type = BooleanType
}.get().boolean().toString() // ✅ 正确的使用方法,得到类型后再进行转换
常用类型扩展功能
在查询方法、变量的时候我们通常需要指定所查询的类型。
示例如下
field {
name = "test"
type = Boolean::class.java
}
在 Kotlin
中表达出 Boolean::class.java
这个类型的写法很长,感觉并不方便。
因此,YukiHookAPI
为开发者封装了常见的类型调用,其中包含了 Android 的基本类型和 Java 的基本类型。
这个时候上面的类型就可以写作如下形式了。
示例如下
field {
name = "test"
type = BooleanType
}
在 Java 中常见的基本类型都已被封装为 类型 + Type 的方式,例如 IntType
、FloatType
。
相应地,数组类型也有方便的使用方法,假设我们要获得 String[]
类型的数组。
需要写做 java.lang.reflect.Array.newInstance(String::class.java, 0).javaClass
才能得到这个类型。
感觉是不是很麻烦,这个时候我们可以使用扩展方法 ArrayClass(StringType)
来得到这个类型。
同时由于 String
是常见类型,所以还可以直接使用 StringArrayClass
来得到这个类型。
一些常见的 Hook 中查询的方法,都有其对应的封装类型以供使用,格式为 类型 + Class。
例如 Hook onCreate
方法需要查询 Bundle::class.java
类型。
示例如下
method {
name = "onCreate"
param(BundleClass)
}
更多类型请 点击这里 前往查看,也欢迎你能贡献更多的常用类型。
调试日志功能
日志是调试过程最重要的一环,
YukiHookAPI
为开发者封装了一套稳定高效的调试日志功能。
普通日志
你可以调用 loggerD
、loggerI
、loggerW
来向控制台打印普通日志。
使用方法如下所示。
示例如下
loggerD(msg = "This is a log")
此时,YukiHookAPI
会调用 android.util.Log
与 XposedBridge.log
同时打印这条日志。
日志默认的 TAG
为你在 YukiHookAPI.Configs.debugTag
中设置的值。
你也可以动态自定义这个值,但是不建议轻易修改 TAG
防止过滤不到日志。
示例如下
loggerD(tag = "YukiHookAPI", msg = "This is a log")
打印的结果为如下所示。
示例如下
[YukiHookAPI][D][宿主包名]--> This is a log
你还可以使用 LoggerType
自定义日志打印的类型,可选择使用 android.util.Log
还是 XposedBridge.log
来打印日志。
默认类型为 LoggerType.BOTH
,含义为同时使用这两个方法来打印日志。
比如我们仅使用 android.util.Log
来打印日志。
示例如下
loggerD(tag = "YukiHookAPI", msg = "This is a log", type = LoggerType.LOGD)
或又仅使用 XposedBridge.log
来打印日志,此方法仅可在 (Xposed) 宿主环境使用。
示例如下
loggerD(tag = "YukiHookAPI", msg = "This is a log", type = LoggerType.XPOSEDBRIDGE)
若你想智能区分 (Xposed) 宿主环境与模块环境,可以写为如下形式。
示例如下
loggerD(tag = "YukiHookAPI", msg = "This is a log", type = LoggerType.SCOPE)
这样 API 就会在不同环境智能选择指定的方法类型去打印这条日志。
更多用法可参考 loggerD、loggerI 及 loggerW 方法。
错误日志
你可以调用 loggerE
来向控制台打印 E
级别的日志。
使用方法如下所示。
示例如下
loggerE(msg = "This is an error")
错误日志的级别是最高的,无论你有没有过滤仅为 E
级别的日志。
对于错误级别的日志,你还可以在后面加上一个异常堆栈。
// 假设这就是被抛出的异常
val throwable = Throwable(...)
// 打印日志
loggerE(msg = "This is an error", e = throwable)
打印的结果为如下所示。
示例如下
[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
在错误日志中,你同样也可以使用 LoggerType
来指定当前打印日志所用到的方法类型。
更多用法可参考 loggerE 方法。
Xposed 模块数据存储功能
这是一个自动对接
SharedPreferences
和XSharedPreferences
的高效模块数据存储解决方案。
我们需要存储模块的数据,以供宿主调用,这个时候会遇到原生 Sp
存储的数据互通阻碍。
原生的 Xposed
给我们提供了一个 XSharedPreferences
用于读取模块的 Sp
数据。
在 Activity 中使用
这里描述了在
Activity
中装载YukiHookModulePrefs
的场景。
通常情况下我们可以这样在 Hook 内对其进行初始化。
示例如下
XSharedPreferences(BuildConfig.APPLICATION_ID)
有没有方便快捷的解决方案呢,此时你就可以使用 YukiHookAPI
的扩展能力快速实现这个功能。
当你在模块中存储数据的时候,若当前处于 Activity
内,可以使用如下方法。
示例如下
modulePrefs.putString("test_name", "saved_value")
当你在 Hook 中读取数据时,可以使用如下方法。
示例如下
val testName = prefs.getString("test_name", "default_value")
你不需要考虑传入模块的包名以及一系列复杂的权限配置,一切都交给 YukiHookModulePrefs
来处理。
若要实现存储的区域划分,你可以指定每个 prefs
文件的名称。
在模块的 Activity
中这样使用。
示例如下
// 推荐用法
modulePrefs("specify_file_name").putString("test_name", "saved_value")
// 也可以这样用
modulePrefs.name("specify_file_name").putString("test_name", "saved_value")
在 Hook 中这样读取。
示例如下
// 推荐用法
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。
更多用法可参考 YukiHookModulePrefs。
在 PreferenceFragment 中使用
这里描述了在
PreferenceFragment
中装载YukiHookModulePrefs
的场景。
若你的模块使用了 PreferenceFragmentCompat
,你现在可以将其继承类开始迁移到 ModulePreferenceFragment
。
!> 你必须继承 ModulePreferenceFragment
才能实现 YukiHookModulePrefs
的模块存储功能。
详情请参考 ModulePreferenceFragment。
Xposed 模块与宿主通讯桥功能
这是一个使用系统无序广播在模块与宿主之间发送和接收数据的解决方案。
!> 需要满足的条件:模块与宿主需要保持存活状态,否则无法建立通讯。
基本用法
这里描述了
wait
与put
方法的基本使用方法。
通过使用 dataChannel
来实现模块与宿主之间的通讯桥,原理为发送接收系统无序广播。
模块示例如下
// 从指定包名的宿主获取
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")
宿主示例如下
// 从模块获取
dataChannel.wait<String>(key = "key_from_module") { value ->
// Your code here.
}
// 发送给模块
dataChannel.put(key = "key_from_host", value = "I am host")
你可以不设置 dataChannel
的 value
来达到仅通知模块或宿主回调 wait
方法。
模块示例如下
// 从指定包名的宿主获取
dataChannel(packageName = "com.example.demo").wait(key = "listener_from_host") {
// Your code here.
}
// 发送给指定包名的宿主
dataChannel(packageName = "com.example.demo").put(key = "listener_from_module")
宿主示例如下
// 从模块获取
dataChannel.wait(key = "listener_from_module") {
// Your code here.
}
// 发送给模块
dataChannel.put(key = "listener_from_host")
!> 接收方需要保持存活状态才能收到通讯数据。
详情请参考 YukiHookDataChannel。
判断模块与宿主版本是否匹配
通过通讯桥功能,
YukiHookAPI
还为你提供了在用户更新模块后,判断模块是否与宿主版本匹配的解决方案。
我们只需要调用 checkingVersionEquals
方法,即可实现这个功能。
在模块与宿主中可进行双向判断。
你可以在模块中判断指定包名的宿主是否与当前模块的版本匹配。
示例如下
// 从指定包名的宿主获取
dataChannel(packageName = "com.example.demo").checkingVersionEquals { isEquals ->
// Your code here.
}
你还可以在宿主中判断是否自身与当前模块的版本匹配。
示例如下
// 从模块获取
dataChannel.checkingVersionEquals { isEquals ->
// Your code here.
}
!> 方法回调的条件为宿主、模块保持存活状态,并在激活模块后重启了作用域中的 Hook 目标宿主对象。
详情请参考 YukiHookDataChannel。
回调事件响应的规则
!> 在模块和宿主中,每一个 dataChannel
对应的 key
的回调事件都不允许重复创建,若重复,之前的回调事件会被新增加的回调事件替换,若在模块中使用,在同一个 Activity
中不可以重复,不同的 Activity
中相同的 key
允许重复。
这里只列出了在模块中使用的例子,在宿主中相同的 key
始终不允许重复创建。
示例如下
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 都可被创建成功。
!> 一个相同 key
的回调事件只会回调当前模块正在显示的 Activity
中注册的回调事件,例如上述中的 test_key
,如果 OtherActivity
正在显示,那么 MainActivity
中的 test_key
就不会被回调。
!> 请特别注意,相同的 key
在同一个 Activity
不同的 Fragment
中注册 dataChannel
,它们依然会在当前 Activity
中同时被回调。
!> 在模块中,你只能使用 Activity
的 Context
注册 dataChannel
,你不能在 Application
以及 Service
等地方使用 dataChannel
,若要在 Fragment
中使用 dataChannel
,请使用 activity?.dataChannel(...)
。
安全性说明
!> 在模块环境中,你只能接收指定包名的宿主发送的通讯数据且只能发送给指定包名的宿主。
为了进一步防止广播滥用,通讯数据中 API 会自动指定宿主和模块的包名,防止其它 APP 监听并利用广播做出超限行为。
宿主生命周期扩展功能
这是一个自动 Hook 宿主 APP 生命周期的扩展功能。
监听生命周期
通过自动化 Hook 宿主 APP 的生命周期方法,来实现监听功能。
我们需要监听宿主 Application
的启动和生命周期方法,只需要使用以下方式实现。
示例如下
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 ->
// ...
}
}
}
详情请参考 AppLifecycle。
注册系统广播
通过
Application.onCreate
方法注册系统广播,来实现对系统广播的监听。
我们还可以在宿主 Application
中注册系统广播。
示例如下
loadApp(name = "com.example.demo") {
// 注册生命周期监听
onAppLifecycle {
// 注册用户解锁时的广播监听
registerReceiver(Intent.ACTION_USER_PRESENT) { context, intent ->
// ...
}
// 注册多个广播监听 - 会同时回调多次
registerReceiver(Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_TIME_TICK) { context, intent ->
// ...
}
}
}
详情请参考 AppLifecycle。
宿主资源注入扩展功能
这是一个将模块资源、
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(R.style.Theme_AppCompat, Configuration().apply { uiMode = Configuration.UI_MODE_NIGHT_YES })
// 直接使用这个包装了模块主题后的 Context 创建对话框
MaterialAlertDialogBuilder(appCompatContext)
.setTitle("AppCompat 主题对话框")
.setMessage("我是一个在宿主中显示的 AppCompat 主题对话框。")
.setPositiveButton("确定", null)
.show()
}
}
这样,我们就可以在宿主中非常简单地使用 MaterialAlertDialogBuilder
创建对话框了。
- 可能存在的问题
由于一些 APP 自身使用的 androidx
依赖库或自定义主题可能会对当前 MaterialAlertDialog
实际样式造成干扰,你可以参考 模块 APP Demo 来修复这个问题。
某些 APP 在创建时可能会发生 ClassCastException
异常,请手动指定新的 Configuration
实例来进行修复。
详情请参考 Context.applyModuleTheme。