最新动态
全民K歌内存篇3——native内存分析与监控
2024-11-09 21:11

《全民K歌内存篇3——native内存分析与监控》

全民K歌内存篇3——native内存分析与监控

一、背景

在2020年的上半年,我们在用户反馈后台发现闪退、白屏问题不断增多,这些问题严重影响用户体验。观察Crash监控平台发现Crash率也在逐步升高,其中Native层的Top1的crash堆栈信息如下:

这个Crash在整体的crash中占比很大,通过这个堆栈信息,发现并没有明显的指向哪个业务代码。此时,把发生Crash时的内存信息上报到后台,分析发现:Crash发生时虚拟内存非常接近4G

以及日志信息:

为此,我们通过脚本模拟用户在不同业务场景反复进出运行,发现内存水位在逐步上升,当接近4G时,复现了该crash。在前期,我们解决了一些专项测试同学提出的内存问题,但是内存水位依然维持较高状态,Crash率也没有得到有效缓解。

经过粗略分析,发现应用内内存占用的大头在native层。那么,该如何系统的分析和解决Native内存问题呢?

2.1 为什么会内存不足

Android系统是基于Linux之上的,在内存管理上基本一致,但也有差异。简化后如下图所示:

在Android手机上,内存的使用受操作系统和内存硬件设备限制,这里以32位的应用运行在8G内存、64位系统的手机上为例,总结几点如下:

2.2 内存分布情况分析

通过命令,可以获取到系统的smaps文件,该文件详细记录了应用虚拟内存的分配情况。分析可知:左侧 代表的是一个虚拟内存区块,右侧 表示这一块内存由JVM虚拟机所申请。如下图可知:虚拟内存主要由虚拟机、系统库文件、应用dex文件,以及业务调用libc.so的malloc所申请。

接着,对smaps文件的pss数据(实际占有的物理内存)进行统计分析,如下图是K歌直播场景内存分布情况:

分析发现,业务申请的内存主要分布在两部分,即:

3.1 Native内存分析工具对比

1)、内存总量分析工具

在团队内,通常用Perfdog来检测应用程序内存问题,这个工具的实现原理是不断的读取系统的内存值。通过Perfdog,我们只能观察内存总量,去判断是否存在内存异常问题。可是,Perfdog无法提供有效的堆栈信息,帮助开发定位问题所在。如下图所示,是在测试K歌直播上下滑场景过程中的内存数据,发现nativePss在不断上涨。

2)、内存分量分析工具

经过调研发现,Android分析native层内存工具,有Android原生支持的,也有开源的。整理如下表:

对比上述工具,结合使用经验,小结如下:

3)、 所期望的Native内存分析工具

上述工具团队内开发、测试都有尝试去使用,但是在使用和分析问题上都存在一些困难,不能高效准确的定位问题。基于现状以及对当前工具调研情况,我们计划基于loli_profiler来自建工具。希望工具能够在开发、测试、灰度以及外网阶段都能发挥作用,并能够结合业务场景,精准定位问题,来帮助我们高效、可持续的优化内存问题。

3.2、Native 层的内存管理机制介绍

这里先简单的介绍下native层内存相关的基础知识。

1)、如何申请和释放内存

如下图,“C++ Application” 指的是业务层,大多数情况下,业务是通过和函数来申请和释放,或者是和关键字,它最终的也是由和的函数来实现。更高阶的实现方式,可以直接调用更底层的系统接口,如。为了更好的管理内存,业务代码极少去直接调用mmap来申请内存。在我们的业务场景内,内存的申请和释放通常最终都是由malloc和free来实现的。

2)、Native内存申请流程

如下图,当应用APP申请内存时,申请的是虚拟内存。当应用访问这块内存并进行写操作时,如果物理内存还未分配则会发生缺页中断并触发分配物理内存。在实际分配物理内存时,是以“页”为单位,每页通常是4KB的内存空间。完成分配后,在MMU模块中的PageTable记录了每一页的虚拟地址和物理地址映射关系。

3.3、So库加载、申请内存的核心流程

1)、实验测试

我们先来做一个测试demo,如下,在libkmemory.so内通过调用malloc函数,申请176k的内存空间 (176k,随机测试的内存大小,建议不要太小,以便申请一块新的虚拟内存区块,方便后面对smaps文件的观察):

运行demo之后,通过,读到smaps文件如下:

可以发现,libkmemory.so在虚拟内存的空间区域为,起始地址也称基址为。

接着,对比调用前后对比smaps文件,发现新增了一段如下:

解读这一段的主要内容:

以上可以验证,由申请的内存在smaps文件中记录在的虚拟内存空间内。

2)、流程分析

那么,so库是如何在手机上运行的呢?简化流程如下图:

由上可知,如果要定位内存申请是由具体那个业务代码所申请,实际也就是要将 “[anon:libc_malloc] ”内的内存和“libkememory.so” 内的 函数关联起来。

3.4、实现So库内存分析的基本方案

1)、如何分析So库内存的申请和释放

如下图,通过hook系统库libc.so的和函数,业务所有的申请和释放操作也就都变成可见了。作为一个内存检测工具,我们需要做以下几个事情:

2)、xhook基本原理

xhook是一个开源的plt hook 方案,基础原理内容涉及众多,笔者也多次阅读相关资料,这里仅简单讲述关键之处。

基本概念

Runtime linker: 动态链接器,负责将so加载到内存中,处理符号解析、地址的计算等操作;

PLT: Procedure linkage Table 程序链接表,实际是一小段代码,这段代码能够触发函数地址的计算以及跳转到对应函数上;

GOT:Gloabal Offeset Table,它是一张表,除了前3个特殊的,其余项都保存着“符号(函数或者变量)名”与“入口函数地址指针”的对应关系;

跳转步骤

如下图所示, 函数内要执行 函数来申请内存,此刻需要找到的函数入口地址。会经历下述几个步骤:

hook方案

观察上图右侧,GOT表可以简化为键值对,key存储的是函数名,value对应的该目标函数地址的指针地址。通过解析GOT表,拿到指针地址,通过这个指针地址,就可以访问指向的内存块,它实际存储的是函数的入口地址;所以,只需要将这个地址改为我们自己的函数的入口地址即可。

3)、获取堆栈地址以及还原函数名

如下图,左侧图是由smaps文件所得虚拟内存的分布情况。首先通过系统堆栈回溯获取的堆栈的指针地址 ,发现是落在了“libkmemory.so”的区域内,由此判定申请内存的函数在libkmemory.so内。然后根据libkmemory.so的基址计算出偏移地址,最后通过脚本自动实现指针地址还原为对应的函数名。这里的方案是首先根据有符号表的so导出的地址与符号的对应关系表,然后采用二分法遍历这一张关系表,查找到最邻近偏移地址所对应的函数名。

3.5、So库内存监控与分析流程

1)、数据采集

检测工具的客户端部分作为一个SDK模块,集成到app中。SDK提供了接口由业务app来控制,接口主要有:启动开关、检测so列表、业务打点。启动开关可以在APP运行的任何阶段控制打开检测,so列表配置了需要检测的so。业务打点是为了精准的将当前运行状态与分析数据关联起来,如下图右侧。是统一将Activity的创建和销毁时机写入记录文件中,分析时就可以按照打点统计这个Activity创建和销毁这段时间内的内存申请和释放情况。当然,也可以将其他场景添加业务打点,比如K歌直播歌房上下滑的每次滑动事件。当需要检测时,只需要开启开关运行业务场景即可,采集的数据自动写入设定的目录文件下。

2)、数据分析

整体内存统计分析

获取到保存在手机上的内存记录文件,通过统计分析业务打点"LiveActivity_onCreate"和"LiveActivity_onDestroyed"这段时间内所有的内存申请和释放记录,我们可以得知这段时间内每个so的内存申请次数和释放次数,以及在这段时间内未释放的内存大小,对应的业务so以及函数。

例如: 直播Activity创建和销毁之间内存申请和释放情况如下:

从以上数据中,可以进一步将堆栈进行归类,分析出哪些函数申请的次数最多,哪些函数申请的内存最多,哪些函数申请了大内存。由此,能够对so库的内存申请释放情况有一个比较深入的分析。

内存占用分布分析

通过在每个activity和fragment的生命周期“打点”(写入一条记录到文件中),以及预埋的业务“打点”,然后统计从“开始点”到每个“打点”之间内存的未释放内存量,就可以绘制检测场景运行过程中各个so的内存占用情况分布图。由此,我们对native的内存分布不再是停留在整体的“nativePss”这个内存总量数值,而是可以清晰明确的观察到具体每个so的内存占用情况。下图是直播上下滑场景so库内存分布,可以观察到某个音频相关的内存占用峰值达到20M以上,退出直播后均完全释放。

3)、整体框架

如下图,将工具SDK集成到应用apk中。运行时,开启检测开关,配置相关参数,如要监控的so列表,内存阈值等。开启后采集到数据后,将记录写入手机文件。然后将文件上传到质量平台,质量平台通过脚本自动化分析,发现内存问题并自动提单到tapd系统,推动开发侧修复。

3.6、 发现问题案例

1)、案例一:So库内部的泄漏问题

通过分析发现,libxxx.so内存占用量持续上涨,20分钟约增长3M,大概率存在内存泄漏问题。

跟进工具统计得到如下堆栈信息:

随即找libxxx.so 负责人查看对应代码,发现确实存在内存泄漏问题。了解到这个问题是K歌打印日志的操作导致,也就是说在K歌运行过程中因为打日志而一直泄漏,连续运行时间越长,泄漏越多,触发OOM的概率也就大大提高。

2)、案例二:业务层导致的泄漏问题

如下图,在我们的自动化内存水位检测时发现歌房业务上下滑50次后,native内存峰值相对历史版本明显偏高。那是什么问题导致的呢?

通过工具检测发现其中一个负责音频处理的so库滑动50次后,泄漏了40M,泄漏的函数是一些初始化操作。最终定位问题是由于每次上下滑切换房间后,打分业务会重新初始化操作,但是在退房时却没有进行释放操作。

通过以上案例,自建工具的优势就很明显的展示出来了。在前面的内存水位图,测试同学已经完成了自动化测试流程,可以帮助我们发现问题,只是还不能提供更多有效信息,帮助我们快速定位并解决问题。其实,测试同学只需要在运行自动测试的过程中,打开so库内存检测开关,就能够得到每个so的内存水位细分图,并附带堆栈信息。这样的做的好处,就是极大节约测试、开发人力成本,提升研发效率。

3.7  在线监控方案探索

基于这段时间的经验,工具的能力以及优势都得以充分验证。但是,如何充分发挥工具的作用?如何在不增加人力成本的情况下覆盖更多场景?如何可持续的监控内存问题?这是我们所思考的。所以在想是否可以实现在线监控。对于在线监控,必须要考虑性能问题,工具面临的两大性能瓶颈如下:

基于对用户性能最低影响的同时又能拥有在线监控能力的考虑,我们提出了一个具备可行性的在线监控方案,如下图:

在不获取堆栈的情况下如何将内存的申请和释放归类到各自的so呢?这里有一个取巧的办法,如下图所示。在hook每个so的malloc和free函数时,让不同的so对应不同的hook函数,这样就可以单独记录各个so的内存分配情况了。估算统计直播场景运行20分钟,某音频处理相关的so产生约74万条记录,数据占用内存仅约4.5M。如果超过1M则将数据写入文件,IO操作也几乎无性能影响。

上述方案还在进一步探索完善中。

在治理内存问题的过程中,我们多次发现内存超出限制时是由于创建图片导致的。如下图案例,在客户端加载后,图片内存占用达到20M以上。

可见,应用内有许多不合理的图片,拉高了内存水位。所以对图片的内存申请展开分析。如下图,可以知道80%以上的用户的手机是8.0及更高系统版本,它的内存申请是在Native层的。因此,图片内存的分析重点放在8.0以及更高的系统版本上。

4.1、分析图片内存申请流程

如下图,在我们的业务中,一般会在布局的xml文件中定义图片,或者在代码中动态给imageview设置图片,或者是用引入的Glide组件来加载网络图片资源。基本的框架流程总结如下图所示:

再来看native层的具体实现:

分析源码,整理如下图。发现所有的图片创建最终都会走到的函数中,通过等内存分配器来分配内存并存储图片的像素数据。完成内存分配后,会创建一个对象,它持有的存储了内存地址。最后调用JNI层的函数,这个函数非常重要,在这一步中创建了Java层的对象。

4.2、检测图片bitmap的创建

分析bitmap的创建流程,总结得到了图片创建的关键函数,所以,我们只需要hook这个函数,就能够拿到每次图片创建时的的Java对象。通过该对象,可以获得图片的尺寸大小、内存占用大小,堆栈等信息,将这些信息上报到性能平台也就达到了检测与监控图片创建的目标。

这里hook的方案采用开源的inline-hook开源方案来完成。原理在本篇中不做描述。怎么查找到这个函数呢?

首先需要通过 拿到系统的so文件,然后通过 ,可以查找到对应的函数名,也就是我们要hook的函数,系统版本不同可能会有差异,注意兼容。

4.3、分析图片的销毁流程

1)、主动调用recycle销毁

在Java层通过对象调用它的方法,即可达到立即释放的目的。在native的具体实现如下,最终是在中调用释放申请的native内存。

2)、等待Gc触发自动销毁

在Java层的Bitmap构造方法内,会通过将Java的和native的对象注册到JVM。

JVM在GC过程中如果发现Java层的`bitmap`对象已经释放,随后也就会触发`delete` native层的`bitmap`对象,最终释放native层的内存。

对比上述两种图片销毁流程,值得注意的是,虽然android的注释文档中说明不需要主动调用bitmap的recycle方法来销毁bitmap,这是因为有上述GC自动触发销毁的保证。但是,如果已经非常明确这个bitmap已不再需要使用,主动调用recycle尽快释放可以帮助降低整体内存水位。

4.4、整体监控方案框架

我们将工具集成到应用中,通过配置网络开关来监控应用内图片的创建。然后把图片的相关信息,如尺寸大小、占用内存大小,调用的堆栈等信息上报到性能平台。后台通过信息的聚类,可以监控到如图片占用内存过大等异常问题。最后一键提bug单到tapd系统,推动业务开发来修复。

性能监控平台数据展示:

4.5、发现问题案例

通过图片检测工具,我们能够很明确得知道应用创建的bitmap占用内存大小,以及创建图片的堆栈信息。经过脚本自动统计和人工分析,主要归类未如下几类问题。

1)、原始图片过大

在解码图片的时候,图片的像素大小是影响图片bitmap占用内存的关键因素之一,所以缩小原图尺寸是减少内存占用的有效手段。这里有两种情况:

原始图片的尺寸大于View:图片的大小超过View的大小数倍时,而解码图片时按照图片尺寸来解码就很浪费了。(注:此类情况主要发生在非Glide组件创建图片的场景);如下案例当中,是直播业务里面的一个红包弹框,View的大小是 宽度255dp,对应382.5px,高度339dp,对应高度508.5px。缩小图片到View的尺寸后可降低到760K,减少内存约2M多。

在清晰度要求不高的场景下,可以适当的缩小原始原图,如下图的案例中,我们发现一张磨砂的模糊图片内存占用超过6.4M,通过优化,优化后将长和宽缩小原来的1/4,内存减少到原来的1/16,约400K,减少内存占用约6M。

2)、 相同图片重复创建

我们抓到了相同的图片由不同的堆栈调用来创建,这里逐一分析是否有优化空间,尽量复用来节约内存。如下,是直播场景内的背景图,可以发现有相同的图有三个不同大小的尺寸,其创建路径堆栈也不一致。修复后复用同一bitmap,内存可减少约5M。

3)、未及时recycle

在业务里,时常有bitmap拷贝行为,通过源bitmap对象获得变换后的bitmap对象,这里需要考虑源bitmap是否可以立即释放。

4)、Web动态框架问题

K歌集成了腾讯浏览器自研的hippy框架,它是一套类ReactNative的移动端跨平台解决方案。我们业务大量使用了该动态框架,甚至运营可以直接配置上架页面,这里时常存在超大超长的不规范图片。hippy加载网络图片时透传url给到客户端,最终的解码由客户端来实现。透传时没有把View的宽和高带给客户端,客户端最终以原始图片尺寸来解码。在原始图片尺寸大于View时,会浪费不必要的内存。这个问题曾经因为web端发了一张超大图片,并在灰度过程中发现OOM由此问题导致。如下案例中:图片尺寸为12300*480,占用内存达22.5M。这里浏览器团队已完成优化此类问题。

5)、图片引擎问题

问题1:我们图片引擎是集成的Glide,设置的默认RGB_565格式没有生效,查看源码是原始设计API在26以上默认到ARGB_8888。如下图所示,是直播全屏背景图,在RGB_565格式下,每个像素占用内存为2个字节,应该是1080x1902x2=4050KB,修复后能减少约4M的内存占用。

问题2:在Glide内部,当图比view小的时候,在ImageView的scaleType不是fitXY和centerInsider配置的时候,会根据View的尺寸来放大解码,从而拉高内存的占用。如下,是性能平台监控到歌房背景图,在房主未设置背景的情况下,会默认取房主头像来作为背景。而背景的View是全屏的,此时 view的尺寸是1440x3064,最后经Glide放大后的真正解码的bitmap会是3064x3064,最后背景图占用的内存高达30多M。修复后按照图片尺寸解码,仅1.56M,如果上面问题1修复可降低到0.78M。差距非常的大。

问题3:业务繁杂,长期处于不可见状态的页面加载的图片一直未释放,其实这里可以适时释放掉,以降低内存水位。

问题4:发现K歌内还有少量业务还在使用旧图片加载引擎,需要彻底删掉。

通过建设so库内存检测工具、图片检测工具,我们可以对native层动态申请的内存进行准确的检测。在这个过程中,发现并解决了不少问题,但也还存在许多地方待进一步研究与优化,主要有如下两个方面:

1)、图片缓存问题

通过工具检测,发现K歌大卡片、歌房、直播反复上下滑的等业务场景下,内存会不断增长,这些增长是由于图片缓存不断累积导致的。研究发现,Glide组件的缓存机制在我们的业务中,存在一些不合理性,比如没有缓存价值的bitmap会加入到缓存池,不能及时回收不在界面展示的bitmap。这里需要进一步探索优化,结合实际业务场景,不影响流畅度的前提下及时释放,降低内存水位。

2)、未知的内存申请

在歌房的练唱房场景内,反复切歌后NativePss会有1~2M的内存增长,这些内存当前的so库检测工具、图片检测工具均未能抓到申请的业务。也尝试使用AndroidStudio来检测,所抓到的都是在系统层面的堆栈信息,这些堆栈还是缺乏一些说服力。那么,这些内存到底是由谁申请的,目前还待研究分析。

在治理Native内存的过程中,通过建设检查工具,分析业务so库和图片的内存申请,就基本对当前业务native内存分配情况了如指掌。如下图分析案例中,我们可以非常直观的观察直播上下滑场景下native内存分配情况。

根据上图,分析如下:

在优化Native内存时,我认为主要抓住以下关键点:

1、聚焦优化目标:降低应用内存水位,修复内存泄漏、不合理申请等问题;

2、分析内存问题并优化:学习内存基础知识,建设so库,图片等检测工具,检测出内存到底由哪些业务占用,并给出有价值的堆栈信息,帮助业务同学定位问题,推动业务优化;

3、监控并持续改善:由于业务是在不停的迭代的,需要有一定的手段来持续监控内存问题,及时发现,推动解决。通过建设工具,完善流程,形成内存优化闭环。

    以上就是本篇文章【全民K歌内存篇3——native内存分析与监控】的全部内容了,欢迎阅览 ! 文章地址:http://sicmodule.kub2b.com/quote/251.html 
     资讯      企业新闻      行情      企业黄页      同类资讯      首页      网站地图      返回首页 企库往资讯移动站 http://changmeillh.kub2b.com/ , 查看更多   
发表评论
0评