背景
项目目前存在使用久了或者重复打开关闭某个页面,内存会一直飙升,居高不下,频繁发生GC。静置一段时间后,情况有所改善,但是问题依旧明显,如图1-1、1-2。
图1-1.操作时的内存使用情况
图1-2.静置时的内存使用情况
如上图1-1,是通过Android Studio查看内存(灰色)和CPU(红色)使用情况,可以看出内存有发生抖动并且是处于比较高的状态,再者,从logcat可以看到一直发生GC,如下图1-3:
图1-3.
出现这些情况,是有很多因素造成的,最主要的原因是发生了内存泄露:页面关闭之后,该页面对象理应被回收,但由于某种原因导致它没有被及时回收掉,一直占据着内存,导致内存居高不下。
目标
本目标就是排查出应用内发生内存泄露的地方,并解决问题,避免组员踩重复的坑,减少应用内存泄露现象,减少内存抖动,减少OOM发生的几率,提高应用的性能、流畅度,减少卡顿。
内存泄露排查优化过程
内存泄露排查过程,有两种方法,一种是通过MAT手动分析排查,另一种是通过LeakCanary注入到应用内辅助排查。
使用MAT分析内存泄露
首先,需要下载、安装内存分析工具MAT(http://www.eclipse.org/mat/)。接着,需要dump取内存,并进行分析。打开应用,先进行几个简单的操作:进入首页->进入社区首页->进入一条问答详情界面->迅速关闭页面->再次进入问答详情界面->再关闭,以此重复几次。然后通过Android Studio的Android Monitor将此时的内存dump下来,dump下来的hprof文件,存放在项目目录下的captures文件夹下,如图1-4、1-5。
图1-4.dump内存
图1-5. hprof文件
dump下来的hprof文件还不能用MAT打开,还需要进一步处理。需要借助Android SDK的一个转换工具,如图1-6。
图1-6.hprof-conv转换工具
通过命令行执行命令:hprof-conv.exe ,是指源文件,是指目标文件,如下图1-7。
图1-7. hprof-conv命令
然后使用MAT打开转换后的1.hprof文件,如下图1-8。
图1-8.内存概览
上图显示的是,当前内存使用情况。打开红色箭头按钮,这是OQL,类似于SQL,可以通过查询语句,查询对象。
为了排查出哪些界面(Activity)发生了内存泄露,可以使用下面的OQL语句查询,如图1-9。
图1-9.
查询结果显示有HomeActivity、QaDetailActivity,是符合之前的操作(进入首页->进入问答详情,详情重复进出)。这里可以看出QaDetailActivity肯定是发生了内存泄露,因为查询结果出现了两个QaDetailActivity,说明内存中存在了两个QaDetailActivity对象,但是当QaDetailActivity被关闭时,是应该被回收的。接着,执行图1-10操作,进一步排查是什么导致了内存泄露。
图1-10.执行Path To GC Roots
图1-11.GC Roots 执行结果
由于JAVA的垃圾回收机制是,当一个对象被持有引用时,如果发生GC时,是不会被回收的。如图1-11所示,当除去弱引用和软引用,执行GC后,this$0(QaDetailActivity)对象还是被mErrorListener所引用,mErrorListener是项目中Volley网络请求库中的一个错误回调接口。可以看下,项目中代码是如何实现的,如图1-12。
图1-12.JsonGet的使用
图1-12是获取问答详情信息发起的JsonGet请求,而Response.Listener和Response.ErrorListener都是匿名对象,一个异步回调,回调之后处理相关逻辑。试想一下,当JsonGet发起请求后,或因网络阻塞,不能及时处理回调,这时候关闭了QaDetailActivity,但是由于Response回调未执行完,还持有QaDetailActivity对象,所以此时QaDetailActivity并不能被回收。所以,目前项目中所有使用该方式请求数据理论上都存在着内存泄露的风险。那么如何解决此问题引起的内存泄露呢?这个和Android常见的会引起内存泄露的Handler一样,都是没有适时的移除回调导致的。优化后的代码如图1-13。
图1-13.
就是将JsonGet等网络请求通过观察者模式进行封装, 在Activity onCreate addObserver,在onDestory时removeObserver,这样就可以避免上述所遇到的内存泄露问题了。
上面是一个完整的内存泄露排查过程,但是,你会发现,这是在一个黑箱检测,你不知道什么时候该去dump下内存进行分析,有可能你操作了很多界面之后dump下的内存并没有内存泄露问题。如果进行黑箱测试的话,这是一个耗时、耗力的过程,那么接下来介绍LeakCanary的使用,这是一个内存泄露检测神器。
使用LeakCanary检测内存泄露
LeakCanary是square开源的一个项目https://github.com/square/leakcanary,它是一个Android和Java的内存泄露检测库,可以大幅度减少了开发中遇到的OOM问题。在项目中,专门开了一个代码分支dev_LeakCanary,用来检测内存泄露。关于LeakCanary的工作原理,可以从github开源项目上的wiki中获知https://github.com/square/leakcanary/wiki/FAQ :
- RefWatcher.watch()创建一个KeyedWeakReference去检测对象;
- 接着,在后台线程,它将会检查是否有引用在不是GC触发的情况下需要被清除的;
- 如果引用引用仍然没有被清除,将会转储堆到.hprof文件到系统文件中(it them dumps the heap into a .hprof file stored on the app file system.);
- HeapAnalyzerService是在一个分离的进程中开始的,HeapAnalyzer通过使用HAHA(https://github.com/square/haha )解析heap dump;
- 由于一个特殊的引用key和定位的泄露引用,HeapAnalyzer可以在heap dump中找到KeyedWeakReference;
- 如果有一个泄露,HeapAnalyzer计算到GC Roots的最短的强引用路径,然后创建造成泄露的引用链;
- 结果在app的进程中传回到DisplayLeakService,并展示泄露的通知消息;
那么如何使用它呢?
首先,添加相关依赖,如图1-14。
图1-14.LeakCanary依赖
然后在Application初始化,并获取一个RefWatcher对象,如图1-15。
图1-15.初始化LeakCanary
最后注册要监听的对象,比如,要监听Activity有没有内存泄露,那么,在BaseActivity$onDestory注册监听,如图1-16。
图1-16.监听Activity
这样所有继承BaseActivity都会被检测。同样进行开始的那样操作界面(进入首页->问答详情->重复进出)。这时候,会发现通知栏有消息,点开消息,如下图1-17。
图1-17.LeakCanary内存泄露分析界面
LeakCanary的分析结果显示,还是因为JsonGet的网络请求,异步回调引起的内存泄露,当然,如果要通过MAT查看更多信息,自己分析,可以在手机SD卡下Download/leakcanary文件夹下找到hprof文件,导出来之后根据之前的操作一样,先通过转换工具进行转换,再用MAT打开查看。
显而易见,通过LeakCanary进行内存泄露检测简单了不少,省时省力,但是如何解决内存泄露问题,还是要靠程序员自身的技能了,它只是告诉你哪里发生了内存泄露。
优化后对比
通过上面的内存泄露排查优化之后,使用相同包名(dev),相同环境(local环境下),相同租户和用户,尽可能的进行相同操作。
图1-18.优化后,操作时内存使用情况
与优化前图1-1相比,优化前内存的使用大小一直处于12M以上,优化后图1-18,内存使用在峰值在10M左右。静置一段时间后(系统发生GC,回收内存),如图1-19。
图1-19.优化后,静置时内存使用情况
静置后与图1-2相比,可以看出,两者内存差不多都稳定在9M左右,但优化后内存抖动情况明显减少。
说明:可能鉴于测试手段不足,不够严谨,测试结果存在偏差在所难免,但是,还是可以看出优化后的效果还是不错的。
总结
内存泄露,会导致应用在使用过程中,内存不断攀升,导致内存不足,应用不流畅,卡顿,甚至导致发生OOM,程序崩溃。所以,在开发过程中,应该避免书写一些会致使内存泄露的代码。这里总结一些常见的内存泄露情景,有则改之无则加勉。
1、 Handler。在使用Handler时应该通过传入一个runnable,来处理消息,然后在适当的时机移除该runnable,比如Activity onDestory时。
2、 网络请求异步回调。同Handler一样,应该在适当的时机移除异步回调操作,比如Activity onDestory时。
3、 匿名内部类。由于匿名内部类会隐式持有当前类的引用,所以应尽可能声明为static
4、 尽可能少使用静态成员变量。之前项目的一个bug就是这个情况引起的,当时的代码是这样的。
5、 在传递Context时,需要特别注意,如果可以的话,尽量使用Application Context。