news 2026/11/5 7:58:07

Harmony 补丁优先级与执行顺序控制:HarmonyPriority、HarmonyBefore 与 HarmonyAfter 实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Harmony 补丁优先级与执行顺序控制:HarmonyPriority、HarmonyBefore 与 HarmonyAfter 实战指南
  • 开发工具

【免费下载链接】Harmony

A library for patching, replacing and decorating .NET and Mono methods during runtime

项目地址:https://gitcode.com/gh_mirrors/ha/Harmony
点击查看免费下载

Harmony(HarmonyLib)在运行时以 IL 重写的方式为 .NET/Mono 方法注入 Prefix、Postfix、Transpiler 与 Finalizer 补丁。当多个插件(Mod)同时补丁同一方法时,它们的执行顺序并不是简单的"先注册先执行",而是由一套优先级 + 依赖关系的排序机制决定。本文基于 Harmony/Documentation/articles/priorities.md 展开,结合仓库中的源码实现(Priority.cs、Attributes.cs、PatchSorter.cs)与排序测试(PatchSorting.cs),系统讲解三种排序注解的语义、优先级常量体系、底层排序算法,以及如何用它解决"谁最后修改返回值"这一经典冲突。

核心思想:补丁顺序不是线性的

在 Harmony 中,补丁的执行顺序并不由加载顺序决定。一个最后加载的插件/Mod,其补丁依然可以排在最先执行的位置。为了实现这一点,补丁需要通过方法注解(annotation)显式声明自己的排序意图。Harmony 提供了三个排序注解:

  • [HarmonyPriority(int)]:设置该 Prefix/Postfix 的优先级。默认值为Priority.Normal(即 400)。
  • [HarmonyBefore(string[])]:声明该补丁应早于参数中列出的所有 Harmony ID 所注册的补丁执行。
  • [HarmonyAfter(string[])]:声明该补丁应晚于参数中列出的所有 Harmony ID 所注册的补丁执行。

这三个注解在源码中的定义位于 Harmony/Public/Attributes.cs,其AttributeUsage均为AttributeTargets.Class | AttributeTargets.Method,即既可以标注在补丁方法上,也可以标注在补丁类上(见下文"类级注解"一节)。它们都继承自HarmonyAttribute,最终把值写入HarmonyMethod对应的priority、before、after字段(见 Harmony/Public/HarmonyMethod.cs)。

优先级常量体系:从 Last 到 First

[HarmonyPriority(int)]接受任意整数值,数值越大越先执行。仓库在 Harmony/Public/Priority.cs 中预定义了一组语义化常量,方便开发者直接引用:

常量值语义
Priority.Last0最后执行
Priority.VeryLow100非常低
Priority.Low200低
Priority.LowerThanNormal300低于普通
Priority.Normal400普通(默认值)
Priority.HigherThanNormal500高于普通
Priority.High600高
Priority.VeryHigh700非常高
Priority.First800最先执行

关键事实:Priority.Normal的默认值是 400,这是注解默认优先级。在 Patch.cs 的构造函数中可以看到,当priority == -1(即未显式声明,HarmonyMethod.priority的默认值)时会被归一为Priority.Normal。也就是说:只要没有显式标注优先级,所有补丁都处于同一档 400。

同一优先级下的排序:后注册者胜出

当多个补丁优先级相同时,Harmony 如何决定顺序?答案藏在排序比较器 PatchJsonConverter.cs 中的PriorityComparer实现里:

if (priority != theirPriority) return -(priority.CompareTo(theirPriority)); // 优先级高的排前面 return index.CompareTo(theirIndex); // 同优先级按注册序号排

也就是说,排序先比较priority(降序),优先级相同则比较index(升序,即注册顺序)。这带来一个重要的实战推论:同一优先级下,后注册的 Postfix 排在前面、后执行,因此它可以覆盖先注册 Postfix 对返回值的修改——这正是原文档示例所演示的场景。

示例:两个插件争夺返回值

给定如下目标方法(完整示例见 Harmony/Documentation/examples/priorities.cs):

class Foo { static string Bar() => "secret"; }

插件 1(examples/priorities.cs):

void Main_Plugin1() { var harmony = new Harmony("net.example.plugin1"); harmony.PatchAll(Assembly.GetExecutingAssembly()); } [HarmonyPatch(typeof(Foo))] [HarmonyPatch("Bar")] class MyPatch { static void Postfix(ref string result) => result = "new secret 1"; }

插件 2(examples/priorities.cs):

void Main_Plugin2() { var harmony = new Harmony("net.example.plugin2"); harmony.PatchAll(Assembly.GetExecutingAssembly()); } [HarmonyPatch(typeof(Foo))] [HarmonyPatch("Bar")] class MyPatch { static void Postfix(ref string result) => result = "new secret 2"; }

两个插件都未声明优先级(默认同为Priority.Normal),且插件 2 后注册。按照"同优先级按 index 升序执行"的规则,插件 1 的 Postfix 先执行,插件 2 的 Postfix 后执行,后执行的会覆盖ref string result的修改。因此调用Foo.Bar()返回"new secret 2"——后注册的 Postfix 覆盖了先注册者改写的结果。

用 HarmonyAfter 挽回执行顺序

作为插件 1 的作者,若你希望自己的 Postfix 在插件 2 之后执行(从而"最后修改返回值"),只需重写为(examples/priorities.cs):

void Main_Plugin1b() { var harmony = new Harmony("net.example.plugin1"); harmony.PatchAll(Assembly.GetExecutingAssembly()); } [HarmonyPatch(typeof(Foo))] [HarmonyPatch("Bar")] class MyPatch { [HarmonyAfter(["net.example.plugin2"])] static void Postfix(ref string result) => result = "new secret 1"; }

[HarmonyAfter(["net.example.plugin2"])]声明该补丁应晚于Harmony ID 为net.example.plugin2的所有补丁执行。由于插件 2 的 Postfix 先执行、插件 1 的 Postfix 后执行,现在Foo.Bar()会返回"new secret 1",插件 1 重新夺回了"最后修改返回值"的权利。

注意:HarmonyBefore/HarmonyAfter参数中的 ID 是创建Harmony实例时传入的标识字符串(即new Harmony("net.example.plugin1")中的"net.example.plugin1"),而不是类名或方法名。在 PatchSorter.cs 中可以看到,依赖关系正是通过node.innerPatch.before/after.Contains(x.innerPatch.owner)来匹配的,owner就是该 Harmony ID。

用 HarmonyPriority 达到同样的效果

原文档还给出了另一个等价方案:插件 1 也可以改用[HarmonyPriority(Priority.Low)]标注自己的 Postfix,使其优先级低于默认的Priority.Normal(400),从而排在插件 2 之后执行。两种方式都可行,选择依据是:

  • 跨插件协作、语义清晰:优先使用HarmonyBefore/HarmonyAfter,直接表达"我要在谁之前/之后";
  • 只关心相对先后、不关心具体插件:使用HarmonyPriority数值比较即可。

类级注解:一次声明,整类生效

三个优先级注解不仅可用于补丁方法,同样可标注在补丁类上,一次性为类内所有补丁方法定义默认优先级。例如:

[HarmonyPriority(Priority.High)] [HarmonyPatch(typeof(Foo))] [HarmonyPatch("Bar")] class MyPatch { static void Prefix() { /* 继承 High 优先级 */ } static void Postfix(ref string result) => result = "new secret"; }

类上的注解与该类内方法上的注解遵循合并规则:方法级注解会覆盖/叠加类级注解。合并逻辑见 HarmonyMethod.cs 的HarmonyMethod.Merge——注意源码注释中明确提到一个细节:priority字段默认值为-1,合并时会跳过值为-1的项,因此类级HarmonyPriority会被方法级显式标注正确覆盖,不会被"未标注"的方法级默认值冲掉。同理,类级HarmonyBefore/HarmonyAfter与方法级声明也会合并为最终的before/after数组。

底层实现:PatchSorter 的依赖图排序

理解注解只是第一步,底层排序算法更能帮助你预判复杂场景下的行为。Harmony 在 Harmony/Internal/PatchSorter.cs 中实现了完整的排序流程,可归纳为三步:

  1. 建图:为每个补丁创建PatchSortingWrapper,遍历before/after中的 Harmony ID,与补丁列表中的owner双向匹配,建立依赖关系(AddBeforeDependency/AddAfterDependency,见 PatchSorter.cs)。
  2. 初始排序:所有补丁按PriorityComparer(优先级降序、index 升序)预排序,此顺序在后续处理中尽量保持。
  3. 拓扑输出:从队列中不断取出"所有 after 依赖都已处理"的补丁输出;若补丁因依赖未满足而无法输出,则进入 waiting list,待依赖解除后再处理。after依赖即"必须等某人先执行",before依赖会被反向注册为对方(目标 owner 的补丁)的after依赖,从而自动满足"我必须在某人之前"。

循环依赖的兜底处理

如果多个补丁相互声明依赖形成循环(例如 A after B、B after C、C after A),排序器无法完全满足所有约束,此时会调用CullDependency()(PatchSorter.cs)从 waiting list 中按优先级从低到高寻找第一个未解决的依赖并将其移除(即"打破"这条依赖边),从而保证排序总能收敛、不会死循环。开启调试时(debug参数为 true),会通过FileLog.LogBuffered记录"Breaking dependance between ..."日志。

这一系列行为都被 HarmonyTests/Patching/PatchSorting.cs 中的测试覆盖验证,例如:

  • Test_PatchOrder_AllPriorities(PatchSorting.cs)验证九个优先级常量从First到Last严格降序执行;
  • Test_PatchOrder_BeforeAndAfterAndPriorities(PatchSorting.cs)验证before、after与优先级混合时的期望顺序;
  • Test_PatchOrder_TransitiveBefore/Test_PatchOrder_TransitiveAfter(PatchSorting.cs)验证依赖具有传递性:A before B、B before C 时,A 会排在 C 之前;
  • Test_PatchCycle0~Test_PatchCycle3(PatchSorting.cs)覆盖了单循环、双循环、交叉循环等循环依赖场景的打破结果。

结果缓存与补丁变更检测

排序结果会缓存在sortedPatchArray中(PatchSorter.cs),下次对同一方法排序时直接复用。若补丁列表发生变化(增删补丁、修改 priority/before/after/owner/index),ComparePatchLists(PatchSorter.cs)会通过PatchDetailedComparer逐字段比对来使缓存失效,Test_PatchSorterCache0(PatchSorting.cs)专门验证了这一点。

排序规则速查与实践建议

综合文档与源码,可以将 Harmony 的补丁排序规则总结为一张速查表:

声明方式影响优先级比较同优先级时
不声明默认Priority.Normal(400)与所有未声明者同档按注册顺序(后注册者后执行)
[HarmonyPriority(N)]数值越大越先执行先按优先级降序按注册顺序
[HarmonyAfter(["id"])]晚于指定 ID 的补丁执行依赖约束优先于数值优先级满足依赖后保持优先级序
[HarmonyBefore(["id"])]早于指定 ID 的补丁执行反向注册依赖,同理满足依赖后保持优先级序

实践建议:

  1. 多插件协同、API 稳定的库/框架:优先使用HarmonyBefore/HarmonyAfter声明依赖,即使对方插件未来调整了优先级数值,你的相对顺序也依然成立。
  2. 同插件内部多个补丁:直接用HarmonyPriority控制即可,不必引入跨 ID 依赖。
  3. Postfix 想"最后说话":标注[HarmonyPriority(Priority.First)]或[HarmonyAfter([...])];Prefix 想"最先拦截"同理。
  4. 避免无意义的循环依赖:虽然排序器会自动打破循环,但打破规则是从低优先级下手,可能产生与你预期相反的最终顺序。
  5. 类级与方法级混用时:确认合并结果是否符合预期,必要时显式标注方法级优先级。

相关资源

  • 文档原文:Harmony/Documentation/articles/priorities.md
  • 完整示例代码:Harmony/Documentation/examples/priorities.cs
  • 优先级常量定义:Harmony/Public/Priority.cs
  • 排序注解定义:Harmony/Public/Attributes.cs
  • 排序算法实现:Harmony/Internal/PatchSorter.cs
  • 排序行为测试:HarmonyTests/Patching/PatchSorting.cs
  • 开发工具

【免费下载链接】Harmony

A library for patching, replacing and decorating .NET and Mono methods during runtime

项目地址:https://gitcode.com/gh_mirrors/ha/Harmony
点击查看免费下载

相关推荐

上一篇:终极 tus-js-client 完整指南:如何实现可靠的文件断点续传功能
下一篇:5个理由告诉你为什么tssh是比传统ssh更智能的选择

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 20:40:03

IT监控自动化落地指南:从重复告警到自愈闭环

监控做到一定阶段,大家会发现一个很尴尬的事:监控系统本身变成了最大的日常维护负担。机器一多、告警一多,运维团队每天不是在处理告警,就是在处理告警的路上。于是很多人开始提“IT 监控自动化”,想靠自动化把监控这个…

作者头像 李华
网站建设 2026/11/5 7:57:59

marketingskills实战:用Claude Code搭建自动化营销工作流

1. 从“marketingskills”说起:一个被低估的AI营销技能库第一次看到“marketingskills”这个词,是在一个做独立站的朋友群里。有人丢了个链接,说“这玩意儿把SEO和CRO的活儿全拆成Claude Code能直接跑的技能包了”。我当时的第一反应是&#…

作者头像 李华