在Windows操作系统中,GDI(图形设备接口)对象负责处理显示和打印数据的通信。最近,软件收到了一些错误报告,这些报告包含了一个异常,提示宿主系统(Windows未知版本)耗尽了可用的GDI句柄。这让回想起了在Windows 3.x编程的童年时光。开始重新研究这个问题,想要弄清楚GDI对象是什么,以及它们对于像这样的普通桌面Windows C#.NET程序员意味着什么。
基本上,对于需要在用户界面(屏幕或打印机)上绘制的每个对象,Windows都会分配一个GDI对象,这是一个原生的Windows对象,引用/代表设备上的该对象。可以将这视为常规表示(某种业务逻辑的抽象对象)与实际设备和像素表示之间的一个钩子。Windows这样做是为了在不同设备之间提供某种抽象,让所有开发者都能以相同的方式使用所有设备。到目前为止,一切都很好。
当使用C或C++对原生Windows进行编程时,程序员负责创建和释放GDI对象。而在.NET中编程时,GDI对象的创建和销毁通常由底层类和实现“幕后”处理。典型的.NET程序员甚至不知道GDI及其相关对象的存在,因此也不关心围绕GDI的限制和可能的错误。然后现实就打击了。
在程序中发生了这样的情况。在所有Windows版本中,似乎都有一个软硬限制,将系统范围内分配的GDI对象数量限制在65,536个(Windows老版本是16位)。有一个(顺便说一句,可以编辑的)系统注册表值设置,每个程序可以分配的最大GDI对象数量为10,000。结果发现,程序达到了这个限制。程序中有一些视图,其中包含一些控件,这些视图显示在标签上(一个GDI对象用于主窗口,一个用于标签控制器,一个用于每个标签,一个用于每个视图,一个用于视图中的每个控件,一个用于每个标签...懂的!)。这种方法相当标准,到目前为止,程序并没有比任何其他程序使用更多的GDI对象。问题是某个视图由几个大型复合UI元素构建。每次程序重新计算视图中的值时——每个组件都被处理和丢弃——至少这样认为的。结果发现,.NET垃圾收集器在处理时存在一些限制。特别是循环引用似乎是个问题。这导致已处理的视图没有被垃圾收集——这意味着它们保留了GDI对象引用。这反过来又导致GDI对象堆积——使程序运行得有点慢——最终达到了程序实例的神奇限制10,000——导致程序崩溃!
这(当然)主要是自己的错——这些开发者应该有知识和智慧来防止这种情况的发生。但不禁要部分责怪.NET隐藏了GDI对象的存在,以及在它们被使用过的视图被处理后,甚至没有释放它们。不禁想象.NET程序的一个更简单的渲染模型,其中UI可能只持有一个GDI对象的引用(在幕后),实际的渲染由一个托管引擎完成。那应该会有效。最后,觉得用户可以随时更改软硬编码值10,000是疯狂的——如果用户将其降低到一个更低的数字——1,000或500——这将导致许多程序出现故障。
最后,开始四处寻找其他流行程序生成了多少GDI对象,这相当有趣。Windows任务管理器实际上会显示每个进程的GDI对象数量。当Google Chrome从干净的启动中启动,除了默认窗口外什么都没有时,它有三个进程,总共有13 + 42 + 34 = 89个GDI对象。加载一个网站(一个丹麦小报新闻网站)使Chrome使用了四个进程和13 + 175 + 44 + 30 = 262个GDI对象。打开第二个(丹麦新闻)网站,总共有五个进程和13 + 175 + 56 + 53 + 212 = 509个GDI对象——有趣的事实:滚动到新闻网站的底部,使212的数字上升到468——使总数达到765个GDI对象!到目前为止,只有两个标签页打开了两个网页——这与通常的浏览习惯相比真的不算什么!相比之下,程序(显示六个标签页的用户数据)使用了大约170个GDI对象。用户在这些视图中创建更多的能力将对GDI对象的数量产生很大影响,但相当确定,如果没有GDI泄漏,程序永远不会达到10,000的限制。对Google Chrome就不那么确定了。
在.NET程序中,GDI对象的管理是一个需要特别注意的问题。由于.NET框架自动处理GDI对象的创建和销毁,开发者往往忽视了这些底层细节。然而,当应用程序的规模增长,或者用户界面变得复杂时,GDI对象的数量可能会迅速增加,导致性能问题甚至崩溃。
为了避免这种情况,开发者需要采取一些措施来优化GDI对象的使用:
以下是一个简单的代码示例,展示了如何在.NET程序中优化GDI对象的使用。
public class GdiObjectManager : IDisposable
{
private Bitmap _bitmap;
private Graphics _graphics;
public GdiObjectManager(string filePath)
{
_bitmap = new Bitmap(filePath);
_graphics = Graphics.FromImage(_bitmap);
}
public void DrawSomething()
{
// 使用_graphics对象进行绘制
}
public void Dispose()
{
if (_graphics != null)
{
_graphics.Dispose();
_graphics = null;
}
if (_bitmap != null)
{
_bitmap.Dispose();
_bitmap = null;
}
GC.SuppressFinalize(this);
}
}