如果说前两节对应用性能优化幅度有限的话,那么本篇内存则直接关系到应用的生死存亡。
好的优化可以让死亡边缘的应用起死回生,避免内存泄漏及OOM。
内存泄漏一般是长生命周期的对象持有短生命周期对象的引用,当短生命周期完成使命要被资源回收时,GC Root发现对象可达,所以并不回收,如果这样的情况发生很多,就容易造成内存浪费,严重时导致OOM。形象的说就好比,在餐厅吃饭,顾客点了一餐,实际上吃完了饭,但是手还端着碗没放开(持有碗的引用,占用内存), 服务员(GC)看到后认为其没吃完饭,所以本将收回碗筷结果就不收了,顾客吃完了,没吃饱,又点了一餐,吃完又没松手(之前的摞在一起),来回几次后,餐厅的碗不够用了,,,虽然不够准确,但也差不多是这个意思。
这里我们将使用多种手段揪出内存中的“病原体”, 涉及到 Android profiler Mem 和 Mat 以及 adb相关命令的使用。
一.Android profiler
Android Profiler网上教程太多了,包括官网也有详细介绍,常规的就不多说了,这里想给大家说下基于Android Profiler的内存优化的思路。
如图,Profiler的内存分析页面主要有两个功能按钮,一个是heap dump,一个是record,它们有什么区别呢?
Heap Dump有个官方的中文名叫堆转储(重要概念后面还会用到),不能指定时长,自动收集几秒的内存分配情况,保存了当前Java堆上所有的内存使用信息,能够完整的反映虚拟机当前的内存状态,并且还有内存泄漏的直接提示;它的文件格式是.hprof。
Record 用于记录内存分配,可以自由控制时长,但功能没有dump全面,不能直接查看内存泄漏。并且在Android 7.1以上版本时是没有这个按钮的,它的文件格式是.alloc。
我们关注 heap dump就好。
先使用dump快速查看内存的大体分配情况,以及是否有内存泄漏情况。
点击dump后生成如下视图(点击dump时会执行一次GC,内存也会稍微升高,这是正常现象)
可以看到,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之后所能回收到内存的总和。这里用一个图来描述更为直观:
把内存中的对象看成图中的节点,并且对象和对象之间互相引用。这里有一个特殊的节点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的对象。
随便选择一个,在红框处右键jump to source,定位到代码,如下:
代码跳转到了NewsListFragment的父类BaseFragment中的内部类SpaceItemDecoration,可以看到它是非静态的,在运行时会持有NewsListFragment的引用,我们将其改为static的,再运行应用重新dump,这个引用不存在了:
这里只是介绍解决问题的思路,并不是说这个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一次更加合理和直观。
点击保存按钮可以把内存信息保存为.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即可。
打开后,先选择after的那个hprof,然后选择 1 overview,2 Histogram(直方图)
再点击3处按钮和 before比较。
比较结果会直观列出Objects(对象数量) ,Shallow内存的变化情况,可以看到,after比before新增加了很多对象和内存。
(注意:跑monkey时尽量保证主Activity不会重建,否则会造成增长过多的假象,影响准确性)。
默认是以类的方式排列,还可以使用包名的方式排列:
这样就可以很直观的看到自己应用内存的变化了。
下面还有第二种比较方式,可以更全面友好的显示内存变化的情况,
在Histogram tab页下点击1处的Navigation history,然后在OverviewPane最后一个histogram上右键Add to Compare Basket
两个都添加过来后(before在上),然后点击红框处的红色叹号(Compare the results) 进行比较
比较结果如下,相比第一种比较方式,这种的列数更多,把before和after的数据都列了出来。
还可以切换其它的视图,比如用百分比显示变化的情况。
如果有增长特别多的类,那么有可能存在内存泄漏,可以选择with incoming references查看引用它的对象。
关于outgoing和incoming备注里还会介绍。
以上是自己手动对比内存的变化,如果你想偷懒,想快速查看是否存在内存泄漏的方法,MAT提供了一个名字很霸气的功能,叫做:
Leak Hunter(泄漏捕手)
两种方式打开:
打开后,长这样:
按泄漏的大小排序,查看其中一个问题的detail:
点击Refercenec Pattern里的类名-List objects-with incoming references 查看谁引用了它。(有的不一定有Refercenec Pattern)
然后在这里列表里查看详细的引用关系:
除了泄漏捕手,MAT还提供了大对象的查看方法,这个也是我们的优化方向。
在overview页面选择Top Consumers查看应用中的大对象并按照大小排序,以饼图的形式展示。
可以看到这个byte[]大对象里面包含了Glide和CircleImageView中的bitmap。
遗憾的是这里并不能像AS proflier中那样直接调转到代码,因为这里是脱离了项目的代码环境。不过也足够详细了,毕竟应用内所创建的java 对象这里都会一个不拉的显示出来:
展开CircleImageView后可以看到,personFragment中有个名为 mHeadIv的CircleImageView类型的对象,它占用的内存空间分别是 Shallow Heap 304字节 , Retained heap 2544字节。
好了,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 包名(后面不加包名则是查看所有的):
不仅能看应用的各项内存指标,还能看到创建了多少个view和activity(1处),甚至还能看到有哪些数据库,是不是很强大(2处)。
第三种:查看系统内存情况:
adb shell cat /proc/meminfo(一般看available即可)
第四种:showmap,可以查看每个so库占用的内存大小
adb shell showmap pid
第五种: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