Memory Leak检測神器--LeakCanary初探

  在之前的文章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的一款开源产品。用于检測程序中的内存泄露。easy上手,操作简单,是广大安卓程序猿的必备神器。
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.HashMapHashMapEntry[].[3]021714:40:10.220:D/LeakCanary(29354):referencesjava.util.HashMapHashMapEntry.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的内存泄露。

为了程序健康,稳健的执行,找出并解决内存泄露问题是一个优化的方式。

原文地址:https://www.cnblogs.com/slgkaifa/p/7389060.html