Memory Leak检测神器--LeakCanary初探

时间:2021-10-20 09:00:36

  在之前的文章Android内存泄露的几种情形中提到过在开发中常见的内存泄露问题,但是过于草率。由于刚开年,工作还没正式展开,就看了一下Github开源大户Square的LeakCanary,并用公司项目的测试环境来练手,试图找出项目中存在的内存泄露。与上一篇不同,这一篇我会先说一下Java的内存区域以及垃圾回收机制,然后再讲LeakCanary的应用,并且会用一个在项目中遇到的真实案例来结尾。

Java的内存模型

  在对于LeakCanary来说,我们主要关心Java程序运行时的堆和栈。堆是用来存放对象的地方,栈是用来存放引用的地方。引用通过对象的句柄或者对象的地址来与对象保持关联。垃圾回收就发生在堆上。

Java垃圾回收算法

  垃圾回收算法有很多种,这里介绍Java中常见的垃圾回收算法:
垃圾回收器(GC)把栈上的一些引用所关联的对象作为根节点(GC Root),根据这些引用去搜索与其关联的对象,搜索所经过的节点所组成的路径称为GC链。比如有三个类A,B,C,其中,A持有B的应用,B持有C的引用,

public class A {

public A(B b)
{
this.b = b;
}
private B b;
}

public class B {

public B(C c){
this.c = c;
}

private C c;
}

public class C {
}

当执行:

    C c = new C();
B b = new B(c);
A a= new A(b);

我们就可以通过引用a来找到C的对象,这一条链就可以作为GC链。

  当一个对象从GC Root有路径可达,就说明这个对象正在被引用。GC对于这种对象会“网开一面”。如果有对象没有任何GC Root可达,GC就会对这些对象打上标记,方便后面回收。

  说到这里有必要再介绍一下 内存泄露。当一个对象的“使命完成”的时候,按照我们的意愿,此时GC应该回收这部分对象的内存空间。例如:一个方法里面包含有一个局部变量A,当这个方法执行完以后,我们希望A很快被回收,但是由于一些原因没有回收,我们就说发生了内存泄露。为什么会有内存泄露?说到底就是因为这时从GC Root到此对象是可达的。对于我们Android来说,Android很多组件都有生命周期的概念,例如:Activity,Fragment。当这些组件的生命周期结束(onDestroy方法被回调)时,这些组件应该被回收掉。但是由于一些原因,比如:Activity被一个生命周期比较长的匿名内部类引用,被一个static对象引用,被Handler(一般是Handler调用了postDelay方法)引用。。。等情况。

  Android对每个进程的内存占用是有大小限制的,以前在16MB以内,这就要求我们对内存的使用十分小心。内存泄露导致对象甚至Android组件(通常包含很多其他引用,占用内存大)不能被回收,就会对程序安全在成极大的隐患,有可能用户在一个会引发内存泄露的动作上反复操作,使内存在很短时间内急剧膨胀,最后造成程序闪退的“悲惨结局”。然而这种结局都不是我们想要的,所以,我们应该尽量做到不让程序产生内存泄露。由于内存泄露,并不会像空指针这种错误一样直接抛出来,普通程序员很难发现内存泄露带来的隐患。据统计,94%得OOM异常都是由于内存泄露引发的。所以,解决内存泄露是我们Android程序员必须面对的话题。

内存泄露检测神器LeakCanary

  LeakCananry是开源大户Square的一款开源产品,用于检测程序中的内存泄露。容易上手,操作简单,是广大安卓程序员的必备神器。
GItHUB项目地址

集成LeakCanary

  由于公司项目还是在Eclipse上面开发,所以这里说的是如何在Eclipse里面集成。
  首先我们下载适用于Eclipse的LeakCanary。项目地址。在此感谢作者的辛勤劳动。
  然后,我们在Eclipse将下载的包import到Eclipse工作空间。将其作为Android的库(library)。
  接着,我们将LeakCanary里面的Service和Activity拷贝到你的项目里面。记得将Service和Activity的名字改成全类名。修改好的清单文件大致为:

.........
.........
你项目的清单
.........

<!-- Leakcanary必须的界面和服务 -->

<service
android:name="com.squareup.leakcanary.internal.HeapAnalyzerService"
android:enabled="false"
android:process=":leakcanary" />

<service
android:name="com.squareup.leakcanary.DisplayLeakService"
android:enabled="false" />


<activity
android:name="com.squareup.leakcanary.internal.DisplayLeakActivity"
android:enabled="false"
android:icon="@drawable/__leak_canary_icon"
android:label="@string/__leak_canary_display_activity_label"
android:taskAffinity="com.squareup.leakcanary"
android:theme="@style/__LeakCanary.Base" >

<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

至此,LeakCanary集成完成。

在项目中使用LeakCanary

我们需要在Application里面对LeakCanary做初始化,然后在BaseActivity或者BaseFragment的onDestroy里面对这个类进行监控。代码为:

    /**
* 初始化内存泄露监测 applicaton里面的代码
*/

private void initRefWatcher() {
this.refWatcher = LeakCanary.install(this);
}

//BaseActivity或者BaseFragment的代码
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = MentorNowApplication.getRefWatcher(this);
refWatcher.watch(this);
}

这样,我们就可以对我们的项目进行检测。

案列

下面,我拿我们项目里面的一个内存泄露案列来讲解具体的使用(前提是你的项目正确集成了LeakCanary)。我把发生内存泄露的代码粘贴出来,也把修改后的代码粘贴出来。
发生内存泄露的代码:
在项目中,我们使用了时间总线EventBus来解耦和,我们都知道,使用EventBUs我们需要先注册,在页面销毁的时候,我们应该先反注册,这是由于EventBus的特定设计而成,EventBus的生命周期和整个应用的生命周期相同。下面,我就用LeakCananry来检测由于未反注册造成的Fragement内存泄露。通过LeakCananry得到的Log信息如下:
02-17 14:40:10.219: D/LeakCanary(29354): * com.mentornow.MainActivity has leaked:
02-17 14:40:10.219: D/LeakCanary(29354): * GC ROOT static event.EventBus.defaultInstance
02-17 14:40:10.220: D/LeakCanary(29354): * references event.EventBus.typesBySubscriber
02-17 14:40:10.220: D/LeakCanary(29354): * references java.util.HashMap.table
02-17 14:40:10.220: D/LeakCanary(29354): * references array java.util.HashMap HashMapEntry[].[3]021714:40:10.220:D/LeakCanary(29354):referencesjava.util.HashMap HashMapEntry.key
02-17 14:40:10.220: D/LeakCanary(29354): * references com.mentornow.fragment.DiscoverFragment.gv
02-17 14:40:10.220: D/LeakCanary(29354): * references com.mentornow.view.MyGridView.mContext
02-17 14:40:10.220: D/LeakCanary(29354): * leaks com.mentornow.MainActivity instance
02-17 14:40:10.220: D/LeakCanary(29354): * Reference Key: fef0c426-0096-475b-9f5c-cb193fa7cecd
02-17 14:40:10.220: D/LeakCanary(29354): * Device: motorola motorola XT1079 thea_retcn_ds
02-17 14:40:10.220: D/LeakCanary(29354): * Android Version: 5.0.2 API: 21 LeakCanary:
02-17 14:40:10.220: D/LeakCanary(29354): * Durations: watch=5042ms, gc=196ms, heap dump=2361ms, analysis=26892ms

分析日志

第一句明确告诉我们MainActivity发生了内存泄露。
第二句造成内存泄露的原因是 从 EventBus的引用defaultInstance到MainActivity是可达的。
后面几句是这条GC链的节点:
EventBus首先会造成DiscoverFragment无法回收,由于DiscoverFragment保有MainActivity的引用(通过framgnet.getActivity()可得到),所以从EventBus到MainActivity是可达的。
由于GCRoot 到MainActivity是可达的,所以GC不会回收MainActivity,从而造成内存泄露。

解决办法

按照EventBus的使用规范,我们应该在使用完以后,进行反注册。我们在Fragment的onDestroy方法里面调用发注册方法,然后运行程序。发现以前的log不再打印。

总结

在我对公司项目排查内存泄露的时候发现,内存泄露常常让人忽略。所以,我还是在最后总结一下会出现内存泄露的几种情形:
1,使用了Handler,并且使用了延时操作。比如轮播图
2,使用了线程。线程一般处理耗时操作,子线程部分的执行时间有可能查出页面的生命周期,如果不在线程中作处理,会发生内存泄露。解决办法有:使用虚引用,在页面销毁时让线程终止运行等。
3,使用了匿名内部类。由于匿名内部类保有外部类的引用,所以在Activity或者Fragment中使用匿名内部类要特别注意不要让内部类的生命周期大于外部类的生命周期。或者使用静态内部类。
4,传入参数有误,由于项目中使用了友盟推送,对外暴露的API是UmengPushAgent这个类保有一个静态的Context,如果传入Activity,就会发生内存泄露。

等等,内存泄露很常见,在使用LeakCanary还会检测到系统SDK的内存泄露。为了程序健康,稳健的运行,找出并解决内存泄露问题是一个优化的方式。