Skip to content

直接 Hook API ​

通过 WorkshopApi.hookRegistrar 注册普通方法 Hook 或修改 UI,Java 对应 WorkshopApi.hookRegistrar()。Hook API 从 0.1.0-dev22 起提供,目前标记为 UnstableSpwWorkshopApi。

如果尚未创建插件项目,请先阅读 开发入门,并确认目标播放器提供所需 API 版本。

开始使用 ​

在插件的 spmod {} 配置中声明权限,并由用户授权。

kotlin
import com.xuncorp.spw.workshop.gradle.PluginPermission

spmod {
    PluginPermissions = listOf(PluginPermission.CLASS_TRANSFORM)
}

注册前可通过 WorkshopApi.manager.isPermissionGranted(PluginPermission.CLASS_TRANSFORM) 检查权限。完整的声明、授权和拒绝处理见 插件权限。

下方的 com.myapp 名称仅用于说明调用方式,使用时应替换为目标宿主的实际类名和方法名。

普通方法 Hook ​

hookMethod 支持三种回调,可按需要组合。

  • before:在原方法之前执行,可以读取或修改参数,也可以提前返回
  • replace:用回调返回值替换原方法结果,需要时通过 invokeOriginal() 调用原方法
  • after:在方法完成后执行,可以读取或修改结果、处理异常

以下以 calculate(Int): Int 为目标。

kotlin
override fun start() {
    WorkshopApi.hookRegistrar.hookMethod(
        className = "com.myapp.Calculator",
        methodName = "calculate",
        parameterTypes = listOf("int")
    ) {
        before { call ->
            call.args[0] = 100
        }
        replace { call ->
            call.invokeOriginal()
        }
        after { call ->
            println(call.result)
        }
    }
}

执行顺序为 before → replace 或原方法 → after。before 调用 setResult() 或 setThrowable() 后,会跳过 replace 和原方法;未配置 replace 时,默认执行原方法。

Java 通过 MethodHook 声明 before 和 after,在 before 中调用 setResult() 即可替换返回值。

java
@Override
public void start() {
    WorkshopApi.hookRegistrar().hookMethod(
        "com.myapp.Calculator",
        "calculate",
        List.of("int"),
        new MethodHook() {
            @Override
            public void before(MethodHookParam call) throws Throwable {
                call.getArgs()[0] = 100;
                call.setResult(call.invokeOriginal());
            }

            @Override
            public void after(MethodHookParam call) {
                System.out.println(call.getResult());
            }
        }
    );
}

读取参数和处理结果 ​

Kotlin / Java用途
method / getMethod()被 Hook 的方法信息
thisObject / getThisObject()当前实例,静态方法为 null
args / getArgs()本次调用的参数数组,基本类型自动装箱
result / getResult()当前返回值
throwable / getThrowable()当前异常
setResult(value)设置返回值,并清除当前异常
setThrowable(failure)设置异常,并清除当前返回值
invokeOriginal()使用当前参数调用原方法,并返回调用结果

before 中修改 args 会影响原方法收到的参数;after 中调用 setResult() 或 setThrowable() 会修改最终结果。setResult(null) 也表示明确返回,void 方法可使用 null 或 Kotlin Unit。

invokeOriginal() 不会再次触发本次入口的 Hook,也不会自动设置返回值。原方法内部的递归调用仍可触发 Hook。

参数和返回值必须符合目标方法的类型。MethodHookParam 只在本次同步调用期间有效,不应保存或传给其他线程;回调在目标方法的调用线程执行。

优先级和异常 ​

默认优先级为 50,Kotlin 通过 priority 参数设置,Java 使用 new MethodHook(priority)。before 按优先级从高到低执行,同优先级按注册顺序;after 对已经进入的 Hook 按相反顺序执行。提前返回会停止后续 before。

replace 或原方法抛出的异常可以在 after 中读取和处理。before/after 自身意外抛出异常时,该回调对参数数组和结果的修改不生效,但对象内部修改和其他副作用不能撤销。

选择目标 ​

参数含义
className声明目标方法的类的完整名称
methodName目标方法名称
parameterTypes用于区分同名方法的参数类型列表
inClassUI 调用所在的类,修改仅作用于该类中的目标调用

parameterTypes 默认为 null,要求目标唯一;空列表表示没有业务参数。类型使用完整名称,如 java.lang.String,基本类型使用 int、long 等名称,数组使用 Class.getName() 的结果。UI Hook 只填写业务参数类型。

UI Hook 要求 inClass 中存在唯一匹配的目标调用。目标名称和签名可能随宿主版本变化,应明确插件支持的宿主版本。

替换或追加 Composable ​

replaceComposable 替换所选组件,afterComposable 在所选组件正常返回后追加内容。

kotlin
WorkshopApi.hookRegistrar.replaceComposable(
    className = "com.myapp.ui.PanelKt",
    methodName = "Panel",
    inClass = "com.myapp.ui.ScreenKt"
) { call ->
    PluginPanel(title = call.argument("title"))
}

这里的 PluginPanel 是插件自行编写的 @Composable 函数。需要在组件之后执行副作用时,使用 Effect,避免重组时重复执行。

kotlin
WorkshopApi.hookRegistrar.afterComposable(
    className = "com.myapp.ui.PanelKt",
    methodName = "Panel",
    inClass = "com.myapp.ui.ScreenKt"
) {
    LaunchedEffect(Unit) {
        WorkshopApi.ui.toast("我被Hook了", WorkshopApi.Ui.ToastType.Success)
    }
}

Java 可以注册 Kotlin 编写的 ComposableHook。

kotlin
class PanelHook : ComposableHook() {
    @Composable
    override fun Content(call: UiHookCall) {
        PluginPanel(title = call.argument("title"))
    }
}
java
WorkshopApi.hookRegistrar().replaceComposable(
    "com.myapp.ui.PanelKt",
    "Panel",
    "com.myapp.ui.ScreenKt",
    new PanelHook()
);

Composable 内容使用启用 Compose compiler 的 Kotlin 编写。UI Hook 的默认优先级为 50,Kotlin 可传入 priority;Java 的完整重载在回调前接收 parameterTypes 和 priority。

同一调用位置有多个替换时,采用最高优先级注册,同优先级采用最早注册;afterComposable 按优先级执行全部追加内容。没有有效替换时,显示原组件。

追加构建内容 ​

appendContent 用于向普通构建回调追加内容,例如菜单。parameter 指定要追加的构建参数,作用域类型应与目标组件一致。

假设目标菜单提供 MenuScope 作用域,插件通过自行编写的 PluginMenuItems.append() 构建菜单项。

kotlin
WorkshopApi.hookRegistrar.appendContent<MenuScope>(
    className = "com.myapp.ui.MenuKt",
    methodName = "Menu",
    inClass = "com.myapp.ui.ScreenKt",
    parameter = "content"
) { call ->
    PluginMenuItems.append(this, call)
}

Java 显式传入作用域类型。

java
WorkshopApi.hookRegistrar().appendContent(
    "com.myapp.ui.MenuKt",
    "Menu",
    "com.myapp.ui.ScreenKt",
    "content",
    MenuScope.class,
    (scope, call) -> PluginMenuItems.INSTANCE.append(scope, call)
);

原构建回调先执行,随后按优先级追加插件内容。这里是普通构建回调,不能直接调用 Composable;需要绘制组件时,将 Composable 内容传入目标作用域提供的内容参数。appendContent 不支持直接追加到 Composable lambda。

UI 参数与状态 ​

通过 UiHookCall 读取当前调用的业务参数。arguments / getArguments() 是不可修改的浅拷贝,基本类型自动装箱;thisObject / getThisObject() 是当前实例,静态调用时为 null。

读取参数前,请确认参数名称、索引和类型,并按以下规则处理。

  1. 按名称读取要求目标提供参数名,缺少名称信息时使用索引;索引必须位于 arguments 的有效范围内,越界会抛出 IndexOutOfBoundsException
  2. Kotlin 的 argument<T>() 会校验类型;当 T 非空且参数缺失或为 null 时,以及类型不匹配时,会抛出带参数标识的 IllegalArgumentException;将 T 声明为可空类型可接收缺失或为 null 的参数
  3. Java 的 getArgument(index, type) / getArgument(name, type) 会校验类型;缺失或为 null 时返回 null,类型不匹配时抛出 IllegalArgumentException,调用方需自行判空

下面分别展示 Kotlin 和 Java 的读取方式。

kotlin
val title = call.argument<String>("title")
val firstArgument = call.argument<Any?>(0)
java
String title = call.getArgument("title", String.class);
Object firstArgument = call.getArgument(0);

参数仅在当前组件或构建器生命周期内有效。状态更新遵循 Compose 的用法:Flow 使用 collectAsState(),异步工作使用 LaunchedEffect,需要释放资源时使用 DisposableEffect。UI 回调抛出的异常会正常向上传播。

默认参数注意事项 ​

  • replaceComposable 只替换业务参数全部显式传入的调用,使用默认参数的调用保持原组件
  • afterComposable 中应只读取已经确认显式传入的参数,不将省略的参数当作最终默认值
  • appendContent 要求目标调用显式提供构建回调,构建参数为 null 或被省略时不追加

注销与生命周期 ​

注册方法返回 HookHandle,需要提前取消时调用 unhook() 或 close(),重复调用是安全的。

kotlin
val handle = WorkshopApi.hookRegistrar.hookMethod(
    className = "com.myapp.Calculator",
    methodName = "calculate"
) {
    after { call -> println(call.result) }
}

handle.unhook()

插件停用、卸载或撤销权限后,注册会自动清理,无需为自动清理保存句柄。已经开始的回调可以完成;插件重新启用后,应重新注册所需 Hook。

常见限制与错误 ​

普通方法 Hook 不支持构造函数、native、abstract、suspend 方法和已内联的调用。Compose、Kotlin 和 Workshop API 等宿主提供的依赖使用 compileOnly,UI 插件应与宿主的 Kotlin / Compose compiler 版本保持一致。

异常检查方向
PluginPermissionDeniedException是否声明并获得 CLASS_TRANSFORM 权限,是否从插件中注册
IllegalArgumentException目标是否存在、是否唯一,参数类型、参数名称或作用域是否匹配
IllegalStateExceptionHook 注册是否成功,结合错误信息检查目标和宿主版本
最近更新