线下场景-内存(Android profiler + MAT + adb)

如果说前两节对应用性能优化幅度有限的话,那么本篇内存则直接关系到应用的生死存亡。
好的优化可以让死亡边缘的应用起死回生,避免内存泄漏及OOM。
内存泄漏一般是长生命周期的对象持有短生命周期对象的引用,当短生命周期完成使命要被资源回收时,GC Root发现对象可达,所以并不回收,如果这样的情况发生很多,就容易造成内存浪费,严重时导致OOM。形象的说就好比,在餐厅吃饭,顾客点了一餐,实际上吃完了饭,但是手还端着碗没放开(持有碗的引用,占用内存), 服务员(GC)看到后认为其没吃完饭,所以本将收回碗筷结果就不收了,顾客吃完了,没吃饱,又点了一餐,吃完又没松手(之前的摞在一起),来回几次后,餐厅的碗不够用了,,,虽然不够准确,但也差不多是这个意思。

图片1.jpeg

这里我们将使用多种手段揪出内存中的“病原体”, 涉及到 Android profiler Mem 和 Mat 以及 adb相关命令的使用。

一.Android profiler

Android Profiler网上教程太多了,包括官网也有详细介绍,常规的就不多说了,这里想给大家说下基于Android Profiler的内存优化的思路。

图片2.png

如图,Profiler的内存分析页面主要有两个功能按钮,一个是heap dump,一个是record,它们有什么区别呢?

Heap Dump有个官方的中文名叫堆转储(重要概念后面还会用到),不能指定时长,自动收集几秒的内存分配情况,保存了当前Java堆上所有的内存使用信息,能够完整的反映虚拟机当前的内存状态,并且还有内存泄漏的直接提示;它的文件格式是.hprof。

Record 用于记录内存分配,可以自由控制时长,但功能没有dump全面,不能直接查看内存泄漏。并且在Android 7.1以上版本时是没有这个按钮的,它的文件格式是.alloc。

我们关注 heap dump就好。
先使用dump快速查看内存的大体分配情况,以及是否有内存泄漏情况。
点击dump后生成如下视图(点击dump时会执行一次GC,内存也会稍微升高,这是正常现象)

图片3.png

可以看到,1处提示有14处内存泄漏的地方,我们在2处切换到”show activity/fragment Leaks”,查看页面导致的内存泄漏,3处显示了这些造成内存泄漏的fragment,选中第一个,在4处显示了它的所有实例,5处显示了它们的内存分配。
这里有4列,分别解释下它们的含义
Depth : 从任意 GC 根到选定实例的最短路径。
Native Size: 从 C 或 C++ 代码分配的对象的内存
Shallow Size: 对象本身占用内存的大小,不包含其引用的对象。这里可以看到6个实例它们的Shallow size都一样,因为创建fragment的动作都是一样的。
Retained size: 是该对象自己的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和(也可以理解为本身对象内存加上成员变量的内存)。换句话说,retained size是该对象被GC之后所能回收到内存的总和。这里用一个图来描述更为直观:

图片4.png

把内存中的对象看成图中的节点,并且对象和对象之间互相引用。这里有一个特殊的节点GC Roots,这就是reference chain的起点。从obj1入手,上图中蓝色节点代表仅仅只有通过obj1才能直接或间接访问的对象。因为可以通过GC Roots访问,所以左图的obj3不是蓝色节点;而在右图却是蓝色,因为它已经被包含在retained集合内。

所以对于左图,obj1的retained size是obj1、obj2、obj4的shallow size总和;右图的retained size是obj1、obj2、obj3、obj4的shallow size总和。obj1的Depth为1。
在左图中,obj2的retained size是obj2和obj4的shallow size的和;在右图中是obj2、obj3和obj4的shallow size的总和。Obj2的Depth为2。
清楚了基本知识后,我们继续往后看,点击6处的reference可查看所有引用当前NewsListFragment的对象。

图片5.png

随便选择一个,在红框处右键jump to source,定位到代码,如下:

图片6.png

代码跳转到了NewsListFragment的父类BaseFragment中的内部类SpaceItemDecoration,可以看到它是非静态的,在运行时会持有NewsListFragment的引用,我们将其改为static的,再运行应用重新dump,这个引用不存在了:

图片7.png

这里只是介绍解决问题的思路,并不是说这个SpaceItemDecoration就是内存泄漏的元凶,我这里的NewsListFragment是常驻的并不会销毁,只会隐藏和显示,所以即使有个多个不同的对象引用它也是没问题的,proflier这里提示leak,也只是从对象引用的角度来说,它并不知道我们的实际意图。所以仅做参考,并不是说有leak了就一定是问题,必须解决。当然如果你的Activity或者Fragment已经关闭了,在dump中还依然存在实例,那就是内存泄漏,需要解决。

这里放出官方对我们的建议:
在您的堆转储中,请注意由下列任意情况引起的内存泄漏:
1.长时间引用 Activity、Context、View、Drawable 和其他对象,可能会保持对 Activity 或 Context 容器的引用。
2.可以保持 Activity 实例的非静态内部类,如 Runnable。
3.对象保持时间比所需时间长的缓存

Tips
1.我们也可以通过命令导出.hprof文件。
adb shell am dumpheap pid /data/local/tmp/x.hprof

二.Android profiler + MAT

Dump的进阶使用方式,先记录一次,频繁操作一段时间后(可以使用monkey或者按键精灵或者其它自动化测试的工具,实现压力测试),再dump一次, 把两次的结果放到MAT中对比,从而清楚的了解到内存的变化情况。这种方式比只dump一次更加合理和直观。

图片8.png

点击保存按钮可以把内存信息保存为.hprof文件,这个文件需要转成MAT支持的格式(或者说标准的Java hprof格式,主要是版本号不一致),使用SDK/platform-tools里面hprof-conv.exe这个命令,如下:
hprof-conv old.hprof new.hprof
将第一次和第二次的.hprof文件都转换完成后,把这两个文件导入到MAT中(直接拖拽即可)。

Tips:MAT这里稍微科普下
MAT是Memory Analyzer的简称, 是基于Eclipse开发的(这个老Androider应该都用过吧)
官网:http://www.eclipse.org/mat/

导入时,选中Leak Suspects Report 再finish即可。

图片9.png

打开后,先选择after的那个hprof,然后选择 1 overview,2 Histogram(直方图)

图片10.png

再点击3处按钮和 before比较。

图片11.png
图片12.png

比较结果会直观列出Objects(对象数量) ,Shallow内存的变化情况,可以看到,after比before新增加了很多对象和内存。
(注意:跑monkey时尽量保证主Activity不会重建,否则会造成增长过多的假象,影响准确性)。

图片13.png

默认是以类的方式排列,还可以使用包名的方式排列:

图片14.png

这样就可以很直观的看到自己应用内存的变化了。
下面还有第二种比较方式,可以更全面友好的显示内存变化的情况,
在Histogram tab页下点击1处的Navigation history,然后在OverviewPane最后一个histogram上右键Add to Compare Basket

图片15.png

两个都添加过来后(before在上),然后点击红框处的红色叹号(Compare the results) 进行比较

图片16.png

比较结果如下,相比第一种比较方式,这种的列数更多,把before和after的数据都列了出来。

图片17.png

还可以切换其它的视图,比如用百分比显示变化的情况。

图片18.png

如果有增长特别多的类,那么有可能存在内存泄漏,可以选择with incoming references查看引用它的对象。

图片19.png

关于outgoing和incoming备注里还会介绍。
以上是自己手动对比内存的变化,如果你想偷懒,想快速查看是否存在内存泄漏的方法,MAT提供了一个名字很霸气的功能,叫做:
Leak Hunter(泄漏捕手)
两种方式打开:

图片20.png
图片21.png

打开后,长这样:

图片22.png

按泄漏的大小排序,查看其中一个问题的detail:

图片23.png

点击Refercenec Pattern里的类名-List objects-with incoming references 查看谁引用了它。(有的不一定有Refercenec Pattern)

图片24.png

然后在这里列表里查看详细的引用关系:

图片25.png

除了泄漏捕手,MAT还提供了大对象的查看方法,这个也是我们的优化方向。
在overview页面选择Top Consumers查看应用中的大对象并按照大小排序,以饼图的形式展示。

图片26.png
图片27.png

可以看到这个byte[]大对象里面包含了Glide和CircleImageView中的bitmap。
遗憾的是这里并不能像AS proflier中那样直接调转到代码,因为这里是脱离了项目的代码环境。不过也足够详细了,毕竟应用内所创建的java 对象这里都会一个不拉的显示出来:

图片28.png

展开CircleImageView后可以看到,personFragment中有个名为 mHeadIv的CircleImageView类型的对象,它占用的内存空间分别是 Shallow Heap 304字节 , Retained heap 2544字节。

图片29.png

好了,MAT的介绍就到此为止。仅仅只是抛砖引玉,MAT的强大远远不止这些,比如它支持OQL(Object Query Language),你可以查某个类的所有实例甚至是按地址搜索某个对象。

到这里,我们了解到了 4种分析内存的方法,总结如下:
1.单纯Android Profiler Mem: 最便利的方法,可以直接查看leaks情况,功能强大,可跳转到源码(首选)
2.MAT 对比.hprof文件: 操作稍微繁琐,但很直观,也更贴近真实场景
3.MAT leak hunter: 快速查看可能的内存泄漏(感兴趣的人可以和profiler中的leak对比下,看看有没有异同)
4.MAT top consumer: 快速查看大对象
可以说使用了Profiler 和MAT, 简直就是中西结合,药到病除。

三.adb命令

有人说了,前面介绍的这些方法都太麻烦,还有没有简单点的?我就想看下内存占用的情况。
当然有了,它就是adb命令(挺适合没有as的测试人员使用),本节介绍五种方式。

第一种:procrank
adb shell su (需要赋予超级权限,否则可能报错)
procrank -p (按pss排序)

图片30.png

VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)
PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

第二种:dumpsys
adb shell dumpsys meminfo 包名(后面不加包名则是查看所有的):

图片31.png

不仅能看应用的各项内存指标,还能看到创建了多少个view和activity(1处),甚至还能看到有哪些数据库,是不是很强大(2处)。

第三种:查看系统内存情况:
adb shell cat /proc/meminfo(一般看available即可)

图片32.png

第四种:showmap,可以查看每个so库占用的内存大小
adb shell showmap pid

图片33.png

第五种:smaps
查看进程的虚拟内存空间
adb shell run-as 包名 cat /proc/pid/smaps
参考:https://www.cnblogs.com/bravery/archive/2012/06/27/2560611.html

adb总结: 我们介绍了5种查看内存的方式,分别是
Procrank、dumpsys 、proc、showmap、smaps(其实也都是基于Linux的),操作方便,一行命令即可,前四个适合普通测试人员使用。smaps入手门槛较高,适合进阶使用。以上命令在末尾处添加 > xxx.log 都可以把信息保存到文件中。

总结:

本章节我们主要介绍了应用稳定性的重中之重-内存,恼人的OOM和内存泄漏就常常是因为内存使用不当而产生(严格来说,造成oom的原因还可能有线程数量过大、Fd(文件描述符)数量过大、虚拟内存不足等),几乎是最影响应用稳定性的部分了。
我们的解决办法就是dump出hprof内存快照文件,通过两种工具AS和MAT进行分析得出结论,但是这部分只是针对于Java层的堆内存,除了这个还有native的内存,主要和so库相关,不能够直接通过现成的方法获取到相关信息,一般是通过hook系统库libc.so的malloc和free函数去获取到native层所有的内存的申请和释放操作,再结合smaps(进程虚拟内存信息)文件进行分析,将相对独立的内存信息追溯到业务堆栈从而定位到具体方法(这也是为什么bugly中的native异常需要提供so符号表才能定位的原因)。当然也有一些现成的工具,比如 HeapSnap,malloc_debug,asan,valgrind。
这部分涉及到 Linux动态链接等底层知识,我们没法过多的介绍。
感兴趣的小伙伴可以参考 腾讯的native内存分析与监控:
https://mp.weixin.qq.com/s/0cF5Q6_LXrkLAdjkXIwrVQ
以及西瓜视频的线上native内存的泄漏监控Raphael:
https://github.com/bytedance/memory-leak-detector

备注:
1.MAT中的引用关系
list objects -- with outgoing references : 查看这个对象持有的外部对象引用(引用谁)。
list objects -- with incoming references : 查看这个对象被哪些外部对象引用(被谁引用)。
show objects by class -- with outgoing references :查看这个对象类型持有的外部对象引用
show objects by class -- with incoming references :查看这个对象类型被哪些外部对象引用

2.三方分析Heaphero
如果懒得自己去分析内存问题,其实可以交给三方机构HeapHero(应该还有很多类似的),免费还不用注册,只需要提交dump文件即可(知道它为什么叫堆转储了吧,其实就是把瞬间的内存堆信息转化为可存储的持久化信息,有了这个堆信息就像是体检报告,你可以拿着到处问医生开处方了 )
是不是很方便?但前提是英语要过关,,,
https://heaphero.io/heap-index.jsp#header

3.Android 8.0之后 图片内存的申请由Java层切换到了native层

4.这里需要注意,物理内存不足时,会引起 onLowMemory 回调;当虚拟内存占用超过最大限制的 90% 时,触发为低内存告警。超过最大限制则直接触发 OOM,因此我们也需要监听虚拟内存的占用情况。

一些可供参考的文章:
MAT: Incoming Vs Outgoing References
https://cloud.tencent.com/developer/article/1530223

Java堆:Shallow Size和Retained Size
https://blog.csdn.net/kingzone_2008/article/details/9083327

内存分析诊断系列-理解heap dump
https://blog.csdn.net/u012811805/article/details/106547389

官方教程
https://developer.android.google.cn/studio/profile
https://developer.android.google.cn/studio/command-line/dumpsys

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容