深入理解Windows GDI对象及其在.NET程序中的应用

在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就不那么确定了。

GDI对象的管理和优化

在.NET程序中,GDI对象的管理是一个需要特别注意的问题。由于.NET框架自动处理GDI对象的创建和销毁,开发者往往忽视了这些底层细节。然而,当应用程序的规模增长,或者用户界面变得复杂时,GDI对象的数量可能会迅速增加,导致性能问题甚至崩溃。

为了避免这种情况,开发者需要采取一些措施来优化GDI对象的使用:

  • 避免不必要的GDI对象创建:在设计用户界面时,尽量减少GDI对象的使用。例如,可以通过复用控件和图像来减少GDI对象的数量。
  • 及时释放GDI对象:当GDI对象不再需要时,应该及时释放它们。这可以通过调用对象的Dispose方法来实现。
  • 监控GDI对象的使用:使用工具如Windows任务管理器来监控应用程序的GDI对象使用情况。这有助于识别潜在的性能瓶颈。
  • 优化垃圾收集:.NET的垃圾收集器(GC)负责管理内存,但它可能无法及时释放GDI对象。可以通过调整GC的行为来优化性能,例如,通过调用GC.Collect()来强制进行垃圾收集。
  • 使用第三方库:有些第三方库提供了更高效的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); } }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485