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

news2025/1/24 22:35:30

目录

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

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

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

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

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

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

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

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

3、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N4P3https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N4P3https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具案例集锦(专栏文章正在更新中...)icon-default.png?t=N4P3https://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中显示的是用户态虚拟内存。

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

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

       鉴于当前的操作场景,可能是桌面共享功能模块有内存泄漏,于是让测试发起桌面共享之前记录一下总的虚拟内存大小,然后发起桌面共享,然后再关闭桌面共享,看看内存有没有明显增长。经测试发现,发起共享时会申请内存,但停止共享后没有将申请的内存释放掉,所以这个操作有内存泄漏。后来使用Windbg分析内存泄漏,分析出发生泄漏内存的函数调用堆栈就指向桌面共享的模块代码中。具体如何使用Windbg分析内存泄漏,可以参见我之前写的文章:
使用Windbg定位Windows C++程序中的内存泄漏icon-default.png?t=N4P3https://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放大器方式遇到的这两个典型的问题做个详细的总结,以便给大家提供一个借鉴或参考。
    

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

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

相关文章

4年经验去面试21k测试岗,看到这样的面试题我还是心虚了....

我是着急忙慌的准备简历——4年软件测试经验&#xff0c;可独立测试大型产品项目&#xff0c;熟悉项目测试流程...薪资要求&#xff1f;4年测试经验起码能要个21K吧 我加班肝了一页半简历&#xff0c;投出去一周&#xff0c;面试电话倒是不少&#xff0c;自信满满去面试&#…

Highcharts for Python crack

Highcharts for Python crack   Aligned the API with Highcharts Core v11.1. In particular, this includes:   Added AccessibilityPoint.description_format property.   Added support for .legend_symbol to plot options and series options.   Added .border_…

6月10日两练来了!

今天是2023年6月10日&#xff0c;每日两练来了&#xff01; 一。围城 题目 编程星球上有一片100*100的空地&#xff0c;啊哈沃德在此处建立了N座发电站&#xff0c;每座发电站占地位置1*1格。 为了防止源码巨人的攻击&#xff0c;啊哈沃德需要在所有发电站外围修建电网&#x…

redis第九章-Redis队列Stream、Redis6多线程详解

Redis队列Stream 前置说明&#xff1a;Redis5.0 最大的新特性就是多出了一个数据结构 Stream&#xff0c;它是一个新的强大的支持多播的可持久化的消息队列&#xff0c;作者声明 Redis Stream 地借鉴了 Kafka 的设计。 Redis Stream 的结构如上图所示,每一个Stream都有一个消息…

【浅谈DBA 最重要的素质---读书笔记】

&#x1f448;【上一篇】 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 【下一篇】&#x1f449; &#x1f53b;【来自DBA大佬的见解1】 对于一个准备进入 DBA 领域的人&#xff0c;我希望他勤奋、严谨、具有钻研精神及独立思考能力。…

Android应用程序进程的启动过程

Android应用程序进程的启动过程 导语 到这篇文章为止&#xff0c;我们已经简要地了解过了Android系统的启动流程了&#xff0c;其中比较重要的内容有Zygote进程的启动和SystemService以及Launcher的启动&#xff0c;接下来我们将要学习的是Android应用程序的启动过程&#xff…

Python爱好者的自我修养(1):简单输入与输出

Python简单输入与输出 1.输出1.1 简单输出1.2 转义字符1.2.1 定义1.2.2 常见的转义字符用法 2.输入3.温馨提示 终于…… 终于…… 我开始玩Python了 &#xff08;不是C不学了哈&#xff0c;C还是照更~&#xff09; 今天先来简单讲下输入和输出 1.输出 1.1 简单输出 输出的函…

【一篇让你学会】Web接口测试工具--Jmeter

关于Jmeter性能测试工具不再过多介绍。如果你要学习软件性能测试&#xff0c;那么多少应该会对它有所耳闻。 强烈建议阅读官方文档学习&#xff1a;http://jmeter.apache.org/index.html 还有比这个更权威更全面的介绍Jmeter工具使用的么&#xff1f; 不过&#xff0c;此处要介…

Win7批量执行Python文件

问题背景 平时都是用Pycharm跑代码&#xff0c;但是每次都需要在Configuration里修改Parameters&#xff0c;跑完一个才能重新修改跑下一个&#xff0c;很不方便&#xff0c;于是决定借助.bat文件实现批量执行。 困难一 电脑存在cmd闪退问题&#xff0c;之前一直逃避懒得解决…

ExtractOfficeContent: 提取Office文件中文本、表格和图像

引言 最近有空写了一下这个库&#xff0c;用来提取Office文件中的文本和图像内容&#xff0c;用作后续整理训练语料使用。最新更新请移步&#xff1a;Github Extract Office Content Use Installextract_office_content$ pip install extract_office_contentRun by CLI. Ext…

STM32——04-初识STM32单片机

什么是单片机&#xff1f; 单片机&#xff08; Single-Chip Microcomputer &#xff09;是一种集成电路芯片&#xff0c;把具有数据处理能力的中央处 理器 CPU 、随机存储器 RAM 、只读存储器 ROM 、多种 I/O 口和中断系统、定时器 / 计数器等功 能&#xff08;可能还包括显示驱…

day49_mybatis

今日内容 1 引言 2 MyBatis介绍 3 环境搭建 4 入门演示(MyBatis开发步骤) 5 CRUD 6 配置细节 一、引言 1.1 复习 第一阶段(JavaSE) java基本语法流程控制面向对象常用类集合异常IO多线程JDK新特性 第二阶段(JavaWeb) 前端 htmlcssjs --> JQueryboostraplayui 页面元素内置模…

攻防世界—file_include

打开之后发现是一段php代码 可以看出这是段代码有文件包含漏洞 下面是学习部分。。。着急看题解继续往下滑。。。谢谢。。。 文件包含漏洞&#xff08;File Inclusion Vulnerability&#xff09;是一种Web应用程序常见的安全漏洞&#xff0c;也是攻击者常用的攻击手段之一。这种…

一文讲完Java常用设计模式(全23种)

文章目录 介绍设计模式的六大原则一、创建型模式1、单例模式&#xff08;Singleton Pattern&#xff09;1&#xff09;饿汉式2&#xff09;懒汉式&#xff0c;双检锁3&#xff09;静态内部类4&#xff09;枚举 2、原型模式&#xff08;Prototype Pattern&#xff09;3、工厂模式…

基于Java+SpringBoot+Vue前后端分离教学资源共享平台系统

博主介绍&#xff1a;✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

10:00面试,10:08就出来了 ,问的实在是太...

从外包出来&#xff0c;没想到算法死在另一家厂子 自从加入这家公司&#xff0c;每天都在加班&#xff0c;钱倒是给的不少&#xff0c;所以也就忍了。没想到8月一纸通知&#xff0c;所有人不许加班&#xff0c;薪资直降30%&#xff0c;顿时有吃不起饭的赶脚。 好在有个兄弟内…

如何利用google的protobuf设计、实现自己的RPC框架

一、前言 这篇文章我们就来聊一聊 RPC 的相关内容&#xff0c;来看一下如何利用 Google 的开源序列化工具 protobuf&#xff0c;来实现一个我们自己的 RPC 框架&#xff0c;内容有点长&#xff0c;请耐心看完。 序列化[1]&#xff1a;将结构数据或对象转换成能够被存储和传输&…

有趣的机器人工具坐标系

当机器人旋转轴上的夹爪中心不在旋转轴中心时&#xff0c;如何让旋转轴围绕夹爪中心旋转&#xff0c;这就是工具坐标系&#xff0c;怎末实现呢&#xff1f;机器人都是建立工具坐标系实现&#xff0c;这是什么原理&#xff1f;我们来探索一下&#xff1a; 世界上的有些特例让人…

【AIOT】手势捕捉调研

title: Data Glove Record date: 2020-06-06 20:40:13 author: liudongdong1 img: https://gitee.com/github-25970295/blogImage/raw/master/img/gloves-1268930__340.webp reprintPolicy: cc_by cover: false categories: AIOT tags: Sense 动作捕捉(Motion capture)&#x…

程序员凡尔赛,工作三年晒出5月工资条,直言加班太累了

最近有工作3年的程序员晒出自己9月份的工资条&#xff0c;并直言加班太累了。 从工资条上可以看到&#xff0c;这个收入确实不算低&#xff0c;才3年时间&#xff0c;月工资就已经到了二万五了&#xff0c;这个工资已经可以击败绝大多数行业了。 不过二万五只是税前工资&…