如图,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的对象。

图片7.png

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

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

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即可。

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

图片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排序)

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

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

查看进程的虚拟内存空间
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