张洋 3 weeks ago
parent
commit
be8eaca926

+ 96 - 5
DuiLib/Core/UIManager.cpp

@@ -588,6 +588,84 @@ namespace DuiLib
 	CStdPtrArray CPaintManagerUI::m_aPreMessages;
 	CStdPtrArray CPaintManagerUI::m_aPreMessages;
 	CStdPtrArray CPaintManagerUI::m_aPlugins;
 	CStdPtrArray CPaintManagerUI::m_aPlugins;
 
 
+	// =====================================================================
+	// 【第 47 轮】GDI+ 运行时:从「每窗口一份」提升为「进程级一份」
+	// ---------------------------------------------------------------------
+	// 原实现把 GdiplusStartup/GdiplusShutdown 当成 CPaintManagerUI 的构造/析构副作用,
+	// 而 CPaintManagerUI 在本项目是【每窗口一份】(CToastWnd::m_pm / CMainWnd::m_pm
+	// 都是业务窗口类的内联成员) ⇒ 每开/关一个弹框、每弹一次 Toast(1.5 秒自关)
+	// 都会来一次 GDI+ 启动 + 一次关闭。
+	//
+	// 为什么这是错的(三条,都有官方依据):
+	//  1) MSDN《GdiplusShutdown》Remarks 原文:
+	//     "you must delete all of your GDI+ objects (or have them go out of scope)
+	//      before you call GdiplusShutdown."
+	//     ⇒ 调 Shutdown 之前必须销毁【所有】GDI+ 对象。但 GDI+ 对象并不都受
+	//        单个 manager 约束:
+	//          · 库内 static TResInfo m_SharedResInfo 的 m_ImageHash 里存着
+	//            TImageInfo::pImage(Gdiplus::Image*),生命周期 = 整个进程;
+	//            而 ~CPaintManagerUI 调的是 RemoveAllImages()(bShared 默认 false),
+	//            【不清】这份共享缓存。(本 app 暂未启用 bShared=true,
+	//            但库一旦被这样用,Shutdown 就必然踩到这条约束。)
+	//          · 库外业务侧也在直接用 GDI+ 且从不调 Startup:
+	//            helper/CBitmapHelper.cpp:110  "new Gdiplus::Bitmap(wsPath)"
+	//            print/CPosPrinterData.cpp:5516 "CBitmapHelper helper;"
+	//            print/CPosPrinterData.cpp:5549 "Gdiplus::Color pixelColor;"
+	//            ⇒ 业务对象靠 DuiLib 的窗口对象"顺带"把 GDI+ 撑活着。
+	//  2) MSDN 同一页的 Windows 7 专条原文:
+	//     "For Windows 7 and earlier, if GDI+ can't create a font family, it substitutes
+	//      the generic Sans Serif family and client-side caches the pointer ... the
+	//      operating system retains the pointer for the generic family object past the
+	//      object's lifetime, which causes the operating system to crash."
+	//     ⇒ Win7 上「反复 Startup/Shutdown」存在官方写明的崩溃路径。本项目必须支持 Win7。
+	//  3) DuiLib 是 DLL。若改成"由静态对象析构时 Shutdown",那份析构可能发生在
+	//     DllMain 里,而 MSDN 明令禁止在 DllMain 调 GdiplusStartup/GdiplusShutdown。
+	//
+	// 为什么"至今没炸":GdiplusStartup/Shutdown 在 gdiplus.dll 内部是【引用计数】的 ——
+	//   非末次 Shutdown 是 no-op(Raymond Chen《The case of the hung Explorer window》原文:
+	//   "That call to GdiplusShutdown will not shut down GDI+ because there is still an
+	//    outstanding client.")。目前主窗口的 manager 一直活着 ⇒ 计数恒 ≥1 ⇒
+	//   那些每窗口的 Startup/Shutdown 全是空转。【不是代码对,是运气好。】
+	//
+	// 修法(只改生命周期归属,不动任何绘制/时序):
+	//   · 静态存储保存 token / StartupInput(天然活到进程结束,满足 MSDN 对
+	//     "input 必须在 Shutdown 之前有效"的要求);
+	//   · 引用计数:0→1 时做唯一一次 Startup;递减时【不】调用 GdiplusShutdown;
+	//   · 进程退出时由 OS 回收 GDI+(业界通行做法:GDI+ 是进程级资源,
+	//     进程结束本就不需要"手动卸载")。
+	// 注:本 app 至今未发生"进程内 manager 计数归零",所以这次改动【不改变现有行为】,
+	//     只是把"靠运气"换成"靠结构";它同时消除了 Win7 那条反复 Startup/Shutdown 的路径。
+	// =====================================================================
+	namespace
+	{
+		ULONG_PTR                    g_gdiplusToken = 0;
+		volatile LONG                g_gdiplusRefs  = 0;
+		Gdiplus::GdiplusStartupInput g_gdiplusStartupInput;   // 静态存储:天然覆盖整个进程
+		bool                         g_gdiplusStarted = false; // 仅供诊断:Startup 是否成功
+
+		// 每构造一个 CPaintManagerUI 调用一次(保持"引用计数"语义,便于将来若真要
+		// 在某个确定时机收尾时有据可依)。只有 0→1 那一次才真正 GdiplusStartup。
+		inline void GdiplusAddRef()
+		{
+			if (::InterlockedIncrement(&g_gdiplusRefs) == 1)
+			{
+				const Gdiplus::Status st = Gdiplus::GdiplusStartup(&g_gdiplusToken,
+				                                                   &g_gdiplusStartupInput,
+				                                                   NULL);
+				g_gdiplusStarted = (st == Gdiplus::Ok);
+			}
+		}
+
+		// 每析构一个 CPaintManagerUI 调用一次。
+		// ★ 这里【故意不调用】GdiplusShutdown:见上方 1)/2)/3) 三条理由 ——
+		//   进程内可能仍有不受本 manager 约束的 GDI+ 对象(共享图片缓存 / 业务侧
+		//   CBitmapHelper 的 Gdiplus::Bitmap),而 GdiplusShutdown 会让全局 GDI+ 失效。
+		inline void GdiplusRelease()
+		{
+			if (g_gdiplusRefs > 0) ::InterlockedDecrement(&g_gdiplusRefs);
+		}
+	}
+
 	CPaintManagerUI::CPaintManagerUI() :
 	CPaintManagerUI::CPaintManagerUI() :
 		m_hWndPaint(NULL),
 		m_hWndPaint(NULL),
 		m_hDcPaint(NULL),
 		m_hDcPaint(NULL),
@@ -712,8 +790,18 @@ namespace DuiLib
 		::ZeroMemory(&m_rcLayeredUpdate, sizeof(m_rcLayeredUpdate));
 		::ZeroMemory(&m_rcLayeredUpdate, sizeof(m_rcLayeredUpdate));
 		m_ptLastMousePos.x = m_ptLastMousePos.y = -1;
 		m_ptLastMousePos.x = m_ptLastMousePos.y = -1;
 
 
-		m_pGdiplusStartupInput = new Gdiplus::GdiplusStartupInput;
-		Gdiplus::GdiplusStartup(&m_gdiplusToken, m_pGdiplusStartupInput, NULL); // 加载GDI接口
+		// 【第 47 轮】GDI+ 已提升为进程级(见文件上方 "进程级 GDI+ 运行时" 的长注释):
+		// 这里只递增引用计数,0→1 时才做进程内唯一一次 GdiplusStartup。
+		GdiplusAddRef();
+
+		// 两个成员降级为【借用的只读快照】,不再拥有任何资源:
+		//   · m_pGdiplusStartupInput 指向文件级静态对象 ⇒ ★ 绝对禁止 delete ★(它不是你 new 的)
+		//   · m_gdiplusToken 只是当时 token 的快照 ⇒ 仅供调试观察,禁止再传给 GdiplusShutdown
+		// 保留这两个成员(而不是删掉)是为了不改动 CPaintManagerUI 的布局:
+		// 本类被业务侧当内联成员按值持有(CToastWnd::m_pm / CMainWnd::m_pm),
+		// 布局一变业务 exe 就必须重编,否则内存错位。
+		m_pGdiplusStartupInput = &g_gdiplusStartupInput;
+		m_gdiplusToken         = g_gdiplusToken;
 
 
 		CShadowUI::Initialize(m_hInstance);
 		CShadowUI::Initialize(m_hInstance);
 
 
@@ -762,9 +850,12 @@ namespace DuiLib
 		m_aPreMessages.Remove(m_aPreMessages.Find(this));
 		m_aPreMessages.Remove(m_aPreMessages.Find(this));
 		// 销毁拖拽图片
 		// 销毁拖拽图片
 		if (m_hDragBitmap != NULL) ::DeleteObject(m_hDragBitmap);
 		if (m_hDragBitmap != NULL) ::DeleteObject(m_hDragBitmap);
-		//卸载GDIPlus
-		Gdiplus::GdiplusShutdown(m_gdiplusToken);
-		delete m_pGdiplusStartupInput;
+		// 【第 47 轮】GDI+ 收尾:只递减进程级引用计数,【故意不调用 GdiplusShutdown】。
+		// 理由见文件上方 "进程级 GDI+ 运行时" 的注释(MSDN 要求 Shutdown 前销毁所有 GDI+ 对象、
+		// Win7 反复 Startup/Shutdown 的官方崩溃路径、DLL 静态析构不能碰 Gdiplus*)。
+		// ★ 原实现那两行 `GdiplusShutdown(m_gdiplusToken); delete m_pGdiplusStartupInput;`
+		//   已删除:前者会误伤仍存活的 GDI+ 对象,后者会 delete 掉静态对象(本实例并不拥有它)。
+		GdiplusRelease();
 		// DPI管理对象
 		// DPI管理对象
 		if (m_pDPI != NULL)
 		if (m_pDPI != NULL)
 		{
 		{

+ 8 - 0
DuiLib/Core/UIManager.h

@@ -1003,6 +1003,14 @@ namespace DuiLib {
 		// 是否开启Gdiplus
 		// 是否开启Gdiplus
 		bool m_bUseGdiplusText;
 		bool m_bUseGdiplusText;
 		int m_trh;
 		int m_trh;
+		// 【第 47 轮】GDI+ 已提升为【进程级】(实现见 UIManager.cpp 文件头的
+		// "进程级 GDI+ 运行时")。下面两个成员保留仅为【不改变本类布局】——
+		// 本类被业务侧按值内联持有(CToastWnd::m_pm / CMainWnd::m_pm),
+		// 删成员会让业务 exe 必须重编。它们现在的含义是"借用的只读快照":
+		//   · m_gdiplusToken          :进程级 token 的快照,仅供调试观察;
+		//                               ★ 禁止再传给 GdiplusShutdown(本类不再做收尾)
+		//   · m_pGdiplusStartupInput  :指向文件级静态对象;
+		//                               ★ 绝对禁止 delete(本类不拥有它)
 		ULONG_PTR m_gdiplusToken;
 		ULONG_PTR m_gdiplusToken;
 		Gdiplus::GdiplusStartupInput *m_pGdiplusStartupInput;
 		Gdiplus::GdiplusStartupInput *m_pGdiplusStartupInput;
 
 

BIN
bin/Win32/Debug/zhipuzi_pos_windows/DuiLib_d.dll


BIN
bin/Win32/Release/zhipuzi_pos_windows/DuiLib.dll


BIN
bin/Win32/Release/zhipuzi_pos_windows/zhipuzi_pos_windows.exe


BIN
dll/debug/DuiLib_d.dll


BIN
dll/release/DuiLib.dll


BIN
pdb/release/DuiLib.pdb