1. 程式人生 > >Android 記憶體檢測工具

Android 記憶體檢測工具

所謂記憶體洩漏,是指本該被回收的記憶體由於某種原因繞開了GC回收演算法,從而導致該記憶體無法被有效資料使用而使得總記憶體減小的情況。

記憶體洩漏會導致記憶體消耗的增加,大量的消耗會使得APP OOM,特別是在一些記憶體比較小的機器上。下面我們看看有哪些工具可以用來分析記憶體洩漏。

Heap Dump

Heap Dump的主要功能就是檢視不同的資料型別在記憶體中的使用情況。它可以幫助你找到大物件,也可以通過資料的變化發現記憶體洩漏。

使用Heap Dump

開啟Android Device Monitor工具,在左邊Devices列表中選擇要檢視的應用程式程序,點選Update Heap按鈕這裡寫圖片描述

,在右邊選擇Heap選項,並點選Cause GC按鈕,就會開始顯示資料。我們每次點選Cause GC按鈕都會強制應用程式進行垃圾回收,並將清理後的資料顯示在Heap工具中。如下圖所示。

這裡寫圖片描述

從上圖可以看出,Heap工具共有三個區域,分別是總覽檢視(上部)、詳情檢視(中部)和記憶體分配柱狀圖(下部)。

總覽檢視

其中總覽檢視可以檢視整體的記憶體情況,表中的顯示資訊如下所示:

  • Heap Size 堆疊分配給該應用程式的記憶體大小
  • Allocated 已使用的記憶體大小
  • Free 空閒的記憶體大小
  • %Used 當前Heap的使用率(Allocated/Heap Size)
  • #Objects 物件的數量

詳情檢視

詳細檢視展示了所有的資料型別的記憶體情況,表中列的資訊如下所示:

  • Total Size 總共佔用的記憶體大小
  • Smallest 將該資料型別的物件從小到大排列,排在第一個的物件所佔用的記憶體
  • Largest 將該資料型別的物件從小到大排列,排在最後一個的物件所佔用的記憶體
  • Median 將該資料型別的物件從小到大排列,排在中間的物件所佔用的記憶體
  • Average 該資料型別的物件所佔用記憶體的平均值

除了列的資訊,還有行資訊:

  • data object 物件
  • class object 類
  • 1-byte array (byte[],boolean[]) 1位元組的陣列物件
  • 2-byte array (short[],char[]) 2位元組的陣列物件
  • 4-byte array (object[],int[],float[]) 4位元組的陣列物件
  • 6-byte array (long[],double[]) 8位元組的陣列物件
  • non-Java object 非Java物件

行資訊中兩個比較重要的引數:
free—它與總覽檢視中的free的含義不同,它代表記憶體碎片。當新建立一個物件時,如果碎片記憶體能容下該物件,則複用碎片記憶體,否則就會從free空間(總覽檢視中的free)重新劃分記憶體給這個新物件。free是判斷記憶體碎片化程度的一個重要的指標。
1-byte array—圖片是以byte[]的形式儲存在記憶體中的,如果1-byte array一行的資料過大,則需要檢查圖片的記憶體管理了。

檢測記憶體洩漏

我們先寫一個記憶體洩漏的例子:
MainActivity:

public class MainActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button =(Button)findViewById(R.id.bt_next);
        button.setText("SecondActivity");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this,SecondActivity.class));
            }
        });
    }
}

SecondActivity:

public class SecondActivity extends AppCompatActivity {
    private static Object inner;
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.bt_next);
        button.setText("MainActivity");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                createInnerClass();
                finish();
            }
        });
    }

    class InnerClass {
    }

    private void createInnerClass() {
        inner = new InnerClass();
    }
}

記憶體洩漏的原因很簡單,SecondActivity的內部類InnerClass Hold住了外部類例項的引用,而InnerClass的例項是靜態的,就會間接的長期維持著外部類例項的引用,阻止被系統回收,導致SecondActivity例項不能被釋放。

Heap Dump檢測記憶體洩漏:通常做法是使用Update Heap進行記憶體監聽,然後操作可能發生洩漏的APP功能、介面,並點選Cause GC進行手動GC,經過多次操作後檢視data object的Total Size大小是否有很大的變化,如果有則可能發生了記憶體洩漏,導致記憶體使用不斷增大。

步驟:
(1)在左邊Devices列表中選擇要檢視的應用程式程序,點選Update Heap按鈕,在右邊選擇Heap選項,並點選Cause GC按鈕,就會開始顯示資料,如下圖所示。
這裡寫圖片描述

(2)在MainActivity和SecondActivity間跳轉多次。這樣會生成多個SecondActivity例項且不能釋放。重新點選Update Heap和Cause GC按鈕,顯示新的資料。
這裡寫圖片描述
可以看到data object由610.500KB增長到1.158MB

(3)這時我點選Cause GC按鈕,資料顯示為:
這裡寫圖片描述

經過Cause GC的操作,Total Size的值從1.158MB變為了667.109KB,這是一個比較大的變化,說明在Cause GC操作之前有518.683KB(1.158MB-667.109KB)的記憶體沒有被回收,可能發生了記憶體洩漏。你多GC幾次,甚至會釋放更多的記憶體。

Allocation Tracker

使用Heap Dump可以讓你對APP的記憶體整體使用情況進行掌控,但缺點是無法瞭解每塊記憶體具體分配給哪個物件了,這時就需要使用Allocation Tracker工具來進行記憶體跟蹤。它允許你在執行某些操作的同時監視在何處分配物件,瞭解這些分配使你能夠調整與這些操作相關的方法呼叫,以優化應用程式效能和記憶體使用。

Allocation Tracker能夠做到如下的事情:

  • 顯示程式碼分配物件型別、大小、分配執行緒和堆疊跟蹤的時間和位置。
  • 通過重複的分配/釋放模式幫助識別記憶體變化。
  • 當與 HPROF Viewer結合使用時,可以幫助你跟蹤記憶體洩漏。例如,如果你在堆上看到一個bitmap物件,你可以使用Allocation Tracker來找到其分配的位置。

使用Allocation Tracker

AS和DDMS中都有Allocation Tracker,這裡會介紹AS中的Allocation Tracke如何使用。
使用的步驟為:

  • 執行需要監控的應用程式。
  • 點選AS面板下面的Android Monitors選項,檢視 這裡寫圖片描述
  • 點選Start Allocation Tracking按鈕 這裡寫圖片描述
  • 操作應用程式。
  • 點選Stop Allocation Tracking按鈕 這裡寫圖片描述,結束快照。這時Memory Monitor會顯示出捕獲快照的期間,如下圖所示。
    這裡寫圖片描述

  • 過幾秒後就會自動開啟一個視窗,顯示當前生成的alloc檔案的記憶體資料。

alloc檔案分析

這裡寫圖片描述

該alloc檔案顯示以下資訊:

  • Method—負責分配的Java方法
  • Count—分配的例項總數
  • Total Size—分配記憶體的總位元組數

目前的選單選項是Group by Method我們也可以選擇 Group By Allocator,如下圖所示
這裡寫圖片描述

同樣是上面的demo,我們在MainActivity和SecondActivity間跳轉了5次。
可以看到SecondActivity生成了5個匿名內部類OnClickListener例項(SecondActivity$1 表示它的第一個匿名內部類)和5個內部類InnerClass的例項,每個例項16個位元組,且都沒有被釋放記憶體。

我們可以選擇列表中的一項,單擊滑鼠右鍵,在彈出的選單中選擇jump to the source就可以跳轉到對應的原始檔中。
除此之外,還可以點選Show/Hide Chart按鈕來顯示資料的圖形化,如下圖所示。
這裡寫圖片描述

MAT

如果想要深入的進行分析並確定記憶體洩漏,就要分析疑似發生記憶體洩漏時所生成堆儲存檔案。堆儲存檔案可以使用DDMS或者Memory Monitor來生成,輸出的檔案格式為hprof,而MAT就是來分析堆儲存檔案的。

MAT,全稱為Memory Analysis Tool,是對記憶體進行詳細分析的工具,它是Eclipse的外掛,如果用Android Studio進行開發則需要單獨下載它,下載地址為:http://eclipse.org/mat/

生成hprof檔案

我們這裡分析一下用AS的Memory Monitor來生成hprof檔案。

我們還是先寫一個記憶體洩漏的例子:

public class MainActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button =(Button)findViewById(R.id.bt_next);
        button.setText("SecondActivity");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this,SecondActivity.class));
            }
        });
    }
}
public class SecondActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.bt_next);
        button.setText("MainActivity");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LeakThread leakThread = new LeakThread();
                leakThread.start();
                finish();
            }
        });
    }

    class LeakThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(60 * 60 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

記憶體洩漏的原因是非靜態內部類LeakThread Hold住了外部類的引用,而LeakThread中做了耗時操作,導致外部類SecondActivity無法被釋放。

生成hprof檔案主要分為一下幾個步驟:

  • 執行需要監控的應用程式。
  • 點選AS面板下面的Android Monitors選項,檢視 這裡寫圖片描述
  • 操作應用程式(本文的例子就是不斷切換Activity)。
  • 點選Dump Java Heap按鈕 這裡寫圖片描述,生成hprof檔案。

Memory Monitor生成的hprof檔案不是標準的,AS提供了便捷的轉換方式:Memory Monitor生成的hprof檔案都會顯示在AS左側的Captures標籤中,在Captures標籤中選擇要轉換的hprof檔案,並點選滑鼠右鍵,在彈出的選單中選擇Export to standard.hprof選項,即可匯出標準的hprof檔案,如下圖所示。
這裡寫圖片描述

MAT分析hpof檔案

用MAT開啟標準的hprof檔案,選擇Leak Suspects Report選項。這時MAT就會生成報告,這個報告分為兩個標籤頁,一個是Overview,一個是Leak Suspects,如下圖所示。
這裡寫圖片描述

這裡寫圖片描述

Leak Suspects中會給出了MAT認為可能出現記憶體洩漏問題的地方,本例共給出了4個記憶體洩漏猜想,通過點選每個記憶體洩漏猜想的Details可以看到更深入的分析清理情況。如果記憶體洩漏不是特別的明顯,通過Leak Suspects是很難發現記憶體洩漏的位置。

開啟Overview標籤頁,首先看到的是一個餅狀圖,它主要用來顯示記憶體的消耗,餅狀圖的彩色區域代表被分配的記憶體,灰色區域的則是空閒記憶體,點選每個彩色區域可以看到這塊區域的詳細資訊,如下圖所示。

這裡寫圖片描述

再往下看,Actions一欄的下面列出了MAT提供的四種Action,其中分析記憶體洩漏最常用的就是Histogram和Dominator Tree。Histogram可以統計記憶體中物件的名稱、種類、例項數和大小,而Dominator Tree則是建立這些記憶體物件之間的關係。
我們點選Actions中給出的連結或者在MAT工具欄中就可以開啟Histogram和Dorminator Tree。

Histogram

這裡寫圖片描述

圖中可以看出Dorminator Tree有四列資料。

  • Class Name:類名
  • Objects:物件例項的個數
  • Shallow Heap:物件自身佔用的記憶體大小,不包括它引用的物件。如果是陣列型別的物件,它的大小是陣列元素的型別和陣列長度決定。如果是非陣列型別的物件,它的大小由其成員變數的數量和型別決定。
  • Retained Heap:一個物件的Retained Set所包含物件所佔記憶體的總大小。換句話說,Retained Heap就是當前物件被GC後,從Heap上總共能釋放掉的記憶體。

在列表頂部的Regex區域,可以輸入過濾條件(支援正則表示式),通常Activity的記憶體洩漏,可以直接通過輸入Activity名獲取與之相關的的例項。

這裡寫圖片描述

可以看到,SecondActivity例項建立了11次,基本可以判斷記憶體洩漏了。具體是如何洩漏的呢?可以通過檢視GC物件的引用鏈來分析。在SecondActivity上右鍵,選擇Merge Shortest Paths to GC Root,並通過彈出的列表選擇相關型別的引用(強、軟、弱、虛),分析不同引用型別下的GC情況,這裡我們選擇exclude all phantom/weak/soft etc. references,因為這個選項排除了虛引用、弱引用和軟引用,這些引用一般是可以被回收的。這時MAT就會給出SecondActivity的GC引用鏈。

這裡寫圖片描述

到這裡整個記憶體洩漏一目瞭然了,引用SecondActivity的是內部類LeakThread,this$0的含義就是內部類自動保留的一個指向所在外部類的引用,而這個外部類就是SecondActivity,導致SecondActivity無法被GC。

同時,在Histogram中還可以檢視一個物件包含了哪些物件的引用。例如檢視SecondActivity包含的引用,在SecondActivity上右鍵,選擇List objects—with incoming references(顯示選中物件被哪些外部物件引用,而with outcoming references表示選中物件持有哪些物件的引用)

這裡寫圖片描述

Dominator Tree

Dorminator Tree意味支配樹,從名稱就可以看出Dorminator Tree更善於去分析物件的引用關係。而Histogram更側重於量的分析。

這裡寫圖片描述

Shallow Heap和Retained Heap的含義和上面Histogram中的一樣。

同樣過濾SecondActivity:

這裡寫圖片描述

發現有些圖示帶有小圓點,表示它們可以被GC系統訪問到,是記憶體洩漏的重點懷疑物件。那麼SecondActivity沒有原點,是不是代表不能被GC訪問,可以回收呢?當然不是,如果可以回收,又怎麼會存在這麼多的例項呢。那怎麼找到它的GC Root呢?在SecondActivity上右鍵,選擇Path To GC Roots,同樣選擇exclude all phantom/weak/soft etc. references

這裡寫圖片描述

得出的結果和上面是一樣的,引用SecondActivity的是LeakThread,這導致了SecondActivity無法被GC。

OQL

OQL全稱為Object Query Language,類似於SQL語句的查詢語言,能夠用來查詢當前記憶體中滿足指定條件的所有的物件。它的查詢語句的基本格式為:

SELECT * FROM [ INSTANCEOF ]    <class_name> [ WHERE <filter-expression>]

當我們點選OQL按鈕這裡寫圖片描述,輸入條件select * from instanceof android.app.Activity並按下F5時(或者按下工具欄的紅色歎號),會將當前記憶體中所有Activity都顯示出來,如下圖所示。

這裡寫圖片描述

更過用法詳見官方文件

LeakCanary

LeakCanary 是一個開源的在debug版本中檢測記憶體洩漏的java庫。

使用LeakCanary

在APP的build.gradle檔案新增:

dependencies {
  debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.2'
  releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.2'
}

接下來在Application加入如下程式碼

public class BaseApplication extends Application {
    @Override public void onCreate() {
    super.onCreate();
    //如果當前的程序是用來給LeakCanary進行堆分析的則return
    if (LeakCanary.isInAnalyzerProcess(this)) {
      return;
    }
    LeakCanary.install(this);
  }
}

上面程式碼只能夠檢測Activity的記憶體洩漏,當然還存在其他類的記憶體洩漏,這時我們就需要使用RefWatcher來進行監控。改寫Application,如下所示

public class BaseApplication extends Application {
        private RefWatcher refWatcher;

        @Override public void onCreate() {
        super.onCreate();
        refWatcher= setupLeakCanary();
    }

    private RefWatcher setupLeakCanary() {
        //如果當前的程序是用來給LeakCanary進行堆分析的則return
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return RefWatcher.DISABLED;
        }
        return LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher(Context context) {
        BaseApplication baseApplication = (BaseApplication) context.getApplicationContext();
        return baseApplication.refWatcher;
    }
}

這裡我們仍然使用上一節的demo,只是在SecondActivity中實現onDestroy方法,其中得到RefWatcher,並呼叫它的watch方法,watch方法的引數就是要監控的物件。

public class SecondActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.bt_next);
        button.setText("MainActivity");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LeakThread leakThread = new LeakThread();
                leakThread.start();
                finish();
            }
        });
    }

    class LeakThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(60 * 60 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = BaseApplication.getRefWatcher(this);
        refWatcher.watch(this);
    }
}

其實這個例子中onDestroy方法是多餘的,因為LeakCanary在呼叫install方法時會啟動一個ActivityRefWatcher類,它用於自動監控Activity執行onDestroy方法之後是否發生記憶體洩露。這裡只是為了方便舉例,如果想要監控Fragment,在Fragment中新增如上的onDestroy方法是有用的。

操作

執行程式,這時會在介面生成一個名為Leaks的應用圖示。接下來不斷的切換Activity,這時會閃出一個提示框,提示內容為:“Dumping memory app will freeze.Brrrr.”。再稍等片刻,記憶體洩漏資訊就會通過Notification展示出來

這裡寫圖片描述

Notification中提示了MainActivity發生了記憶體洩漏, 洩漏的記憶體為4.3KB。點選Notification就可以進入記憶體洩漏詳細頁,除此之外也可以通過Leaks應用的列表介面進入,列表介面如下圖所示

這裡寫圖片描述 這裡寫圖片描述

點選加號就可以檢視具體類所在的包名稱。整個詳情就是一個引用鏈:SecondActiviy的內部類LeakThread引用了LeakThread的this0this0的含義就是內部類自動保留的一個指向所在外部類的引用,而這個外部類就是詳情最後一行所給出的SecondActiviy的例項,這將會導致SecondActiviy無法被GC,從而產生記憶體洩漏。

除此之外,我們還可以將 heap dump(hprof檔案)和info資訊分享出去,如下圖所示。

這裡寫圖片描述

需要注意的是分享出去的hprof檔案並不是標準的hprof檔案,還需要將它轉換為標準的hprof檔案,這樣才會被MAT識別從而進行分析。