使用GDIView排查GDI对象泄漏导致的程序UI界面绘制异常问题
目录
1、问题说明
2、初步分析
3、查看任务管理器,并使用GDIView工具分析
4、GDIView可能对Win10兼容性不好,显示的GDI对象个数不太准确
5、采用历史版本比对法,确定初次出现问题的时间点,并查看前一天的代码修改记录
6、将修改的代码与测试现象结合起来,最终定位问题
7、事后的思考
8、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)
https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)
https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_11931267.html 最近在项目中遇到了一个GDI对象泄漏问题,排查该问题也用了不少时间,其问题场景和排查方法很有代表性,今天将当时排查的过程做个详细的记录和总结,以供大家借鉴或参考。
1、问题说明
某天早上来公司上班,有同事反馈我们开发的PC客户端软件的UI界面显示出问题了,窗口中部分区域与按钮等显示不全,应该是部分界面绘制失败了,但软件并没有崩溃。同事说前一天晚上他们为了测试移动APP,使用我们的PC端软件和移动app进行了联动,进行了一夜的拷机测试,早上来就发现PC客户端出了上述问题。他们没有关闭软件,保留着现象及现场,喊我们过去看看是怎么回事。
2、初步分析
于是我到同事那边去查看现象,根据以往经验,感觉很有可能是GDI句柄泄漏引起界面绘制异常,当程序的GDI对象接近1万个时,界面就会出现绘制失败,显示不全的问题。
在Windows系统中,每个进程的GDI对象总数是有上限的,上限就是10000个,当接近或达到这个上限时,界面就会绘制失败,甚至程序会出现闪退崩溃。
类似的问题,前几天有同事反馈过,但其没保留现象,直接将程序重启了,所以当时没有进行分析,这次这个同事反馈的这个问题,从现象上看和之前同事反馈的问题是类似,正好借此机会详细排查一下。
3、查看任务管理器,并使用GDIView工具分析
为了验证GDI对象泄漏的猜想,先是打开系统的任务管理器去查看软件进程的GDI对象总数。打开任务管理器,点击详细信息标签页,找到目标软件进程,看到进程的GDI总数确实有异常,已经达到了9999个,差1个就到1万的上限了,如下所示:
正常情况下,进程在某个时刻使用的GDI对象总数也就几百个左右,最多也就1000多个,此处居然达到了9999个,肯定是有GDI对象泄漏了。使用完的GDI对象,没有调用DeleteObject等接口将之释放掉,这样会导致程序中占用的GDI对象越来越多,如果有GDI对象泄漏的代码在频繁的执行,那么泄漏会很明显,到任务管理器中可以看到目标进程的GDI对象总数在持续不断的上升,可能很快就要达到10000个上限了。
默认情况下,任务管理器的进程列表中不会显示进程的GDI总数,需要右键点击列表头,在弹出的右键菜单中点击“选择列”:
然后在弹出的窗口中找到GDI对象选项:
勾选上即可看到进程的GDI对象总数。
正常情况下,进程的GDI对象最多只会有上千个,如果有好几千,一般可能是有GDI对象泄漏。
4、GDIView可能对Win10兼容性不好,显示的GDI对象个数不太准确
仅仅通过任务管理器中的GDI对象总数判断出有GDI对象是不够的,因为GDI对象有多种,比如常见的GDI对象有Pen(用来划线的画笔)、Brush(用来填充区域颜色的画刷)、Bitmap(用来绘制图片的位图)、Font(用来控制文字显示大小及字形的字体)、Region(区域)、DC(用来绘制窗口的设备上下文)等,如下:
为了有针对性的排查,我们还需要知道具体是哪种类型的GDI对象有泄漏,这就需要使用GDI对象查看工具GDIView。于是在同事的电脑上到GDIView官网上下载了GDIView工具,因为当前Windows系统是64位的,所以要下载64位的GDIView,64位系统上不能运行32位的GDIView,直接运行会报错(按讲64位系统是支持32位程序的,可能是GDIview工具自己的限制),如下:
启动64位GDIView工具后,看到All GDI数目为9999:
查看其他具体类型的项,只有Bitmap位图对象数目比较多,但只有1000多个(1436个),其他类型的GDI对象都比较少,那这个9999总数主要是由哪个对象泄漏引发的呢?从最后的分析结果看,是Bitmap对象泄漏引起的,那Bitmap对象数应该有好几千个,为啥GDIView中只显示1000多个?应该是GDIView工具对Win10系统兼容性不好,显示的各个GDI对象的数目有问题,之前我们在Win7和XP系统上用GDIView工具排查过GDI泄漏问题,GDIView中显示的各个类型的GDI对象都是比较准的。
5、采用历史版本比对法,确定初次出现问题的时间点,并查看前一天的代码修改记录
GDIView中看不到具体是哪个类型的GDI泄漏,这样我们就没法进行有针对性的排查。好在我们有个脚本控制的自动化代码编译系统,只要有修改代码,每天都会自动编译版本,生成程序的安装包,如下所示:
于是我们只能使用历史版本比对法,取几个时间点的版本(安装包),多次安装并执行程序,然后再采取二分法取版本,看看是从哪天开始有这个问题的,然后我们查看前一天提交的代码,可能就能找到排查问题的线索了。
历史版本比对法,比较适用相对独立的客户端程序,虽然是个比较笨重、原始的办法,但很多时候都比较有用,我们在项目中已多次使用。
最终通过对比发现,从2022年12月15日开始编译的版本都有内存泄漏的问题,12月14日的版本是没问题的。于是在SVN上查看前一天(12月14日)的代码提交记录,看看可能是修改哪一处的代码引发的。但12月14日当天修改的代码是处理业务服务器重连问题,修改了相关的逻辑,但这些修改的代码都和GDI绘制没关系,为啥会触发GDI对象泄漏呢?很是奇怪!到此,排查问题的线索似乎又断了。
6、将修改的代码与测试现象结合起来,最终定位问题
12月14日之前业务服务器的重连功能都是有问题的,12月14日修改代码后,重连功能就没问题了。这时,测试同事又提供了一个关键的线索,用当前最新的版本的客户端软件,登录公网上的通用平台是没有GDI对象泄漏的,但登录公司内部的内网测试平台就有GDI对象泄漏。这两个平台有啥差别,导致同一个版本的客户端软件登录后有不同的表现呢?
于是结合修改的代码,12月14日修改的代码是关于某类业务服务器断链后的重连代码,12月14日之前的重连代码都是有问题的。难道是两个平台上某个业务服务器的连通状态是不一样的?于是用客户端分别登录这两个平台,查看日志,看看两个平台的所有业务服务器的连接状态。果然是有差异的,公网平台上的所有业务服务器都是能正常连接的,但内网测试平台上某个业务服务器一直是连不上的,一直在不断重连(业务服务器连不上时会自动去重连)。
于是查看触发重连时的整个流程的所有代码,然后果然找到了问题,重连的流程中会去调用一个接口去自动生成一张图片,调用CreateCompatibleBitmap API函数去创建一个Bitmap位图对象,但这个Bitmap位图对象在使用完后,没有调用DeleteObject将Bitmap对象释放掉,所以导致了GDI对象泄漏。问题代码片如下:
HDC hdc = ::GetDC( NULL );
HDC memDC = ::CreateCompatibleDC( hdc );HBITMAP hBitmap, hOldBitmap;
// 调用CreateCompatibleBitmap创建一个与设备描述表兼容的位图(问题就出在这个)
hBitmap = ::CreateCompatibleBitmap( hdc, nWidth, nHeight );
hOldBitmap = (HBITMAP)SelectObject( memDC, hBitmap ); ::SetBkColor( memDC, WHITE_BRUSH ); // 区域刷白// 设置字体
// 字体创建
LOGFONT lf;
u32 dwXDpi = GetDeviceCaps( hdc, LOGPIXELSX ); // 得到当前显示设备的水平单位英寸像素数;
if ( dwXDpi != 0)
{nPointSize = static_cast<u32>( nPointSize * 96.0 / dwXDpi );
}
memset( &lf, 0, sizeof(LOGFONT) );
lf.lfHeight = -MulDiv ( nPointSize, GetDeviceCaps ( hdc, LOGPIXELSY ), 72 );
lf.lfWidth = lf.lfHeight/2;
lf.lfOutPrecision = OUT_STRING_PRECIS;
lf.lfQuality = CLEARTYPE_QUALITY;
lf.lfWeight = FW_NORMAL;
_tcscpy( lf.lfFaceName, _T("微软雅黑") );
HFONT hFont = ::CreateFontIndirect( &lf );::SetBkMode(memDC, TRANSPARENT);
::SetTextColor( memDC, RGB( 213, 242, 253 ) );
HFONT hOldFont = (HFONT)::SelectObject( memDC, hFont );RECT rcDest;
rcDest.left = 0;
rcDest.top = 0;
rcDest.right = nWidth;
rcDest.bottom = nHeight;if ( emLogoPos == emTopLeft_Api || emLogoPos == emBottomLeft_Api )
{::DrawText( memDC, strName, strName.GetLength(), &rcDest, DT_LEFT | DT_SINGLELINE );
}
else
{::DrawText( memDC, strName, strName.GetLength(), &rcDest, DT_RIGHT | DT_SINGLELINE );
}//::BitBlt( hdc, 0,0,nWidth,nHeight, memDC,0,0, SRCCOPY );CUIString strFile = GetSelfFilePath()+ LOGO_BMP_FILE;
SaveBitmapToBmpFile( hBitmap, strFile, LOGO_DPI_32 );::SelectObject( memDC, hOldFont );
::SelectObject( memDC, hOldBitmap );if ( hFont != NULL )
{::DeleteObject( hFont );
}if ( NULL != memDC )
{::DeleteDC( memDC );
}
::ReleaseDC( NULL, hdc );
代码结尾处释放了Font字体对象和DC对象,但忘记释放Bitmap对象。在代码片的结尾处应该调用DeleteObject将之前创建的Bitmap对象释放掉,即:
if ( hBitmap != NULL )
{::DeleteObject( hBitmap );
}
修改后的代码块如下:
因为测试平台上某个业务服务器始终有问题,客户端连接不上,一直在不断的重连,所以这段包含GDI对象泄漏的代码在持续不断的执行,这样在长时间的拷机运行之后,导致程序的GDI对象总数达到了9999个。至此终于找到产生GDI对象泄漏的源头,修改代码后编译版本再安装运行,就不再有内存泄漏了。
这个地方也说明一个问题,GDIView在Win10系统中运行,显示的各类型的GDI对象的数目是不准确的。本问题中,是Bitmap对象有泄漏,Bitmap对象应该有好几千个才对,结果GDIView中显示的Bitmap对象只有1000多个,这个显示不准确的问题下次要注意了。其实,一开始看到Bitmap对象有1000多个,就应该觉察到Bitmap对象有问题了,一般情况下不可能有这么多的!
此外,之前也写过一篇使用GDIView排查GDI对象泄漏的案例,感兴趣的话可以查看对应的文章:
使用GDIView工具排查GDI对象泄漏问题https://blog.csdn.net/chenlycly/article/details/125399896
7、事后的思考
这段生成图片的代码是十多年前写的,出自于一个刚毕业的应届生之手,应该是因为经验不足,写出的代码不规范,在使用完创建的GDI对象之后应及时地将对象释放掉。我们平时一再地强调,写代码一定要规范,要尽量考虑的全面一些,否则可能会埋下一些或大或小的隐患。
此外,这段GDI对象泄漏的代码掩藏的比较深,在业务服务器都能正常连接的平台上(比如给客户使用的商用平台)不会触发GDI泄漏。公司内部的测试平台正好这段时间业务服务器有问题,触发了客户端软件的重连流程,才将这个内存泄漏的问题暴露出来。
在公司内部测试基本没问题的软件,拿到客户的机器上,拿到各式各样的运行环境中,可能会出现这样那样的问题,比如复杂组网环境中的网络连通问题、软件运行异常等。公司内部的测试及运行环境毕竟是有限的,很多潜在的问题可能很难暴露出来。
8、最后
该问题实例中的问题可能并不是很难,但整个问题的排查方法和思路,以及不同场景下的不同表现现象的启示,都很有参考价值。所以本文详细记录了整个问题的排查过程,以供大家借鉴或参考。
相关文章:

使用GDIView排查GDI对象泄漏导致的程序UI界面绘制异常问题
目录 1、问题说明 2、初步分析 3、查看任务管理器,并使用GDIView工具分析 4、GDIView可能对Win10兼容性不好,显示的GDI对象个数不太准确 5、采用历史版本比对法,确定初次出现问题的时间点,并查看前一天的代码修改记录 6、将…...
蓝桥等考Python组别一级001
第一部分:选择题 1、Python L1 (15分) 下面哪个不是Python的编程环境?( ) Python在线编程IDLEPyCharmScratch正确答案:D 2、Python L1(15分) 世界上第一台通用电子计算机ENIAC是在( )诞生的。 美国英国日本德国正确答案:A 3、Python L1(20分) 关于P…...

Unity之Hololens2开发 如何接入的MRTK OpenXR Plugin
一.前言 什么是Hololens? Hololens是由微软开发的一款混合现实头戴式设备,它将虚拟内容与现实世界相结合,为用户提供了沉浸式的AR体验。Hololens通过内置的传感器和摄像头,能够感知用户的环境,并在用户的视野中显示虚拟对象。这使得用户可以与虚拟内容进行互动,将数字信…...

Ubuntu系统Linux内核安装和使用
安装: 检查树莓派Linux版本,我的是6.1 uname -r 内核下载链接: Raspberry Pi GitHub 找对应版本下载 导入之后,解压安装即可 unzip linux-rpi-6.1.y.zip 其他内容 treee 指令安装 sudo apt-get install tree 使用这…...
数学术语之源——群同态的“核(kernel)”
1. “kernel”这个术语在群论中的起源 Ivar Fredholm 在 1903 年的第27期Acta Math 数学学报发表的一篇关于“积分方程(INTEGRAL EQUATIONS)”的著名论文(“关于一类函数方程(Sur une classe des quations fonctionnelles)”)中使用了法语“noyau(核)”(365-390页)。 David …...

defcon-quals 2023 crackme.tscript.dso wp
将dso文件放到data/ExampleModule目录下,编辑ExampleModule.tscript文件 function ExampleModule::onCreate(%this) { trace(true); exec("./crackme"); __main("aaaaaaaa"); quit(); } 然后点击主目录下的Torque3D-debug.bat就可以在生成的c…...

前端开发 vs. 后端开发:编程之路的选择
文章目录 前端开发:用户界面的创造者1. HTML/CSS/JavaScript:2. 用户体验设计:3. 响应式设计:4. 前端框架: 后端开发:数据和逻辑的构建者1. 服务器端编程:2. 数据库:3. 安全性&#…...
算法练习4——删除有序数组中的重复项 II
LeetCode 80 删除有序数组中的重复项 II 给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。 不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 …...

【C++进阶(六)】STL大法--栈和队列深度剖析优先级队列适配器原理
💓博主CSDN主页:杭电码农-NEO💓 ⏩专栏分类:C从入门到精通⏪ 🚚代码仓库:NEO的学习日记🚚 🌹关注我🫵带你学习C 🔝🔝 栈和队列 1. 前言2. 栈和队列的接口函数熟悉3. …...

linux opensuse使用mtk烧录工具flashtool
环境 linux发行版:opensuse leap 15.5 工具:SP_Flash_Tool_Selector_exe_Linux_v1.2316.00.100.rar 或其他版本 目标:mtk设备 下载链接 https://download.csdn.net/download/zmlovelx/88382784 或网络搜索。 使用 opensuse可直接解压后使…...

Visio如何对文本打下标、上标,以及插入公式编辑器等问题(已解决)
解决这个问题的本质问题,就是在Visio中插入公式编辑器(这不是visio的常用命令,需要添加)。 打开Visio--》文件--选项 点击选项,弹出对话框。在自定义功能区中,点击 常用命令,在下拉选项中&#…...

快速将iPhone大量照片快速传输到电脑的办法!
很多使用iPhone 的朋友要将照片传到电脑时,第一时间都只想到用iTunes 或iCloud,但这2个工具真的都非常难用,今天小编分享牛学长苹果数据管理工具的照片传输功能,他可以快速的将iPhone照片传输到电脑上,并且支持最新的i…...
TCP/IP协议簇包含的协议
应用层(Application Layer): HTTP(Hypertext Transfer Protocol):用于Web浏览器和Web服务器之间的通信。HTTPS(Hypertext Transfer Protocol Secure):安全的HTTP版本&…...

天地图绘制区域图层
背景: 业务方要求将 原效果图 参考效果图 最终实现效果 变更点: 1.将原有的高德地图改为天地图 2.呈现形式修改:加两层遮罩:半透明遮罩层mask区域覆盖物mask 实现过程: 1.更换地图引入源 <link rel"style…...
git权限不够:Ask a project Owner or Maintainer to create a default branch
新仓库还未创建任何分支时,Developer角色时首次提交代码,抛如下异常 remote: GitLab: remote: A default branch (e.g. master) does not yet exist for galaxy/apache-jspf-project remote: Ask a project Owner or Maintainer to cre…...

AI在材料科学中的应用
7 AI在材料科学中的应用 在这一部分,我们将讨论AI技术在材料科学中的应用。首先,我们将介绍晶体材料的概述,并详细定义晶体材料的物理对称性,具体在第7.1节中讨论。接下来,我们将在第7.2节和第7.3节中讨论两个常见且基…...

VSCode快速设置heder和main函数
快速设置header: 点击左侧的齿轮,选择User Snippets: 在出现的选择框中输入python,选择python.json 在最外层的{ }内部添加以下内容 "HEADER": {"prefix": "header","body": ["# -*- encoding:…...

JimuReport积木报表 v1.6.2 版本正式发布—开源免费的低代码报表
项目介绍 一款免费的数据可视化报表,含报表和大屏设计,像搭建积木一样在线设计报表!功能涵盖,数据报表、打印设计、图表报表、大屏设计等! Web 版报表设计器,类似于excel操作风格,通过拖拽完成报…...
sqlsession对象为什么不能被共享?
因为它是一个非线程安全的对象。每个SQLSession对象都维护了一个独立的数据库连接,以及与该连接相关的事务和缓存。如果多个线程共享同一个SQLSession对象,可能会导致数据混乱、事务冲突等问题。另外,SQLSession对象还包含了一级缓存…...

MySQL MMM高可用架构
MySQL MMM高可用架构一、MMM概述1、MMM简介2、MMM高可用架构3、MMM故障切换流程 二、MMM高可用双主双从架构部署1、配置主主复制(master),主从复制(slave)1)修改 Master1的MySQL配置文件2)把配置…...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...
深入理解Optional:处理空指针异常
1. 使用Optional处理可能为空的集合 在Java开发中,集合判空是一个常见但容易出错的场景。传统方式虽然可行,但存在一些潜在问题: // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...

【Linux】自动化构建-Make/Makefile
前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具:make/makfile 1.背景 在一个工程中源文件不计其数,其按类型、功能、模块分别放在若干个目录中,mak…...

【Java多线程从青铜到王者】单例设计模式(八)
wait和sleep的区别 我们的wait也是提供了一个还有超时时间的版本,sleep也是可以指定时间的,也就是说时间一到就会解除阻塞,继续执行 wait和sleep都能被提前唤醒(虽然时间还没有到也可以提前唤醒),wait能被notify提前唤醒…...
FTXUI::Dom 模块
DOM 模块定义了分层的 FTXUI::Element 树,可用于构建复杂的终端界面,支持响应终端尺寸变化。 namespace ftxui {...// 定义文档 定义布局盒子 Element document vbox({// 设置文本 设置加粗 设置文本颜色text("The window") | bold | color(…...
Yii2项目自动向GitLab上报Bug
Yii2 项目自动上报Bug 原理 yii2在程序报错时, 会执行指定action, 通过重写ErrorAction, 实现Bug自动提交至GitLab的issue 步骤 配置SiteController中的actions方法 public function actions(){return [error > [class > app\helpers\web\ErrorAction,],];}重写Error…...
k8s从入门到放弃之Pod的容器探针检测
k8s从入门到放弃之Pod的容器探针检测 在Kubernetes(简称K8s)中,容器探测是指kubelet对容器执行定期诊断的过程,以确保容器中的应用程序处于预期的状态。这些探测是保障应用健康和高可用性的重要机制。Kubernetes提供了两种种类型…...

【技巧】dify前端源代码修改第一弹-增加tab页
回到目录 【技巧】dify前端源代码修改第一弹-增加tab页 尝试修改dify的前端源代码,在知识库增加一个tab页"HELLO WORLD",完成后的效果如下 [gif01] 1. 前端代码进入调试模式 参考 【部署】win10的wsl环境下启动dify的web前端服务 启动调试…...
接口 RESTful 中的超媒体:REST 架构的灵魂驱动
在 RESTful 架构中,** 超媒体(Hypermedia)** 是一个核心概念,它体现了 REST 的 “表述性状态转移(Representational State Transfer)” 的本质,也是区分 “真 RESTful API” 与 “伪 RESTful AP…...