使用GDIView排查GDI对象泄漏导致的程序UI界面绘制异常问题

news2024/11/23 22:42:22

目录

1、问题说明

2、初步分析

3、查看任务管理器,并使用GDIView工具分析

4、GDIView可能对Win10兼容性不好,显示的GDI对象个数不太准确

5、采用历史版本比对法,确定初次出现问题的时间点,并查看前一天的代码修改记录

6、将修改的代码与测试现象结合起来,最终定位问题

7、事后的思考

8、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://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对象泄漏问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125399896

7、事后的思考

        这段生成图片的代码是十多年前写的,出自于一个刚毕业的应届生之手,应该是因为经验不足,写出的代码不规范,在使用完创建的GDI对象之后应及时地将对象释放掉。我们平时一再地强调,写代码一定要规范,要尽量考虑的全面一些,否则可能会埋下一些或大或小的隐患。

        此外,这段GDI对象泄漏的代码掩藏的比较深,在业务服务器都能正常连接的平台上(比如给客户使用的商用平台)不会触发GDI泄漏。公司内部的测试平台正好这段时间业务服务器有问题,触发了客户端软件的重连流程,才将这个内存泄漏的问题暴露出来。

在公司内部测试基本没问题的软件,拿到客户的机器上,拿到各式各样的运行环境中,可能会出现这样那样的问题,比如复杂组网环境中的网络连通问题、软件运行异常等。公司内部的测试及运行环境毕竟是有限的,很多潜在的问题可能很难暴露出来。

8、最后

       该问题实例中的问题可能并不是很难,但整个问题的排查方法和思路,以及不同场景下的不同表现现象的启示,都很有参考价值。所以本文详细记录了整个问题的排查过程,以供大家借鉴或参考。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1050240.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

visual studio下载安装

一、官网下载 地址&#xff1a;https://visualstudio.microsoft.com/zh-hans/ 点击免费visual studio 二、安装 运行下载好的exe文件&#xff0c;自定义安装目录 三、选择需要的组件安装 只需要选择标记组件&#xff0c;然后点击安装 等待安装完成就行 四、重启电脑 安装完之后…

windows系统关闭软件开机自启的常用两种方法

win10中安装软件时经常会默认开机自启动&#xff0c;本文主要介绍两种关闭软件开机自启动方法。 方法1 通过任务管理器设置 1.在任务管理器中禁用开机自启动&#xff1a;打开任务管理器&#xff0c;右键已启动的软件&#xff0c;选择禁用。 方法2 通过windows服务控制开机自启…

SpringBoot——常用注解

Spring Web MVC与Spring Bean注解 Controller/RestController Controller是Component注解的一个延伸&#xff0c;Spring 会自动扫描并配置被该注解标注的类。此注解用于标注Spring MVC的控制器。 Controller RequestMapping("/api/v1") public class UserApiContr…

ssm+vue的OA办公管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的OA办公管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&am…

【Java 集合】准备知识

目录 初识集合框架 什么是集合框架 包装器 1. 基本数据类型和对应的包装器 2. 装箱和拆箱 3. 自动装箱和拆箱 4. Integer 存储机制 5. 包装器的作用 泛型 1. 什么是泛型 2. 引出泛型 2.1 泛型语法 3. 泛型类的使用 4. Java泛型实现的机制 -- 擦除机制 5. 泛型的…

PHP变量底层原理

前言 PHP是解释型的语言&#xff0c;它的执行顺序主要会经过以下几步&#xff1a; 1. 进行词法分析 2. 进行语法分析 3. 通过zend编译器&#xff0c;编译成opcode 4. zend虚拟机执行opcode 我们在写PHP代码的时候就知道&#xff0c;PHP是弱语言类型&#xff0c;而PHP底层又是由…

亚马逊无线充UL62368报告办理 无线充UL2738+UL2056标准UL认证办理亚马逊类目审核

什么是UL测试报告和UL认证有什么区别&#xff1f; 美国是一个对安全要求非常严格的国家&#xff0c;美国本土的所有电子产品生产企业早在很多年前就要求有相关安规检测。而随着亚马逊在全球商业的战略地位不断提高&#xff0c;境外的电子设备通过亚马逊不断涌入美国市场&#…

幂级数和幂级数的和函数有什么关系?

幂级数和幂级数的和函数有什么关系&#xff1f; 本文例子引用自&#xff1a;80_1幂级数运算&#xff0c;逐项积分、求导【小元老师】高等数学&#xff0c;考研数学 求幂级数 ∑ n 1 ∞ 1 n x n \sum\limits_{n1}^{\infty}\frac{1}{n}x^n n1∑∞​n1​xn 的和函数 &#xff…

What are the differences between lsof and netstat on linux?

参考&#xff1a;https://stackoverflow.com/questions/49381124/what-are-the-differences-between-lsof-and-netstat-on-linux https://www.cnblogs.com/pc-boke/articles/10012112.html LSOF: List of Open Files. It lists all the open files belonging to all active pr…

C语言实现——简易通讯录

前言&#xff1a;小伙伴们又见面啦&#xff01;这几天通过我们对自定义数据类型的学习&#xff0c;我们已经掌握了如何同时对多种数据类型进行管理&#xff0c;那么今天这篇文章&#xff0c;我们就来干一件大事——实现简易的通讯录。 一.思路分析 先来想想通讯录有哪些功能&a…

Spring Boot:利用JPA进行数据库的查删

目录标题 DAO 、Service 、 Controller 层控制器文件示例代码-单个查找查找成功示例代码-列表查找查找成功示例代码-删除删除成功 DAO 、Service 、 Controller 层 DAO 层负责数据库访问&#xff0c;它封装了对数据库的访问操作&#xff0c;例如查询、插入、更新和删除等。 Q…

《向量数据库指南》——向量数据库Elasticsearch -> Milvus 2.x

Elasticsearch -> Milvus 2.x 1. 准备 ES 数据 要迁移 ES 数据,前提假设您已经拥有属于自己的 es Server(自建、ElasticCloud、阿里云 ES 等),向量数据存储在 dense_vector,以及其他字段在 index 中,index mapping 形式如: 2. 编译打包 首先下载迁移项目源码:https…

第P7周—咖啡豆识别(1)

数据集及wen件目录介绍&#xff1a; 数据集&#xff1a;工作台 - Heywhale.com 一、前期工作 1.1 数据详情 import torch import torch.nn as nn import torchvision.transforms as transforms import torchvision from torchvision import transforms, datasets import os,…

Ros2 学习02- ubuntu22.04 安装ros2

设置镜像源 sudo vim /etc/apt/sources.list#阿里源 deb http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ jamm…

小波变换学习笔记【1】

【声明】本博客为学习B站视频小波分解与重构所做笔记&#xff0c;供自己和大家查阅学习&#xff0c;想查看 up 原视频请移步 B 站&#xff0c;侵删。 1.1 小波变换的由来 傅里叶变换基本思想&#xff1a;将信号分解成一系列不同频率的连续正弦波的叠加。 其缺点是&#xff0c;…

数据结构 | 二叉树

基本形状 可参照 数据结构&#xff1a;树(Tree)【详解】_数据结构 树_UniqueUnit的博客-CSDN博客 二叉树的性质 三种顺序遍历

基于Java的演唱会网上订票系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

记录一次SQL注入src挖掘过程

记录一次SQL注入src挖掘过程&#xff0c;小白挖洞 先是使用谷歌语法找到一个可以注入的网站 谷歌语法&#xff1a; 公司inurl:php?id 然后该公司的URL为 URL:XXXXXXXXXX/xxx.php?id1 输入测试有无注入&#xff0c;有没有waf 发现有报错 使用sqlmap寻找注入点 python…

golang 结合 cobra 使用 chatgpt qdrant 实现 ai知识库 cli

golang 结合 cobra 使用 chatgpt qdrant 实现 ai知识库 cli 流程 将数据集 通过 openai embedding 得到向量组装payload,存入 qdrant用户进行问题搜索,通过 openai embedding 得到向量,从 qdrant 中搜索相似度大于0.8的数据从 qdrant 中取出数据得到参考答案将问题标题参考答案…

Flutter笔记:用于ORM的Floor框架简记

Flutter笔记 用于ORM的Floor框架简记 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/133377191 【介绍】&#xff1a;最近想找用于Dart和Flutter的ORM框架&#xff0c;偶然间发现了Floor&#xff0c;觉得还不错&#xff0c;做一些记录。 1. Floor 框…