用法示例

这里介绍了 YukiHookAPI 的基本工作方式以及列举了简单的 Hook 例子和常用功能。

结构图解

下方的结构描述了 YukiHookAPI 的基本工作方式和原理。

Host Environment
└─ YukiMemberHookCreator
   └─ Class
      └─ MemberHookCreator
         └─ Member
            ├─ Before
            └─ After
         MemberHookCreator
         └─ Member
            ├─ Before
            └─ After
         ...
   YukiResourcesHookCreator
   └─ Resources
      └─ ResourcesHookCreator
         └─ Drawable
            └─ Replace
         ResourcesHookCreator
         └─ Layout
            └─ Inject
         ...

上方的结构换做代码将可写为如下形式。

// 新版写法
TargetClass.method { 
    // Your code here.
}.hook {
    before {
        // Your code here.
    }
    after {
        // Your code here.
    }
}
// 旧版写法
TargetClass.hook { 
    injectMember { 
        method { 
            // Your code here.
        }
        beforeHook {
            // Your code here.
        }
        afterHook {
            // Your code here.
        }
    }
}
// Resources Hook (2.0.0 将停止支持)
resources().hook {
    injectResource {
        conditions {
            // Your code here.
        }
        replaceTo(...)
    }
}

Demo

你可以在下方找到 API 提供的 Demo 来学习 YukiHookAPI 的使用方法。

同时安装宿主和模块 Demo,通过激活模块来测试宿主中被 Hook 的功能。

一个简单的 Hook 例子

这里给出了 Hook APP、Hook 系统框架与 Hook Resources 等例子,可供参考。

Hook APP

假设,我们要 Hook com.android.browser 中的 onCreate 方法并弹出一个对话框。

encase 方法体中添加代码。

示例如下

loadApp(name = "com.android.browser") {
    ActivityClass.method { 
        name = "onCreate"
        param(BundleClass)
        returnType = UnitType
    }.hook {
        after {
            AlertDialog.Builder(instance())
                .setTitle("Hooked")
                .setMessage("I am hook!")
                .setPositiveButton("OK", null)
                .show()
        }
    }
}

至此,onCreate 方法将被成功 Hook 并在 com.android.browser 中的每个 Activity 启动时弹出此对话框。

那么,我想继续 Hook onStart 方法要怎么做呢?

我们可以对 ActivityClass 使用 Kotlin 的 apply 方法创建一个调用空间。

示例如下

loadApp(name = "com.android.browser") {
    ActivityClass.apply { 
        method { 
            name = "onCreate"
            param(BundleClass)
            returnType = UnitType
        }.hook {
            after {
                AlertDialog.Builder(instance())
                    .setTitle("Hooked")
                    .setMessage("I am hook!")
                    .setPositiveButton("OK", null)
                    .show()
            }
        }
        method { 
            name = "onStart"
            emptyParam()
            returnType = UnitType
        }.hook {
            after {
                // Your code here.
            }
        }
    }
}

对于当前项目下没有的 Class,你可以使用 stub 方式或 String.toClass(...) 方法来得到需要 Hook 的类。

比如,我要得到 com.example.demo.TestClass

示例如下

"com.example.demo.TestClass".toClass()
    .method {
        // Your code here.
    }.hook {
        // Your code here.
    }

com.example.demo 是你要 Hook 的 APP,那么写法可以更简单。

示例如下

"$packageName.TestClass".toClass()
    .method {
        // Your code here.
    }.hook {
        // Your code here.
    }

若这个 Class 不是马上就能被得到的,你可以使用 lazyClass(...) 来定义它。

示例如下

定义 TestClass

val TestClass by lazyClass("com.example.demo.TestClass")

在适当的时候使用它。

TestClass.method {
    // Your code here.
}.hook {
    // Your code here.
}

小提示

更多功能请参考 MemberHookCreator

Hook Zygote

在 APP 启动时,新的进程被 fork 后的第一个事件 initZygote

假设我们要全局 Hook 一个 APP ActivityonCreate 事件

encase 方法体中添加代码。

示例如下

loadZygote {
    ActivityClass.method { 
        name = "onCreate"
        param(BundleClass)
        returnType = UnitType
    }.hook {
        after {
            // Your code here.
        }
    }
}

注意

loadZygote 中进行的功能十分有限,几乎很少的情况下需要用到 loadZygote 方法。

Hook 系统框架

YukiHookAPI 中,Hook 系统框架的实现非常简单。

假设,你要得到 ApplicationInfoPackageInfo 并对它们进行一些操作。

encase 方法体中添加代码。

示例如下

loadSystem {
    ApplicationInfoClass.method {
        // Your code here.
    }.hook {
        // Your code here.
    }
    PackageInfoClass.method {
        // Your code here.
    }.hook {
        // Your code here.
    }
}

特别注意

loadZygoteloadSystem 有直接性区别,loadZygote 会在 initZygote 中装载,系统框架被视为 loadApp(name = "android") 而存在,若要 Hook 系统框架,可直接使用 loadSystem

Hook Resources

注意

此功能将在 2.0.0 版本停止支持并移除。

假设,我们要 Hook com.android.browserstring 类型的 app_name 内容替换为 123

encase 方法体中添加代码。

示例如下

loadApp(name = "com.android.browser") {
    resources().hook {
        injectResource {
            conditions {
                name = "app_name"
                string()
            }
            replaceTo("123")
        }
    }
}

若当前 APP 使用 app_name 设置了标题栏文本,则它就会变成我们的 123

你还可以使用当前 Xposed 模块的 Resources 替换 Hook APP 的 Resources。

假设,我们要继续 Hook com.android.browsermipmap 类型的 ic_launcher

示例如下

loadApp(name = "com.android.browser") {
    resources().hook {
        injectResource {
            conditions {
                name = "ic_launcher"
                mipmap()
            }
            replaceToModuleResource(R.mipmap.ic_launcher)
        }
    }
}

至此目标 APP 的图标将会被替换为我们设置的图标。

若你想替换系统框架的资源,同样也可以这样实现,只需要把 loadApp 换成 loadZygote 即可。

示例如下

loadZygote {
    resources().hook {
        // Your code here.
    }
}

小提示

更多功能请参考 ResourcesHookCreator

解除 Hook

原生的 Xposed 为我们提供了一个 XC_MethodHook.Unhook 功能,可以从 Hook 队列中将当前 Hook 移除,YukiHookAPI 同样可以实现此功能。

第一种方法,保存当前注入对象的 Result 实例,在适当的时候和地方调用 remove 即可解除该注入对象。

示例如下

// 设置一个变量保存当前实例
val hookResult = 
    method { 
        name = "test"
        returnType = UnitType
    }.hook {
        after {
            // ...
        }
    }
// 在适当的时候调用如下方法即可
hookResult.remove()

第二种方法,在 Hook 回调方法中调用 removeSelf 移除自身。

示例如下

method { 
    name = "test"
    returnType = UnitType
}.hook {
    after {
        // 直接调用如下方法即可
        removeSelf()
    }
}

小提示

更多功能请参考 MemberHookCreator

异常处理

YukiHookAPI 重新设计了对异常的监听,任何异常都不会在 Hook 过程中抛出,避免打断下一个 Hook 流程导致 Hook 进程“死掉”。

监听异常

你可以处理 Hook 方法过程发生的异常。

示例如下

hook {
    // Your code here.
}.result {
    // 处理 Hook 开始时的异常
    onHookingFailure {}
    // 处理 Hook 过程中的异常
    onConductFailure { param, throwable -> }
    // 处理全部异常
    onAllFailure {}
    // ...
}

在 Resources Hook 时此方法同样适用。

示例如下

injectResource {
    // Your code here.
}.result {
    // 处理 Hook 时的任意异常
    onHookingFailure {}
    // ...
}

(旧版本适用) 你还可以处理 Hook 的 Class 不存在时发生的异常。

示例如下

TargetClass.hook {
    injectMember {
        // Your code here.
    }
}.onHookClassNotFoundFailure {
    // Your code here.
}

你还可以处理查找方法时的异常。

示例如下

method {
    // Your code here.
}.onNoSuchMethod {
    // Your code here.
}

小提示

更多功能请参考 MemberHookCreator.ResultResourcesHookCreator.Result

这里介绍了可能发生的常见异常,若要了解更多请参考 API 异常处理

抛出异常

在某些情况下,你可以手动抛出异常来达到提醒某些功能存在问题的目的。

上面已经介绍过,在 hook 方法体内抛出的异常会被 YukiHookAPI 接管,避免打断下一个 Hook 流程导致 Hook 进程“死掉”。

以下是 YukiHookAPI 接管时这些异常的运作方式。

示例如下

// <情景1>
injectMember {
    method {
        throw RuntimeException("Exception Test")
    }
    afterHook {
        // ...
    }
}.result {
    // 能够捕获到 RuntimeException
    onHookingFailure {}
}
// <情景2>
injectMember {
    method {
        // ...
    }
    afterHook {
        throw RuntimeException("Exception Test")
    }
}.result {
    // 能够捕获到 RuntimeException
    onConductFailure { param, throwable -> }
}

以上情景只会在 (Xposed) 宿主环境被处理,不会对宿主自身造成任何影响。

若我们想将这些异常直接抛给宿主,原生的 Xposed 为我们提供了 param.throwable 方法,YukiHookAPI 同样可以实现此功能。

若想在 Hook 回调方法体中将一个异常直接抛给宿主,可以有如下实现方法。

示例如下

method {
    // ...
}.hook {
    after {
        RuntimeException("Exception Test").throwToApp()
    }
}

你也可以直接在 Hook 回调方法体中抛出异常,然后标识将异常抛给宿主。

示例如下

method {
    // ...
}.hook {
    after {
        throw RuntimeException("Exception Test")
    }.onFailureThrowToApp()
}

以上两种方法均可在宿主接收到异常从而使宿主进程崩溃。

注意

为了保证 Hook 调用域与宿主内调用域相互隔离,异常只有在 beforeafter 回调方法体中才能抛给宿主。

扩展用法

你可以在 Hook 过程中使用下面的方法方便地实现各种判断和功能。

多个宿主

如果你的模块需要同时处理多个 APP 的 Hook 事件,你可以使用 loadApp 方法体来区分你要 Hook 的 APP。

示例如下

loadApp(name = "com.android.browser") {
    // Your code here.
}
loadApp(name = "com.android.phone") {
    // Your code here.
}

小提示

更多功能请参考 PackageParam.loadApp

多个进程

如果你 Hook 的宿主 APP 有多个进程,你可以使用 withProcess 方法体来对它们分别进行 Hook。

示例如下

withProcess(mainProcessName) {
    // Your code here.
}
withProcess(name = "$packageName:tool") {
    // Your code here.
}

小提示

更多功能请参考 PackageParam.withProcess

写法优化

为了使代码更加简洁,你可以删去 YukiHookAPI 的名称,将你的 onHook 入口写作 lambda 形式。

示例如下

override fun onHook() = encase {
    // Your code here.
}

你还可以在仅需要一个 Hook 回调事件的时候简写 hook { ... } 方法体。

示例如下

ActvityClass.method {
    // Your code here.
}.hook().after {
    // Your code here.
}

Xposed 模块状态

通常情况下,Xposed 模块的开发者都会去选择读取当前 Xposed 模块的激活信息以更好地向用户展示当前功能的生效状态。

除了基本的 Hook 功能,YukiHookAPI 还为开发者设计了一套 Xposed 模块状态判断的功能,如激活状态、Hook Framework 信息。

判断自身激活状态

通常情况下,我们会选择写一个方法,使其返回 false,然后 Hook 掉这个方法使其返回 true 来证明 Hook 已经生效。

YukiHookAPI 中你完全不需要再这么做了,YukiHookAPI 已经帮你封装好了这个操作,你可以直接进行使用。

现在,你可以直接使用 YukiHookAPI.Status.isXposedModuleActive 在模块中判断自身是否被激活。

示例如下

if(YukiHookAPI.Status.isXposedModuleActive) {
    // Your code here.
}

由于一些特殊原因,在太极、无极中的模块无法使用标准方法检测激活状态。

此时你可以使用 YukiHookAPI.Status.isTaiChiModuleActive 判断自身是否被激活。

示例如下

if(YukiHookAPI.Status.isTaiChiModuleActive) {
    // Your code here.
}

若你想使用两者得兼的判断方案,YukiHookAPI 同样为你封装了便捷的方式。

此时你可以使用 YukiHookAPI.Status.isModuleActive 判断自身是否在 Xposed 或太极、无极中被激活。

示例如下

if(YukiHookAPI.Status.isModuleActive) {
    // Your code here.
}

小提示

更多功能请参考 YukiHookAPI.Status

注意

如果你的模块 API 版本高于 29 且正在目标 API 为 29 以上的系统中运行,你需要在 AndroidManifest.xml 中添加如下权限声明才能正常判断模块在太极、无极中的激活状态。

示例如下

<queries>
    <intent>
        <action android:name="android.intent.action.MAIN" />
    </intent>
</queries>

还有一种方案,你可以直接声明 android.permission.QUERY_ALL_PACKAGES 权限,但是不推荐且会被代码检查警告。

示例如下

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />

若模块激活判断中包含太极、无极中的激活状态,就必须将模块的 Application 继承于 ModuleApplication 或直接使用 ModuleApplication

获取 Hook Framework 信息

除了判断自身激活状态之外,你还可以通过 YukiHookAPI.Status 中的 Executor 来获取当前 Hook Framework 的相关信息。

例如我们可以使用 YukiHookAPI.Status.Executor.name 来获取当前 Hook Framework 的名称。

示例如下

val frameworkName = YukiHookAPI.Status.Executor.name

我们还可以使用 YukiHookAPI.Status.Executor.apiLevel 来获取当前 Hook Framework 的 API Level。

示例如下

val frameworkApiLevel = YukiHookAPI.Status.Executor.apiLevel

小提示

更多功能请参考 YukiHookAPI.Status.Executor

注意

1.0.91 版本后的 YukiHookAPI 修改了获取 Xposed 模块状态的逻辑判断方式,现在你可以在模块与 Hook APP (宿主) 中同时使用此 API;

需要确保 InjectYukiHookWithXposed.isUsingXposedModuleStatus 是启用状态;

YukiHookAPI 仅对已知的获取方式进行了对接,除了提供标准 API 的 Hook Framework 之外,其它情况下模块可能都将无法判断自己是否被激活或是获取 Hook Framework 的相关信息。