安卓内存泄漏检测及原因分析

一 内存泄漏检测方法

1.自行生成Heap Dump文件分析内存

​ Heap Dump是堆转储文件,其中包含很多内容:

  • 所有的对象信息
    对象的类信息、字段信息、原生值(int, long等)及引用值
  • 所有的类信息
    类加载器、类名、超类及静态字段
  • 垃圾回收的根对象
    根对象是指那些可以直接被虚拟机触及的对象
  • 线程栈及局部变量
    包含了转储时刻的线程调用栈信息和栈帧中的局部变量信息

生成的方法很简单, 点击 View > Tool Windows > Profiler 然后在Memory Profiler界面中点击 2处的按钮即可

然后点击生成的Heap Dump文件可以看到如下图所示界面,界面中左侧会显示项目内所有内存分配的部分。我对secondActivity专门做了内存泄漏的操作,可以看到为这个Activity所分配的内存大小远远大于它本身的实例大小,所以这个Activity是有问题的,然后点击可以在右侧上方的Instance View中观察他的实例,第一个是明显有问题的,然后点击他在下方的Refercence中可以看到该实例的引用持有,然后不停点击就会发现其中的handler存在着循环引用,一直释放不掉。所以可以通过分析堆转储文件来找到响应的泄漏。

2. Record memory allocations录制一段时间内的内存分配

当我们对某个页面的某个时间的内存进行了一定的怀疑可以使用此方案,录制这段期间的内存分配。运行项目在Memory Profiler文件中,然后在时间轴上拖动某个范围,就可以拿到这段时间的堆转储文件。然后可以用方法一来进行分析,这样可以精准分析某个操作的内存事件,而不用像上面一样全部分析。

3. LeakCanary第三方工具

LeakCanary是一个比较便捷的库,build导入之后也无须在文件中进行配置,直接运行项目它会在设备上生成一个leakcanary的app,当出现有内存泄漏的情况时,进入app里面会有相应的内存分析。

简单说下LeakCanary的原理:

  • 监听
    在Android中,当一个Activity走完onDestroy生命周期后,说明该页面已经被销毁了,应该被系统GC回收。通过Application.registerActivityLifecycleCallbacks()方法注册Activity生命周期的监听,每当一个Activity页面销毁时候,获取到这个Activity去检测这个Activity是否真的被系统GC。

  • 检测
    当获取了待分析的对象后,需要确定这个对象是否产生了内存泄漏。
    通过WeakReference + ReferenceQueue来判断对象是否被系统GC回收,WeakReference 创建时,可以传入一个 ReferenceQueue 对象。当被 WeakReference 引用的对象的生命周期结束,一旦被 GC 检查到,GC 将会把该对象添加到 ReferenceQueue 中,待ReferenceQueue处理。当 GC 过后对象一直不被加入 ReferenceQueue,它可能存在内存泄漏。
    当我们初步确定待分析对象未被GC回收时候,手动触发GC,二次确认。

  • 分析
    分析这块使用了Square的另一个开源库haha利用它获取当前内存中的heap堆信息的快照snapshot,然后通过待分析对象去snapshot里面去查找强引用关系。

下面是部分内存泄漏在该app中显示的内容:依次点进去,它会比较详细的告诉你你的哪个页面的哪个方法泄漏掉了。



二 内存泄漏原因分析

1.单例

当调用getInstance时,如果传入的context是Activity的context。只要这个单例没有被释放,那么这个Activity也不会被释放一直到进程退出才会释放。

能使用Application的Context就不要使用Activity的Content,Application的生命周期伴随着整个进程的周期。

2. Handler 造成的泄漏

如果Handler中有延迟任务或者等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生泄漏。

  • 首先,非静态的Handler类会默认持有外部类的引用,如Activity等。
  • 然后,还未处理完的消息(Message)中会持有Handler的引用。
  • 还未处理完的消息会处于消息队列中,即消息队列MessageQueue会持有Message的引用。
  • 消息队列MessageQueue位于Looper中,Looper的生命周期跟应用一致。

3. 线程未终止造成的泄漏

子线程中不当的使用Looper.prepare()和Looper.loop()方法造成内存泄漏。 Looper.loop()是一个无限循环的方法,它是反复的去MessageQueue里面去取出Message并分发给对应的Handler去执行,如果在子线程中调用了Looper.prepare()和Looper.loop()方法,Looper.loop()会导致这个线程一直不死,一直堵在这里,因此线程就无法结束运行,在Looper.prepare()和Looper.loop()之间的所有对象都没办法被释放,解决方案就是在不用的时候及时的把Looper给quit掉

4. 对象的注册和反注册没有成对出现

类似的还有一些系统的服务注册之后一定要注销,Activity中的启动了属性动画在销毁的时候,也要调用cancle方法。如果不cancel掉属性动画就会一直运行并且一直去执行控件的onDraw方法,那么控件持有了Activity对象,而属性动画ObjectAnimator持有了I该控件,ObjectAnimator一直在运行,那么Activity对象也就不能被释放了。

三 如何避免写出内存泄漏的代码

  1. 注意不要让类变量直接或间接地持有Activity context引用
  2. 尽量不要在单例中使用Activity context,如果要用,不能将其作为全局变量
  3. 时刻注意内部类(尤其是Activity的内部类)的生命周期,尽量使用静态内部类代替内部类,如果内部类需要访问外部类的成员,可以用“静态内部类+弱引用”代替;内部类的生命周期不应该超出外部类,外部类结束前,应该及时结束内部类生命周期(停止线程、AsyncTask、TimerTask、Handler消息等,移除类变量或长生命周期的线程对Callback、listener等的强引用)
  4. 及时注销广播以及一些系统服务的监听器
  5. 属性动画在Activity销毁前记得cancel
  6. 文件流、Cursor等资源用完及时关闭
  7. Activity销毁前WebView的移除和销毁