Memory Leak檢測神器--LeakCanary初探

  在以前的文章Android內存泄露的幾種情形中提到過在開發中常見的內存泄露問題,但是過於草率。由於剛開年,工做還沒正式展開,就看了一下Github開源大戶Square的LeakCanary,並用公司項目的測試環境來練手。試圖找出項目中存在的內存泄露。與上一篇不一樣,這一篇我會先說一下Java的內存區域以及垃圾回收機制,而後再講LeakCanary的應用。並且會用一個在項目中遇到的真實案例來結尾。java

Java的內存模型

  在對於LeakCanary來講,咱們主要關心Java程序執行時的堆和棧。android

堆是用來存放對象的地方。棧是用來存放引用的地方。引用經過對象的句柄或者對象的地址來與對象保持關聯。git

垃圾回收就發生在堆上。github

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鏈。markdown

  當一個對象從GC Root有路徑可達,就說明這個對象正在被引用。app

GC對於這樣的對象會「網開一面」。假設有對象沒有不論什麼GC Root可達。GC就會對這些對象打上標記,方便後面回收。eclipse

  講到這裏有必要再介紹一下 內存泄露。當一個對象的「使命完畢」的時候,依照咱們的意願,此時GC應該回收這部分對象的內存空間。好比:一個方法裏面包括有一個局部變量A,當這種方法執行完之後。咱們但願A很是快被回收。但是由於一些緣由沒有回收。咱們就說發生了內存泄露。爲何會有內存泄露?說究竟就是由於這時從GC Root到此對象是可達的。對於咱們Android來講,Android很是多組件都有生命週期的概念,好比:Activity,Fragment。當這些組件的生命週期結束(onDestroy方法被回調)時,這些組件應該被回收掉。ide

但是由於一些緣由。比方: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.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的內存泄露。

爲了程序健康,穩健的執行,找出並解決內存泄露問題是一個優化的方式。

相關文章
相關標籤/搜索