动态加载卸载Delphi DLL的GDI泄漏排查及句柄计数差异解析
用精准工具追踪GDI对象细节
放弃任务管理器的粗略计数,使用GDIView这类工具捕获完整的GDI对象列表,包括对象类型、创建线程、所属模块。每次加载卸载DLL后对比快照,找出新增的未被常规句柄表捕获的对象——这类对象通常是DevExpress组件初始化时创建的全局资源(如默认皮肤画刷、字体),DLL卸载时未被释放。检查dxForms单元的初始化与清理逻辑
dxForms单元在初始化阶段可能自动创建全局GDI资源,但finalization块可能未妥善释放。可以:- 在DLL卸载前,手动调用dxForms提供的清理函数(如
dxForms.Finalize类方法); - 若有源码权限,直接查看
initialization和finalization块,排查是否存在GDI对象创建后未配对调用DeleteObject的情况。
- 在DLL卸载前,手动调用dxForms提供的清理函数(如
Hook GDI API追踪资源生命周期
在进程中HookCreatePen、CreateBrushIndirect、CreateFontIndirect、DeleteObject等GDI核心API,记录每个对象的创建栈、线程ID、所属模块。加载卸载DLL后,对比创建与释放日志,定位未被销毁的GDI对象。可借助Delphi的MadHook或Windows Detours库实现Hook逻辑。排查DLL全局对象的资源持有问题
Delphi DLL的全局对象(如类实例、全局变量)会在加载时初始化、卸载时销毁。若这些对象持有GDI资源但未在销毁时释放,会导致泄漏:- 检查DLL中是否存在全局的DevExpress控件实例(即使未创建窗体,部分全局对象可能隐含创建GDI资源);
- 在
DllMain的DLL_PROCESS_DETACH阶段,手动清理所有可能持有GDI资源的全局对象。
明确句柄计数差异的根源
任务管理器的GDI计数包含系统窗口类默认资源、DevExpress皮肤缓存等“隐藏”资源,这些可能未出现在常规句柄表中。调用GetGuiResourcesAPI获取进程真实GDI句柄数,结合工具快照定位这类全局管理的资源,需调用组件库专用清理接口释放。最小化场景验证问题根源
创建仅引用dxForms单元的空DLL,测试加载卸载后的GDI变化:- 若仍泄漏,说明问题源于dxForms单元自身的初始化/清理逻辑;
- 若不泄漏,逐步添加原DLL代码,定位到具体导致泄漏的代码块。
内容的提问来源于stack exchange,提问作者AWirthK2

