首页/文章列表/文章详情

.NET外挂系列:3. 了解 harmony 中灵活的纯手工注入方式

编程知识892025-05-20评论

一:背景

1. 讲故事

上一篇我们讲到了注解特性,harmony 在内部提供了 20个 HarmonyPatch 重载方法尽可能的让大家满足业务开发,那时候我也说了,特性虽然简单粗暴,但只能解决 95% 的问题,言外之意还有一些事情做不到,所以剩下的 5%只能靠完全手工的方式了。

二:注解特性的局限性

虽然有20个重载方法,但还不能达到100%覆盖,不要以为我说的这种情况比较罕见,是很正常的场景,比如说:

  1. 嵌套类。
  2. 程序集中的某些特殊不对外公开类。

这里我就拿第二种来说把,参考代码如下:

internal sealed class ServiceProviderEngineScope : IServiceScope, IDisposable, IServiceProvider, IKeyedServiceProvider, IAsyncDisposable, IServiceScopeFactory{ public ServiceProviderEngineScope(ServiceProvider provider, bool isRootScope) { ResolvedServices = new Dictionary<ServiceCacheKey, object>(); RootProvider = provider; IsRootScope = isRootScope; }}

这段代码有几个要素:

1. internal

代码是程序集可访问,所以你不能使用任何typeof(xxx)形式的构造函数,否则就会报错,参考如下:

2. 有参构造函数

由于不能使用typeof(xxx),所以只能通过字符串模式反射type,当你有心查找你会发现第20个重载方法虽然支持string格式,但不提供Type[] argumentTypes参数信息,代码如下:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = true)]public class HarmonyPatch : HarmonyAttribute{ ... public HarmonyPatch(string typeName, string methodName, MethodType methodType = MethodType.Normal); ...}

所以这个就是很无语的事情了,哈哈,上面所说的其实就是我最近遇到了一例.NET托管内存暴涨 问题,观察托管堆之后,发现有 975w 的 ServiceProviderEngineScope 类,截图如下:

熟悉这个类的朋友应该明白,这是上层调用serviceProvider.CreateScope()方法没有释放导致的,那接下来的问题是到底谁在不断的调用CreateScope() 呢? 直接监控 ServiceProviderEngineScope的构造函数就可以了。

三:解决方案

1. 使用 TargetMethod 口子函数

上一篇跟大家聊过 harmony 的口子函数 TargetMethods,它可以批量返回需要被 patch 的方法,如果你明确知道只需返回一个,可以用 TargetMethod口子来实现,有了这些思路之后,完整的实现代码如下:

 internal class Program { static void Main(string[] args) { var harmony = new Harmony("com.dotnetdebug.www"); harmony.PatchAll(); // 1. 创建服务集合 var services = new ServiceCollection(); // 2. 注册一个作用域服务 services.AddScoped<MyService>(); // 3. 构建服务提供者 var serviceProvider = services.BuildServiceProvider(); // 4. 创建作用域 var scope = serviceProvider.CreateScope(); var myService = scope.ServiceProvider.GetRequiredService<MyService>(); myService.DoSomething(); Console.ReadLine(); } } class MyService : IDisposable { public MyService() { Console.WriteLine("i'm MyService..."); } public void DoSomething() { Console.WriteLine($"{DateTime.Now} Doing work..."); } public void Dispose() { Console.WriteLine($"{DateTime.Now} Disposing MyService"); } } [HarmonyPatch] public class HookServiceProviderEngineScope { [HarmonyTargetMethod] static MethodBase TargetMethod() { var engineScopeType = Type.GetType("Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope, Microsoft.Extensions.DependencyInjection"); var constructor = engineScopeType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)[0]; return constructor; } public static void Prefix(bool isRootScope) { Console.WriteLine("----------------------------"); Console.WriteLine($"isRootScope:{isRootScope}"); Console.WriteLine(Environment.StackTrace); Console.WriteLine("----------------------------"); } }

有些朋友可能要说了,这地方为什么会有两个调用栈,熟悉底层的朋友应该知道分别由services.BuildServiceProviderserviceProvider.CreateScope贡献的。

写到这里的时候,出门抽了个烟,突然灵光一现,既然20个单重载方法不够用,我完全可以使用 HarmonyPatch 注解特性组合呀。。。相当于平级补充,说干就干,参考代码如下:

 [HarmonyPatch("Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope, Microsoft.Extensions.DependencyInjection", null, MethodType.Constructor)] [HarmonyPatch(new Type[2] { typeof(ServiceProvider), typeof(bool) })] public class HookServiceProviderEngineScope { public static void Prefix(bool isRootScope) { Console.WriteLine("----------------------------"); Console.WriteLine($"isRootScope:{isRootScope}"); Console.WriteLine(Environment.StackTrace); Console.WriteLine("----------------------------"); } }

有了胜利喜悦之后,我想可有神鬼不测之术来解决嵌套类的问题,纠结了之后用HarmonyPatch特性理论上搞不定。

2. 完全动态hook

整体上来说前面的TargetMethod模式属于混合编程(特性+手工),如果让代码更纯粹一点话,就要把所有的 Attribute 摘掉,这就需要包装器类 HarmonyMethod,修改后的代码如下:

 internal class Program { static void Main(string[] args) { var harmony = new Harmony("com.dotnetdebug.www"); var engineScopeType = Type.GetType("Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope, Microsoft.Extensions.DependencyInjection"); var originalMethod = engineScopeType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)[0]; var prefixMethod = typeof(HookServiceProviderEngineScope).GetMethod("Prefix"); harmony.Patch(originalMethod, new HarmonyMethod(prefixMethod)); // 1. 创建服务集合 var services = new ServiceCollection(); // 2. 注册一个作用域服务 services.AddScoped<MyService>(); // 3. 构建服务提供者 var serviceProvider = services.BuildServiceProvider(); // 4. 创建作用域 var scope = serviceProvider.CreateScope(); var myService = scope.ServiceProvider.GetRequiredService<MyService>(); myService.DoSomething(); Console.ReadLine(); } } class MyService : IDisposable { public MyService() { Console.WriteLine("i'm MyService..."); } public void DoSomething() { Console.WriteLine($"{DateTime.Now} Doing work..."); } public void Dispose() { Console.WriteLine($"{DateTime.Now} Disposing MyService"); } } public class HookServiceProviderEngineScope { public static void Prefix(bool isRootScope) { Console.WriteLine("----------------------------"); Console.WriteLine($"isRootScope:{isRootScope}"); Console.WriteLine(Environment.StackTrace); Console.WriteLine("----------------------------"); } }

这里稍微提一下HarmonyMethod类,它的内部有很多的参数可以配置,比如优先级日志功能,这些都是Attribute所做不了的,参考如下:

public class HarmonyMethod{ public MethodInfo method; public string category; public Type declaringType; public string methodName; public MethodType? methodType; public Type[] argumentTypes; public int priority = -1; public string[] before; public string[] after; public HarmonyReversePatchType? reversePatchType; public bool? debug; public bool nonVirtualDelegate;}

四:总结

特性搞不定的时候,手工HarmonyMethod编程是一个很好的补充,这几篇我们只关注了Prefix,毕竟从高级调试的角度看,我们更关注问题代码的调用栈,从而寻找引发故障的元凶。
图片名称

神弓

一线码农

这个人很懒...

用户评论 (0)

发表评论

captcha