概要
应用内存问题,大体可分为三种类型,内存泄漏、内存消耗过大、内存抖动等,内存异常会导致app卡顿、不流畅等,在整体内存不大的机器上甚至会影响系统性能,严重影响着用户体验,本文主要从以上三个方面来说明内存优化问题。
本文仅适用于应用内存优化。
内存泄漏异常
内存泄漏是指该回收的对象被其它对象持有,导致无法被回收,内存越用越多的现象。内存泄漏是开发者的噩梦,解决问题的关键就是切断异常的引用关系,实现对象正常回收。
怎么检测是否有内存泄漏呢?有两个方法。第一个方法为,使用ddms heap判断有无内存泄漏,打开ddms面板,选中对应的进程,点击 update heap 按钮,在heap面板上Cause GC,对app执行相关操作后,如果data object的数量一直呈上升趋势,那么则有内存泄漏。
第二个方法,利用dumpsys meminfo + 进程名,查看当前进程内存情况,对app操作后,如果内存一直上升,基本上也可以判断应用有内存泄漏。
常见的内存泄漏原因有:
- 资源对象没有关闭(cursor、file、输入输出流等)
- 注册对象没有反注册(广播、监听器等)
- 重复bindService,没有unbindService
- context、static变量导致
- 重复载入字体文件导致
内存泄漏分析
Mat是分析内存泄漏的神兵利器,它能显示具体进程中对象的个数、引用关系、占用内存大小等等。通过引用关系,就能查找出异常引用。对象个数往往也是很重要的指标,如果某个activity个数大于1,很可能就存在内存泄漏。
假定场景,activity中只注册广播但未反注册,重复进入此activity或者旋转屏幕,此时抓取hprof文件,利用mat分析,整体流程如下:
- 操作app一段时间,引发可能出现的内存泄漏,在ddms中,选中对应进程,点击 dump hprof file 按钮,生成hprof文件。
- 调用hprof-conv origin.hprof goal.hprof,转换成mat能识别的文件
- 用mat打开goal.hprof
Mat主要有三个重要面板,Histogram,列出每个类实例的个数。Leak Suspects,疑似内存泄漏报告。Dominator Tree,列出消费内存最多的对象。根据个人经验,从Histogram入手,最容易也最快就能找到问题所在。Leak Suspects,也较为方便可以查到问题所在。而从Dominator Tree入手,较难查找。针对模拟问题,从Histogram查看,并搜索特定包名(本例中特定包名为okunu),因为系统对象一般不会泄漏。
FirstActivity对象竟然有五个,这显示不对,右键选中FirstActivity,List objects,再选with incoming references,查看对象来源。
如上图,引用着FirstActivity对象的居然有这么多,FirstActivity无法释放,肯定是以上其中之一非法引用了FirstActivity,那么只能一一点开查看具体是哪个不良引用了。
这是一种查看不良引用的方式,还有另一种方式,查看gc路径。
第一种方式,直接查看持有FirstActivity引用的对象,最终找到未注册的广播监听器。第二种方式,查看FirstActivity的gc路径,最后找到LoadedApk对象中的mReceivers对象(在context注册广播里会在LoadedApk保存相关对象),所以也相当于找出了内存泄漏原因。
如果使用Leak Suspects查找内存泄漏原因,会稍显不直观,查看本例的Leak Suspects,发现占用最大的是char数组对象,于是打开Histogram,果然占用内存最大的即为char数组对象。
Char数组对象被FirstActivity的uiThread所引用,这明显不正常,还被好多个uiThread一同引用着,间接说明FirstActivity存在多个对象,它属于泄漏人员。那FirstActivity为啥泄漏呢?请见前文。
Mat确实是内存问题的神器,但因为内存对象的循环引用,信息太多,反而不太方便查找,mat往往给人这样的感觉,大海捞针。所以建议同学们使用Histogram面板,通过信息检索包名,快速定位,其它两个面板信息不直观,用处不是太大。最主要的两种查引用方式,with incoming reference(查看对象来源,能查看持有此对象引用的列表)和gc path(垃圾回收路径),多尝试,一定会有收获的。
内存开销大
内存开销过大,与内存泄漏有本质区别,内存泄漏是真正的bug,而且会内存越用越大,最终导致应用崩溃重启。内存开销过大,只是形容应用占用内存高,且一直维持在一定区间,并不会一直增大,在内存较小的机器上,内存开销过大会影响其它应用,影响总体性能,所以也是需要优化的。
解决内存开销大问题的关键点是,找到内存占用高的原因并想办法优化。但这并没有解决问题的明确套路或方法,并没有相关工具可以说清楚内存的构成(以前的android版本配合mat是可以的),最好的方法是删代码,使用排除法来找优化点,针对app常见的内存大户,比如说图片等,删除代码后查看内存情况,往往会有收获。
此类问题最重要的还是平时养成较好的编码习惯,减少内存使用,像帧动画之类的可以不用就尽量不用。
内存抖动
内存抖动,是指在使用过程中迅速申请内存又迅速释放,导致gc频繁,app卡顿,影响用户体验。在Android studio上可以动态查看内存使用情况,非常方便地就能定位内存抖动。内存抖动常常能见到如下波形:
此时也能通过log定位,搜索关键字gc,往往会有如下log
此类情况常常发生在循环中,比如字符串拼接,在循环中构造bitmap对象等等。在图片处理应用中也可能发生这种情况,申请内存和释放内存频率过高,导致异常。这类问题是能够很好地避免的,如果逻辑需要,必然存在大量内存申请又释放的问题,那就只能采用缓存方式了,将所需资源缓存起来,不释放或在特定时间点再释放,用空间换取应用的流畅性。
总结
以上三类内存问题均可以通过良好编码习惯避免的,了解常见的内存泄漏原因,清楚内存泄漏形成原理,在日常工作中注意及时释放资源等,都是可以避免的。mat信息非常庞大,日常多看看,也有助于解决内存泄漏等问题。而内存开销大,有时是一件很检验代码熟悉程度的工作,优化有一定难度,需要沉下心来。内存抖动反而是以上三种中最容易解决的问题。