- 开发工具
【免费下载链接】Harmony
A library for patching, replacing and decorating .NET and Mono methods during runtime
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.Last | 0 | 最后执行 |
Priority.VeryLow | 100 | 非常低 |
Priority.Low | 200 | 低 |
Priority.LowerThanNormal | 300 | 低于普通 |
Priority.Normal | 400 | 普通(默认值) |
Priority.HigherThanNormal | 500 | 高于普通 |
Priority.High | 600 | 高 |
Priority.VeryHigh | 700 | 非常高 |
Priority.First | 800 | 最先执行 |
关键事实: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 中实现了完整的排序流程,可归纳为三步:
- 建图:为每个补丁创建
PatchSortingWrapper,遍历before/after中的 Harmony ID,与补丁列表中的owner双向匹配,建立依赖关系(AddBeforeDependency/AddAfterDependency,见 PatchSorter.cs)。 - 初始排序:所有补丁按
PriorityComparer(优先级降序、index 升序)预排序,此顺序在后续处理中尽量保持。 - 拓扑输出:从队列中不断取出"所有 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 的补丁执行 | 反向注册依赖,同理 | 满足依赖后保持优先级序 |
实践建议:
- 多插件协同、API 稳定的库/框架:优先使用
HarmonyBefore/HarmonyAfter声明依赖,即使对方插件未来调整了优先级数值,你的相对顺序也依然成立。 - 同插件内部多个补丁:直接用
HarmonyPriority控制即可,不必引入跨 ID 依赖。 - Postfix 想"最后说话":标注
[HarmonyPriority(Priority.First)]或[HarmonyAfter([...])];Prefix 想"最先拦截"同理。 - 避免无意义的循环依赖:虽然排序器会自动打破循环,但打破规则是从低优先级下手,可能产生与你预期相反的最终顺序。
- 类级与方法级混用时:确认合并结果是否符合预期,必要时显式标注方法级优先级。
相关资源
- 文档原文: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
相关推荐
从零开始部署Hermes Agent:企业级多环境实战指南
从零开始部署Hermes Agent:企业级多环境实战指南 Hermes Agent是一款功能强大的AI代理平台,支持从云服务器到边缘设备的全方位跨平台部署方案
AI Agent人工智能AI 应用工具调用Agent 记忆交互助手RAG任务调度MCP 服务为什么选择DeepSeek-V4-Flash-NVFP4?AMD平台AI大模型部署的5大优势
为什么选择DeepSeek V4 Flash NVFP4?AMD平台AI大模型部署的5大优势 DeepSeek V4 Flash NVFP4是基于AMD平台优化
Harmony项目中的补丁优先级机制详解
Harmony项目中的补丁优先级机制详解 什么是Harmony补丁优先级 在Harmony项目中,补丁的执行顺序不是简单的线性顺序。即使某个插件或模块最后加载,
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考