0


开源WebRTC库放大器模式在采集桌面图像时遇到的DPI缩放与内存泄漏问题排查

VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具案例集锦(专栏文章正在更新中...)https://blog.csdn.net/chenlycly/category_12279968.html WebRTC开源库中实现桌面图像采集的方式有多种,为了支持过滤部分窗口的功能,我们采用了magnification放大器方式,但在使用放大器这种采集方式时遇到了一些问题,在这里大概地总结一下,给大家提供一个借鉴或参考。

1、在非100%的显示比例下放大器采集到的桌面图像不全问题

   我们软件为了支持过滤窗口,采用了开源WebRTC库中支持的放大镜采集模式,但使用放大器模式后测试发现,**当系统的显示比例调成非100%的显示比例(比如150%、200%等)后,放大器组件采集出来的桌面图像不全,只采集到桌面的一部分**。应该是系统DPI显示缩放引起的,默认情况下,系统会根据当前的显示比例自动对程序进行缩放。

1.1、通过manifest文件禁止系统对软件进行缩放

    一般再高分辨率的电脑上,均需要将系统的显示比例设置成100%以上的比例。设置入口是,在桌面空白处点击右键,在弹出的右键菜单中点击显示设置,然后在打开的窗口中找到缩放与布局栏,就可以更改系统的显示比例了,如下所示:

默认情况下,系统会根据当前的显示比例自动对程序进行缩放,除非我们想禁用一下系统的缩放,让程序始终保持100%的显示效果,看看放大器组件采集出来的桌面图像是否完整。直接到我们程序桌面快捷方式的属性中设置禁止系统对我们程序进行缩放,设置入口如下所示:

即不管系统设置了什么显示比例,我们的程序始终保持100%的显示效果,重新运行程序,发现采集出来的桌面图像就完整了。

** 上述设置是手动修改的,有没有办法通过代码去设置呢?告诉系统不要对我的程序进行缩放呢?以前我们研究过系统API函数SetProcessDPIAware,这个函数可以禁止系统对我们程序进行缩放,但将程序放到Win10系统中运行就没有效果了,系统还是对程序进行了缩放。后来查看SetProcessDPIAware函数在MSDN上说明,提示不要再使用这接口了,应该使用嵌入manifest文件的方式**,相关说明如下:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <asmv3:application>
    <asmv3:windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

我们新建txt文件,然后把上述内容拷贝到该文件中,然后再将文件重命名为.mannifest文件即可(注意manifest文件的名称要和进程名称一致)。然后直接将manifest文件像添加文件一样添加到工程中,编译后启动程序就生效了,Win10系统中就能实现禁止系统缩放了。

1.2、调用SetThreadDpiAwarenessContext函数,禁止系统对目标线程中的窗口进行缩放

   如果要让放大器组件采集到完整的桌面图像,就需要禁用系统对程序的缩放。但要禁用系统的缩放,让程序始终显示100%大小效果,会导致程序在一些高分率的电脑上(比如2K或4K的微软Surface平板电脑),显示太小,根本就没法看没法操作了。腾讯系的很多软件都禁用了系统缩放,软件自己实现了跟随系统显示比例的缩放,所以他们没有这样的困扰。但程序自己去实现缩放是有难度的,我们目前做不到,所以还是需要依赖系统缩放的。

   后来找到了一个针对线程设置DPI缩放属性的API函数**SetThreadDpiAwarenessContext**,**可以禁止系统对某个线程中创建的窗口禁止缩放**。我们可以将放大器组件的操作放到一个线程中,然后调用这个函数禁止系统对该线程中放大器窗口进行缩放,这样放大器组件就能采集到完整的桌面图像了。

   这个API接口是放置在系统库user32.dll中的:

Win10以前的版本是不支持的,即便是Win10系统也要1607(2016年7月发布)之后的版本才支持。所以,要从user32.dll库中动态加载,如果找不到SetThreadDpiAwarenessContext接口,则直接返回;如果找到接口再去调用,相关代码如下:

bool SetThreadDpiAware() 
{
  //设置本线程为DPI感知模式,解决缩放时屏幕采集不全的问题
  HMODULE user32_module = GetModuleHandle(TEXT("user32.dll"));
  if (nullptr == user32_module) 
  {
      return false;
  }

  decltype(
      &SetThreadDpiAwarenessContext) set_thread_dpi_awareness_context_func =
      (decltype(&SetThreadDpiAwarenessContext))GetProcAddress(
          user32_module, "SetThreadDpiAwarenessContext");
  if (nullptr == set_thread_dpi_awareness_context_func) 
  {
      return false;
  }

  DPI_AWARENESS_CONTEXT original_dpi_awareness_context = set_thread_dpi_awareness_context_func(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
  return true;
}

1.3、使用winver命令查看Windows的年月版本

    上面说到的SetThreadDpiAwarenessContext函数只有Win10系统才支持,并且只有“Windows 10, version 1607”以后的版本才支持,此处的版本号1607是2016年7月发布的意思。这个version 1607是**年月版本**,在命令行中使用systeminfo命令查看的版本是系统内部版本:

   如果要查看Windows发布的年月版本,则需要使用**winver命令**,执行该命令后会打开如下的版本窗口:

如图所示,当前机器的年月版本为1909,即2019年9月发布的版本。

2、使用放大器模式遇到的内存泄漏问题

   测试同事反馈,在多次发起桌面共享后,软件会出现闪退问题,异常捕获模块没有捕获到,没有生成dump文件。

2.1、使用Windbg动态调试发现软件因为申请内存失败抛出bad_alloc异常导致程序闪退

  ** 对于这种没有捕获到异常的场景,就需要使用Windbg进行动态调试了。**于是让同事重新将软件启动起来,然后将Windbg附加上去,让Wingdbg和软件一起跑,然后按照操作步骤将闪退问题复现出来。问题复现时,Windbg第一时间感知到并中断下来,使用kn命令查看此时的函数的函数调用堆栈,如下所示:

通过堆栈看出当前是在用new动态申请内存时抛出了bad_alloc异常,所以引发了异常崩溃。

2.2、进一步分析发现时内存泄漏导致进程内存不足,引发申请内存失败抛出bad_alloc异常

   应该是内存不足导致内存申请失败,进而抛出bad_alloc异常。此时的进程还在的,**使用Process Explorer工具查看进程的虚拟内存占用(显示的是用户态的虚拟内存)**,看到进程的虚拟内存已经占用了1.7GB。注意,Window任务管理器看不到进程占用的虚拟内存,只能看到与物理内存相关的内容,需要借助Process Explorer工具去查看。

Process Explorer工具默认是不显示Virtual Size虚拟内存的,需要右键点击进程列表的标题栏,点击“Select Columns”,在弹出的窗口中点击“Process Memory”标签页,然后将“Virtual Size”选项:

   我们的程序是32位的,系统给进程分配了4GB的虚拟内存,其中用户态虚拟内存占2GB,内核态虚拟内存占2GB。当前程序进程的用户态虚拟内存占用达到1.7GB:

按将离上限2GB还有300MB空闲,为啥用new申请内存时会失败呢?可能申请的是一段较长的buffer,而空闲的300MB虚拟内存是分散在不同地方的零零散散的小块内存(这就是我们经常讲的内存碎片),找不到一段很大的连续内存去分配了,所以出现内存分配失败了。为啥程序的虚拟内存占用会达到1.7GB之多呢?估计是程序中有内存泄漏了

   鉴于当前的操作场景,可能是桌面共享功能模块有内存泄漏,于是让测试发起桌面共享之前记录一下总的虚拟内存大小,然后发起桌面共享,然后再关闭桌面共享,看看内存有没有明显增长。经测试发现,发起共享时会申请内存,但停止共享后没有将申请的内存释放掉,所以这个操作有内存泄漏。后来使用Windbg分析内存泄漏,分析出发生泄漏内存的函数调用堆栈就指向桌面共享的模块代码中。具体如何使用Windbg分析内存泄漏,可以参见我之前写的文章:

使用Windbg定位Windows C++程序中的内存泄漏https://blog.csdn.net/chenlycly/article/details/121295720

2.3、排查桌面共享模块内存泄漏的原因

2.3.1、怀疑是放大器组件回调上来的buffer没有释放导致内存泄漏的

   桌面共享模块为了实现窗口过滤,选用了上面讲到的magnification放大器模式,于是详细去排查操作放大器的相关代码,看看为啥会有内存泄漏。代码中主要使用了**MagInitialize、MagUninitialize、MagSetWindowSource、MagSetWindowFilterList、MagSetImageScalingCallback**这几个系统API函数,到MSDN上详细查看了这几个函数的详细说明,但说明都比较少,没有找到线索,也没有说需要额外释放哪些资源。**我们在结束桌面共享时也调用MagUninitialize去释放资源,但还是存在内存泄漏。**

   排查至此,陷入了僵局,没法进行下去了。后来想,难道是放大器组件调用设置进去的回调函数回调出来的buffer需要外部去释放?这个回调函数是调用**MagSetImageScalingCallback**函数设置的,函数声明如下:
ypedef BOOL (CALLBACK* MagImageScalingCallback)(HWND hwnd, void * srcdata, MAGIMAGEHEADER srcheader, void * destdata, MAGIMAGEHEADER destheader, RECT unclipped, RECT clipped, HRGN dirty );

回调出来的buffer地址就存放在void * srcdata指针变量中,通过加打印发现回调出来的buffer首地址一个不变的地址,估计在放大器组件内部在开始时申请的一段buffer,存放抓取的桌面共享图像数据的,多次抓取的图像数据都是保存在该buffer中,然后每次将该buffer的地址回调出去。

   **怀疑这个放大器组件内部申请的buffer内存没有释放,导致了内存泄漏**。于是将回调函数回调出来的buffer地址保存到成员变量void* m_srcdata中,在结果共享时上层去主动将这个bufer内存给释放掉。最开始尝试用delete去释放,结果执行到delete时产生了异常。

2.3.2、参考API函数GetAdaptersAddresses的Remarks部分的说明,决定使用HeapFree去释放

   动态申请内存的方式有多种,**比如使用new(要用delete去释放),比如使用malloc(要用free去释放),再比如调用系统API函数HeapCreate或者HeapAlloc(要用HeapFree去释放),还有可以调用API函数VirtualAlloc(要用VirtualFree去释放),还有其他的API函数。**

   到底这个buffer使用哪种方式呢?以前在写读取多个网卡信息时,需要调用系统API函数**GetAdaptersAddresses**,在调用该接口时传入用来存放网卡信息的buffer需要外部申请好,看MSDN上GetAdaptersAddresses函数的Remarks部分说明:

建议使用HeapAlloc去申请内存,然后使用完后使用HeapFree将内存释放掉。调用系统API函数GetAdaptersAddresses的示例代码如下:

#include <winsock2.h>
#include <iphlpapi.h>
#include <stdio.h>
#include <stdlib.h>

// Link with Iphlpapi.lib
#pragma comment(lib, "IPHLPAPI.lib")

void PrintAdapterInfo()
{
    // Declare and initialize variables
    DWORD dwSize = 0;
    DWORD dwRetVal = 0;

    unsigned int i = 0;

    // Set the flags to pass to GetAdaptersAddresses
    ULONG flags = GAA_FLAG_INCLUDE_PREFIX;

    // default to unspecified address family (both)
    ULONG family = AF_UNSPEC;// AF_INET - ipv4, AF_INET6 - ipv6
    
    LPVOID lpMsgBuf = NULL;

    PIP_ADAPTER_ADDRESSES pAddresses = NULL;
    ULONG outBufLen = 0;
    ULONG Iterations = 0;

    PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL;
    PIP_ADAPTER_UNICAST_ADDRESS pUnicast = NULL;
    PIP_ADAPTER_ANYCAST_ADDRESS pAnycast = NULL;
    PIP_ADAPTER_MULTICAST_ADDRESS pMulticast = NULL;
    IP_ADAPTER_DNS_SERVER_ADDRESS *pDnServer = NULL;
    IP_ADAPTER_PREFIX *pPrefix = NULL;

    // Allocate a 15 KB buffer to start with.
    outBufLen = 15000;

    do 
    {
        pAddresses = (IP_ADAPTER_ADDRESSES *)HeapAlloc(GetProcessHeap(), 0, outBufLen);
        if (pAddresses == NULL) 
        {
            printf("Memory allocation failed for IP_ADAPTER_ADDRESSES struct\n");
            return;
        }

        dwRetVal = GetAdaptersAddresses( family, flags, NULL, pAddresses, &outBufLen );
        if (dwRetVal == ERROR_BUFFER_OVERFLOW) 
        {
            HeapFree( GetProcessHeap(), 0, pAddresses );
            pAddresses = NULL;
        }
        else 
        {
            break;
        }

        Iterations++;
    } while ((dwRetVal == ERROR_BUFFER_OVERFLOW) && (Iterations < 3));

    if (dwRetVal == NO_ERROR) 
    {
        // If successful, output some information from the data we received
        pCurrAddresses = pAddresses;
        while (pCurrAddresses) 
        {
            printf("\tLength of the IP_ADAPTER_ADDRESS struct: %ld\n",
                pCurrAddresses->Length);
            printf("\tIfIndex (IPv4 interface): %u\n", pCurrAddresses->IfIndex);
            printf("\tAdapter name: %s\n", pCurrAddresses->AdapterName);

            pUnicast = pCurrAddresses->FirstUnicastAddress;
            if (pUnicast != NULL) 
            {
                for (i = 0; pUnicast != NULL; i++)
                    pUnicast = pUnicast->Next;
                printf("\tNumber of Unicast Addresses: %d\n", i);
            }
            else
                printf("\tNo Unicast Addresses\n");

            pAnycast = pCurrAddresses->FirstAnycastAddress;
            if (pAnycast) 
            {
                for (i = 0; pAnycast != NULL; i++)
                    pAnycast = pAnycast->Next;
                printf("\tNumber of Anycast Addresses: %d\n", i);
            }
            else
                printf("\tNo Anycast Addresses\n");

            pMulticast = pCurrAddresses->FirstMulticastAddress;
            if (pMulticast) 
            {
                for (i = 0; pMulticast != NULL; i++)
                    pMulticast = pMulticast->Next;
                printf("\tNumber of Multicast Addresses: %d\n", i);
            }
            else
                printf("\tNo Multicast Addresses\n");

            pDnServer = pCurrAddresses->FirstDnsServerAddress;
            if (pDnServer) 
            {
                for (i = 0; pDnServer != NULL; i++)
                    pDnServer = pDnServer->Next;
                printf("\tNumber of DNS Server Addresses: %d\n", i);
            }
            else
                printf("\tNo DNS Server Addresses\n");

            printf("\tDNS Suffix: %wS\n", pCurrAddresses->DnsSuffix);
            printf("\tDescription: %wS\n", pCurrAddresses->Description);
            printf("\tFriendly name: %wS\n", pCurrAddresses->FriendlyName);

            if (pCurrAddresses->PhysicalAddressLength != 0) 
            {
                printf("\tPhysical address: ");
                for (i = 0; i < (int)pCurrAddresses->PhysicalAddressLength;i++) 
                {
                    if (i == (pCurrAddresses->PhysicalAddressLength - 1))
                        printf("%.2X\n",
                        (int)pCurrAddresses->PhysicalAddress[i]);
                    else
                        printf("%.2X-",
                        (int)pCurrAddresses->PhysicalAddress[i]);
                }
            }

            printf("\tFlags: %ld\n", pCurrAddresses->Flags);
            printf("\tMtu: %lu\n", pCurrAddresses->Mtu);
            printf("\tIfType: %ld\n", pCurrAddresses->IfType);
            printf("\tOperStatus: %ld\n", pCurrAddresses->OperStatus);
            printf("\tIpv6IfIndex (IPv6 interface): %u\n",
                pCurrAddresses->Ipv6IfIndex);
            printf("\tZoneIndices (hex): ");
            for (i = 0; i < 16; i++)
                printf("%lx ", pCurrAddresses->ZoneIndices[i]);
            printf("\n");

            printf("\tTransmit link speed: %I64u\n", pCurrAddresses->TransmitLinkSpeed);
            printf("\tReceive link speed: %I64u\n", pCurrAddresses->ReceiveLinkSpeed);

            pPrefix = pCurrAddresses->FirstPrefix;
            if (pPrefix) 
            {
                for (i = 0; pPrefix != NULL; i++)
                    pPrefix = pPrefix->Next;
                printf("\tNumber of IP Adapter Prefix entries: %d\n", i);
            }
            else
                printf("\tNumber of IP Adapter Prefix entries: 0\n");

            printf("\n");

            pCurrAddresses = pCurrAddresses->Next;
        }
    }
    else 
    {
        printf("Call to GetAdaptersAddresses failed with error: %d\n",
            dwRetVal);

        if (dwRetVal == ERROR_NO_DATA)
            printf("\tNo addresses were found for the requested parameters\n");
        else 
        {
            if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
                FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                NULL, dwRetVal, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                // Default language
                (LPTSTR)& lpMsgBuf, 0, NULL)) 
            {
                printf("\tError: %s", lpMsgBuf);
                LocalFree(lpMsgBuf);
                if (pAddresses)
                    HeapFree(GetProcessHeap(), 0, pAddresses);
            }
        }
    }

    if (pAddresses) 
    {
        HeapFree(GetProcessHeap(), 0, pAddresses);
    }

    return;
}
   根据此处的提示,**估计Windows API内部比较喜欢实用HeapAlloc去动态申请堆内存,所以此处决定试试HeapFree去释放放大器组件回调上来的buffer内存。**经测试,使用HeapFree是有效的,使用新版本测试,每次发桌面共享停止后不再有明显的内存泄漏了,那基本确定就是没有释放回调出来的buffer内存引起内存泄漏的。

2.4、为啥MagSetImageScalingCallback接口设置的回调函数回调上来的buffer需要外部释放呢?

   在MSDN的MagSetImageScalingCallback函数说明页面:

已经显示MagSetImageScalingCallback函数被微软废弃了,应用程序不要再使用了。但对于桌面图像采集,必须要用到这个接口去设置回调的,通过设置的回调将采集到的桌面图像回调上来的。至于回调函数回调上来的buffer需要外部释放的问题,不知道是否与这点有关系。

2.5、解决了放大器模式下的内存泄漏,但WebRTC相关模块还是有小的内存泄漏

    但详细测试下来,发起桌面共享并停止后还是有轻微的内存泄漏,每次大概泄漏5MB左右,用以前的老版本测试也存在同样的问题,每次泄漏5MB左右。这应该是开源的WebRTC库中有内存泄漏,应该和使用放大器模式没关系,因为老版本的桌面共享使用的不是桌面共享模式。这个5MB的泄漏,可以容忍,不会直接引发问题,但这是个隐患。比如客户长时间电脑不关机,软件长时间运行,如果多次发起桌面共享,内存泄漏会持续累计,比如发起100次桌面共享就会泄漏100*5=500MB的内存,这个影响就比较大了。

   但开源WebRTC内部的代码比较复杂,排查起来比较困难,等后面有时间的时候再去详细研究,目前这个放大器模式引发的明显内存泄漏算是告一段落了。

3、最后

   之前在遇到上述问题时,在网上一直也没搜到有用的信息,这里将使用magnification放大器方式遇到的这两个典型的问题做个详细的总结,以便给大家提供一个借鉴或参考。
 

本文转载自: https://blog.csdn.net/chenlycly/article/details/131146506
版权归原作者 dvlinker 所有, 如有侵权,请联系我们删除。

“开源WebRTC库放大器模式在采集桌面图像时遇到的DPI缩放与内存泄漏问题排查”的评论:

还没有评论