性能优化一般都是从性能瓶颈开始。项目中有这样一个控件,它包含很多个Item,每个Item字体可能相同,也可能不同。且该控件经常在同一个Form上大量使用。正是这个控件在使用GDI画每个Item的文字时,出现了性能瓶颈...
性能优化一般都是从性能瓶颈开始。项目中有这样一个控件,它包含很多个Item,每个Item字体可能相同,也可能不同。且该控件经常在同一个Form上大量使用。正是这个控件在使用GDI画每个Item的文字时,出现了性能瓶颈。
IntPtr handle = font.ToHfont(); //性能瓶颈
//…
SafeNativeMethods.DeleteObject(handle);
由于该控件在使用GDI画字时,通过调用Font.ToHfont()方法获得Font的Handle。而这个方法非常慢。并且控件在画每个Item时都被调用这个方法,Form中又有很多个这样的控件,因此调用次数相当可观。这就造成了这个性能瓶颈。
由于操作系统是不允许GDI 的Handle个数大于9999的。如果大于9999个的话,程序就会崩掉。因此,我们绝对不能使程序中GDI的Handle个数与某些因素有线性增长关系。所有,一般都是在使用GDI画字时创建Handle,用完之后就删除掉。这样也可以防止GDI泄露。
考虑到很多时候,Font都是相同的,如果能将Font创建的Handle缓存起来,性能就会有很大的提升。但是,缓存的Handle不及时删除的话,如果Font不相同的太多,就有机会达到操作系统允许的最大个数,从而使程序崩溃。
以下是我的解决方案:
1,使用SafeFontHandle类来防止GDI泄露。SafeFontHandle派生自SafeHandleZeroOrMinusOneIsInvalid,而SafeHandleZeroOrMinusOneIsInvalid又派生自CriticalFinalizerObject。GC会对CriticalFinalizerObject做特别处理,保证所有关键终止代码都有机会执行。

Code
#region The SafeFontHandle class
internal sealed class SafeFontHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private SafeFontHandle()
: base(true)
{
}
public SafeFontHandle(IntPtr preexistingHandle, bool ownsHandle)
: base(ownsHandle)
{
base.SetHandle(preexistingHandle);
}
protected override bool ReleaseHandle()
{
return SafeNativeMethods.DeleteNativeFontHandle(base.handle);
}
}
#endregion
2,使用HandleCollector类防止Font的Handle超过操作系统最大限制。HandleCollector会跟踪Font的Handle,并在其达到指定阀值时强制执行垃圾回收。垃圾回收后,SafeFontHandle会释放Font的handle。

Code
[SuppressUnmanagedCodeSecurity]
internal static class SafeNativeMethods
{
private static HandleCollector FontHandleCollector = new HandleCollector("GdiFontHandle", 500, 1000);
internal static IntPtr CreateNativeFontHandle(Font font)
{
IntPtr handle = font.ToHfont();
if (handle != IntPtr.Zero)
{
FontHandleCollector.Add();
}
return handle;
}
internal static bool DeleteNativeFontHandle(IntPtr handle)
{
bool success = DeleteObject(handle);
if (success)
{
FontHandleCollector.Remove();
}
return success;
}
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
internal static extern bool DeleteObject(System.IntPtr gdiObject);
}
3,使用弱引用缓存类WeakReferenceCachePool<TKey, TItem>来缓存SafeFontHandle,这样可以不影响SafeFontHandle被GC正常垃圾回收,从而释放Font的Handle。关于弱引用缓存类WeakReferenceCachePool<TKey, TItem>,可以参考《一个弱引用缓存类》这篇文章。

Code
internal static class SafeFontHandleFactory
{
#region Instance Data
private static WeakReferenceCachePool<Font, SafeFontHandle> _cachePool = new WeakReferenceCachePool<Font, SafeFontHandle>();
#endregion
#region Methods
public static SafeFontHandle CreateSafeFontHandle(Font font)
{
if (font == null)
{
throw new ArgumentNullException();
}
SafeFontHandle safeFontHandle = _cachePool[font];
if (safeFontHandle == null)
{
IntPtr nativeHandle = SafeNativeMethods.CreateNativeFontHandle(font);
safeFontHandle = new SafeFontHandle(nativeHandle, true);
_cachePool[font] = safeFontHandle;
}
return safeFontHandle;
}
#endregion
}
这样就成功的缓存了GDI的Handle,而且在使用完成后,GDI的Handle不会线性增长,只要有GC回收发生,GDI的Handle都会清零,或者总个数达到HandleCollector指定的阀值时,也会清零。利用GC垃圾回收机制,在性能和内存占用之间自动平衡。
这里是测试代码,性能测试如下:
不使用弱引用缓存
Time Elapsed: 350ms
CPU Cycles: 952,061,115
Gen 0: 1
Gen 1: 0
Gen 2: 0
GDI increment: 0
使用弱引用缓存
Time Elapsed: 42ms
CPU Cycles: 142,020,499
Gen 0: 0
Gen 1: 0
Gen 2: 0
GDI increment: 0
欢迎拍砖。。。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?