Android 記憶體檢測工具
所謂記憶體洩漏,是指本該被回收的記憶體由於某種原因繞開了GC回收演算法,從而導致該記憶體無法被有效資料使用而使得總記憶體減小的情況。
記憶體洩漏會導致記憶體消耗的增加,大量的消耗會使得APP OOM,特別是在一些記憶體比較小的機器上。下面我們看看有哪些工具可以用來分析記憶體洩漏。
Heap Dump
Heap Dump的主要功能就是檢視不同的資料型別在記憶體中的使用情況。它可以幫助你找到大物件,也可以通過資料的變化發現記憶體洩漏。
使用Heap Dump
開啟Android Device Monitor工具,在左邊Devices列表中選擇要檢視的應用程式程序,點選Update 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的this
除此之外,我們還可以將 heap dump(hprof檔案)和info資訊分享出去,如下圖所示。
需要注意的是分享出去的hprof檔案並不是標準的hprof檔案,還需要將它轉換為標準的hprof檔案,這樣才會被MAT識別從而進行分析。