一:背景

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,毕竟从高级调试的角度看,我们更关注问题代码的 调用栈 ,从而寻找引发故障的元凶。

.NET外挂系列:3. 了解 harmony 中灵活的纯手工注入方式的更多相关文章

  1. Spring中bean的四种注入方式

    一.前言   最近在复习Spring的相关内容,这篇博客就来记录一下Spring为bean的属性注入值的四种方式.这篇博客主要讲解在xml文件中,如何为bean的属性注入值,最后也会简单提一下使用注解 ...

  2. Spring中对象和属性的注入方式

    一:Spring的bean管理 1.xml方式 bean实例化三种xml方式实现 第一种 使用类的无参数构造创建,首先类中得有无参构造器(重点) 第二种 使用静态工厂创建 (1)创建静态的方法,返回类 ...

  3. 【SSH系列】深入浅出spring IOC中三种依赖注入方式

    spring的核心思想是IOC和AOP,IOC-控制反转,是一个重要的面向对象编程的法则来消减计算机程序的耦合问题,控制反转一般分为两种类型,依赖注入和依赖查找,依赖什么?为什么需要依赖?注入什么?控 ...

  4. spring中bean配置和bean注入

    1 bean与spring容器的关系 Bean配置信息定义了Bean的实现及依赖关系,Spring容器根据各种形式的Bean配置信息在容器内部建立Bean定义注册表,然后根据注册表加载.实例化Bean ...

  5. spring IOC中四种依赖注入方式

    在spring ioc中有三种依赖注入,分别是:https://blog.csdn.net/u010800201/article/details/72674420 a.接口注入:b.setter方法注 ...

  6. 深入浅出spring IOC中三种依赖注入方式

    深入浅出spring IOC中三种依赖注入方式 spring的核心思想是IOC和AOP,IOC-控制反转,是一个重要的面向对象编程的法则来消减计算机程序的耦合问题,控制反转一般分为两种类型,依赖注入和 ...

  7. 转:深入浅出spring IOC中四种依赖注入方式

    转:https://blog.csdn.net/u010800201/article/details/72674420 深入浅出spring IOC中四种依赖注入方式 PS:前三种是我转载的,第四种是 ...

  8. 痞子衡嵌入式:了解i.MXRTxxx系列ROM中灵活的串行NOR Flash启动硬复位引脚选择

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是i.MXRTxxx系列ROM中灵活的串行NOR Flash启动硬复位引脚选择. 关于 i.MXRT 系列 BootROM 中串行 NOR ...

  9. 给Source Insight做个外挂系列之三--构建外挂软件的定制代码框架

    上一篇文章介绍了“TabSiPlus”是如何进行代码注入的,本篇将介绍如何构建一个外挂软件最重要的部分,也就是为其扩展功能的定制代码.本文前面提到过,由于windows进程管理的限制,扩展代码必须以动 ...

  10. 给Source Insight做个外挂系列之一--发现Source Insight

    一提到外挂程序,大家肯定都不陌生,QQ就有很多个版本的去广告外挂,很多游戏也有用于扩展功能或者作弊的工具,其中很多也是以外挂的形式提供的.外挂和插件的区别在于插件通常依赖于程序的支持,如果程序不支持插 ...

随机推荐

  1. 用于敏捷开发的最佳免费 UML 工具 2022

    Table of Contents  hide  1 最好的在线免费 UML图工具 2 免费的 UML Visual Paradigm 在线平台 3 其他福利 4 用于正式和大规模可视化建模的 Vis ...

  2. ‌PCI-5565PIO主要应用场景

    ‌PCI-5565PIO主要应用场景包括军事领域.工业自动化和控制系统.仿真与培训以及数据采集与分发‌.在军事领域,PCI-5565PIO可用于航空航天系统的飞行控制计算机.导航系统和传感器系统之间的 ...

  3. Spark - [04] RDD编程

    题记部分 一.RDD编程模型   在 Spark 中,RDD 被表示为对象,通过对象上的方法调用来对 RDD 进行转换.经过一系列的 transformations 定义 RDD 之后,就可以调用 a ...

  4. 初探本地DeepSeek VL + Typescript + FFMPEG:提取视频关键帧分析内容

    初探本地DeepSeek VL + Typescript + FFMPEG:提取视频关键帧分析内容 前言 随着人工智能和计算机视觉技术的发展,利用图像识别来分析视频内容已经成为现实.本文的主要目标是: ...

  5. sql server 2017 STRING_AGG() 替代方案

    SELECT @StuId='"'+STRING_AGG(Id,'","')+'"'FROM( SELECT 'a'+cast(Id as varchar) I ...

  6. 这几天用ng5做了一个俄罗斯方块

    为什么要做这个东西呢? 嗯 为了证明自己会ng吧,最主要的是这几天的工作全是静态页,实在无聊枯燥的很.当然了,肯定还有其他妙不可言的原因,这里不想说出来. 我也忘记我是什么时候有想到用ng做俄罗斯方块 ...

  7. C++判断文本编码

    #include <iostream> #include <fstream> #include <string> #include <sstream> ...

  8. Windows 10右键添加 "在此处打开命令窗口" 菜单

    1.添加右键菜单的两种效果: 第一种是在 桌面/文件夹窗口中/选中文件夹上直接点击右键,显示"在此处打开命令窗口"选项,如图: 第二种是在 桌面/文件夹窗口中/选中文件夹上按住Sh ...

  9. K8S 问题排查: cgroup 内存泄露问题

    Posted on 2019年12月6日Leave a comment Contents [hide] 1 前言 2 现象 3 原因 4 解决方案 4.1 方案一 4.2 方案二 4.3 方案三 5 ...

  10. 一文速通Python并行计算:03 Python多线程编程-多线程同步(上)—基于互斥锁、递归锁和信号量

    一文速通 Python 并行计算:03 Python 多线程编程-多线程同步(上)-基于互斥锁.递归锁和信号量 摘要: 在 Python 多线程编程中,线程同步是确保多个线程安全访问共享资源的关键技术 ...