Files
YukiHookAPI/docs/guide/special-feature.md

50 KiB
Raw Blame History

特色功能

除了基本的 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() // 得到方法的结果

是的,对于确切不会变化的方法,你可以精简查询条件。

在只使用 getwait 方法得到结果时 YukiHookAPI 会默认按照字节码顺序匹配第一个查询到的结果

问题又来了,这个 Class 中有一个 release 方法,但是它的方法参数好长,而且很多的类型都无法直接得到。

通常情况下我们会使用 param(...) 来查询这个方法,但是有没有更简单的方法呢。

此时,在确定方法唯一性后,你可以使用 paramCount 来查询到这个方法。

示例如下

// 假设这就是这个 Class 的实例
val instance = Test()
// 使用 YukiHookAPI 调用并执行
Test::class.java.method {
    name = "release"
    // 此时我们不必确定方法参数具体类型,写个数就好
    paramCount = 3
}.get(instance) // 得到这个方法

在父类查询

你会注意到 Test 继承于 BaseTest,现在我们想得到 BaseTestdoBaseTask 方法,在不知道父类名称的情况下,要怎么做呢?

参照上面的查询条件,我们只需要在查询条件中加入一个 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 条件,否则只能查询到当前 ClassMethod

模糊查询

如果我们想查询一个方法名称,但是又不确定它在每个版本中是否发生变化,此时我们就可以使用模糊查询功能。

假设我们要得到 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 来调用。

示例如下

"com.demo.Test".toClass()
    .getDeclaredConstructor(Boolean::class.java)
    .apply { isAccessible = true }
    .newInstance(true)
    .apply {
        javaClass
            .getDeclaredMethod("doTask", String::class.java)
            .apply { isAccessible = true }
            .invoke(this, "task_name")
    }

但是感觉这种做法好麻烦,有没有更简洁的调用方法呢?

这个时候,我们还可以借助 buildOfbuildOfAny 方法来创建一个实例。

示例如下

"com.demo.Test".toClass().buildOfAny(true) { param(BooleanType) }?.current {
    method {
        name = "doTask"
        param(StringType)
    }.call("task_name")
}

更多用法可参考 CurrentClass 以及 Class.buildOf 方法。

原始调用

若你正在使用反射调用的一个方法是被 Hook 过的,此时我们如何调用其原始方法呢?

XposedBridge 为我们提供了一个 XposedBridge.invokeOriginalMethod 功能,现在,在 YukiHookAPI 中你可以便捷地实现这个功能。

假设下面是我们要演示的 Class

示例如下

public class Test {

    public static String getString() {
        return "Original";
    }
}

下面是 Hook 这个 ClassgetString 方法的方式。

示例如下

Test::class.java.hook {
    injectMember {
        method {
            name = "getString"
            emptyParam()
            returnType = StringType
        }
        replaceTo("Hooked")
    }
}

此时,我们再使用反射调用这个方法,则会得到 Hook 后的结果 "Hooked"

示例如下

// result 的结果会是 "Hooked"
val result = Test::class.java.method {
    name = "getString"
    emptyParam()
    returnType = StringType
}.get().string()

如果我们想得到这个方法未经 Hook 的原始方法及结果,只需要在结果中加入 original 即可。

示例如下

// result 的结果会是 "Original"
val result = Test::class.java.method {
    name = "getString"
    emptyParam()
    returnType = StringType
}.get().original().string()

更多用法可参考 MethodFinder.Result.original 方法。

再次查询

假设有三个不同版本的 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 {}
}

在创建 Hook 的时候使用可参考 MethodFinder.Process.allConstructorFinder.Process.all

更多用法可参考 MethodFinder.RemedyPlanConstructorFinder.RemedyPlanFieldFinder.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()) "com.demo.ATest".toClass() else "com.demo.BTest".toClass()
// 然后再查询这个方法并调用
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 的方式,例如 IntTypeFloatType

相应地,数组类型也有方便的使用方法,假设我们要获得 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 为开发者封装了一套稳定高效的调试日志功能。

普通日志

你可以调用 loggerDloggerIloggerW 来向控制台打印普通日志。

使用方法如下所示。

示例如下

loggerD(msg = "This is a log")

此时,YukiHookAPI 会调用 android.util.LogXposedBridge.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 就会在不同环境智能选择指定的方法类型去打印这条日志。

更多用法可参考 loggerDloggerIloggerW 方法。

错误日志

你可以调用 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 模块数据存储功能

这是一个自动对接 SharedPreferencesXSharedPreferences 的高效模块数据存储解决方案。

我们需要存储模块的数据,以供宿主调用,这个时候会遇到原生 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 模块与宿主通讯桥功能

这是一个使用系统无序广播在模块与宿主之间发送和接收数据的解决方案。

!> 需要满足的条件:模块与宿主需要保持存活状态,否则无法建立通讯。

基本用法

这里描述了 waitput 方法的基本使用方法。

通过使用 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")

你可以不设置 dataChannelvalue 来达到仅通知模块或宿主回调 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 中同时被回调。

!> 在模块中,你只能使用 ActivityContext 注册 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 继承于 ModuleAppActivityModuleAppCompatActivity

这些 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



浏览下一篇  ➡️