Android 性能優化之使用MAT分析內存泄露問題

轉載請註明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/42396507),請尊重他人的辛勤勞動成果,謝謝!

我們平常在開發Android應用程序的時候,稍有不慎就有可能產生OOM,雖然JAVA有垃圾回收機,但也不能杜絕內存泄露,內存溢出等問題,隨着科技的進步,移動設備的內存也越來越大了,但由於Android設備的參差不齊,可能運行在這臺設備好好的,運行在那臺設備就報OOM,這些適配問題也是比較蛋疼的,比如我們平常運行着一個應用程序,運行的好好的,突然到某個Activity就給你爆出一個OOM的錯誤,你可能會以爲是這個Activity導致的內存泄露,你會想到也有可能是內存有泄露嗎?內存泄露就像一個定時炸彈,隨時都有可能使我們的應用程序崩潰掉,所以作爲一名Android開發人員,還是需要有分析內存泄露的能力,說道這裏我們還是要說下什麼是內存泄露,內存泄露是指有個引用指向一個不再被使用的對象,導致該對象不會被垃圾回收器回收。因此,垃圾回收器是無法回收內存泄露的對象。本文就使用DDMS(Dalvik Debug Monitor Server)和MAT(Memory Analyzer Tool)工具帶大家來分析內存泄露問題。


工具的準備

DDMS是ADT自帶的調試工具,有關DDMS的使用請參考http://developer.android.com/tools/debugging/ddms.html,而MAT的就需要我們自行安裝Eclipse插件,安裝方法我就不多說了,下面給出一個在線安裝的地址:http://download.eclipse.org/mat/1.3/update-site/,MAT可以檢測到內存泄露,降低內存消耗,它有着非常強大的解析堆內存空間dump能力。


如何檢測內存泄露

1.使用DDMS檢測內存泄露

打開Devices視圖,選擇我們需要分析的應用程序進程,點擊Updata Heap按鈕


然後在打開DDMS, 選擇Heap標籤,然後點擊Cause GC按鈕,點擊Cause GC是手動觸發JAVA垃圾回收器,如下圖


如果我們要測試某個Activity是否發生內存泄露,我們可以反覆進入和退出這個Activity, 再手動觸發幾次垃圾回收,觀察上圖中 data object這一欄中的 Total Size的大小是保持穩定還是有明顯的變大趨勢,如果有明顯的變大趨勢就說明這個Activity存在內存泄露的問題,我們就需要在具體分析。


2.使用Logcat檢測內存泄露

當垃圾回收機在進行垃圾回收之後,會在Logcat中作相對於的輸出,所以我們也可以通過這些信息來判斷是否存在內存泄露問題

一,上面消息的第一個部分產生GC的原因,一共有四種類型
GC_CONCURRENT   當你的堆內存快被用完的時候,就會觸發這個GC回收
GC_FOR_MALLOC  堆內存已經滿了,同時又要試圖分配新的內存,所以系統要回收內存
GC_EXTERNAL_ALLOC   在Android3.0 (Honeycomb)以前,釋放通過外部內存(比如在2.3以前,產生的Bitmap對象存儲在Native Memory中)時產生。Android3.0和更高版本中不再有這種類型的內存分配了。
GC_EXPLICIT  調用System.gc時產生,上圖中就是點擊Cause GC按鈕手動觸發垃圾回收器產生的log信息

二,freed 1413K表示GC釋放了1434K的內存

三,20% free 9349K/11644K, 20%表示目前可分配內存佔的比例,9349K表示當前活動對象所佔內存,11644K表示Heap的大小

四,paused 8ms + 3ms, total 71ms,則表示觸發GC應用暫停的時間和GC總共消耗的時間

有了這些log信息,我們就可以知道GC運行幾次以後有沒有成功釋放出一些內存,如果分配出去的內存在持續增加,那麼很明顯存在內存泄露,如下存在內存泄露的Log信息

很明顯Heap中空閒內存佔總Heap的比例在縮小,Heap中活動對象所佔的內存在增加。


內存泄露分析實戰

下面是一個存在內存泄露的例子代碼,這也是比較常見的一種內存泄露的方式,就是在Activity中寫一些內部類,並且這些內部類具有生命週期過長的現象

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. package com.example.memoryleak;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. import android.app.Activity;  
  7. import android.os.Bundle;  
  8.   
  9. public class LeakActivity extends Activity {  
  10.     private List<String> list = new ArrayList<String>();  
  11.       
  12.   
  13.     @Override  
  14.     protected void onCreate(Bundle savedInstanceState) {  
  15.         super.onCreate(savedInstanceState);  
  16.           
  17.         //模擬Activity一些其他的對象  
  18.         for(int i=0; i<10000;i++){  
  19.             list.add("Memory Leak!");  
  20.         }  
  21.           
  22.         //開啓線程  
  23.         new MyThread().start();  
  24.           
  25.     }  
  26.       
  27.       
  28.     public class MyThread extends Thread{  
  29.   
  30.         @Override  
  31.         public void run() {  
  32.             super.run();  
  33.               
  34.             //模擬耗時操作  
  35.             try {  
  36.                 Thread.sleep(10 * 60 * 1000);  
  37.             } catch (InterruptedException e) {  
  38.                 e.printStackTrace();  
  39.             }  
  40.               
  41.         }  
  42.           
  43.           
  44.     }  
  45. }  
運行例子代碼,選擇Devices視圖,點擊上面Updata Heap標籤,然後再旋轉屏幕,多重複幾次,然後點擊Dump HPROF file, 之後Eclipse的MAT插件會自動幫我們打開,如下圖


我們看到下面有Histogram(直方圖)他列舉了每個對象的統計,Dominator Tree(支配樹)提供了程序中最佔內存的對象的排列,這兩個是我在排查內存泄露的時候用的最多的

Histogram(直方圖)

我們先來看Histogram, MAT最有用的工具之一,它可以列出任意一個類的實例數。它支持使用正則表達式來查找某個特定的類,還可以計算出該類所有對象的保留堆最小值或者精確值, 我們可以通過正則表達式輸入LeakActivity, 看到Histogram列出了與LeakActivity相關的類


我們可以看到LeakActivity,和MyThread內部類都存在16個對象,雖然LeakActivity和MyThread存在那麼多對象,但是到這裏並不能讓我們準確的判斷這兩個對象是否存在內存泄露問題, 選中com.example.memoryleak.LeakActivity,點擊右鍵,如下圖


Merge Shortest Paths to GC Roots 可以查看一個對象到RC  Roots是否存在引用鏈相連接, 在JAVA中是通過可達性(Reachability Analysis)來判斷對象是否存活,這個算法的基本思想是通過一系列的稱謂"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,搜索所走得路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連則該對象被判定爲可以被回收的對象,反之不能被回收,我們可以選擇 exclude all phantom/weak/soft etc.references(排查虛引用/弱引用/軟引用等)因爲被虛引用/弱引用/軟引用的對象可以直接被GC給回收.


可以看到LeakActivity存在GC Roots鏈,即存在內存泄露問題,可以看到LeakActivity被MyThread的this$0持有。

除了使用Merge Shortest Paths to GC Roots 我們還可以使用

List object - With outgoing References   顯示選中對象持有那些對象

List object - With incoming References  顯示選中對象被那些外部對象所持有

Show object by class - With outgoing References  顯示選中對象持有哪些對象, 這些對象按類合併在一起排序

Show object by class - With incoming References  顯示選中對象被哪些外部對象持有, 這些對象按類合併在一起排序


Dominator Tree(支配樹)

它可以將所有對象按照Heap大小排序顯示, 使用方法跟Histogram(直方圖)差不多,在這裏我就不做過多的介紹了


我們知道上面的例子代碼中我們知道內部類會持有外部類的引用,如果內部類的生命週期過長,會導致外部類內存泄露,那麼你會問,我們應該怎麼寫那不會出現內存泄露的問題呢?既然內部類不行,我們就外部類或者static的內部類,如果我們需要用到外部類裏面的一些東西,我們可以將外部類Weak Reference傳遞進去

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. package com.example.memoryleak;  
  2.   
  3. import java.lang.ref.WeakReference;  
  4. import java.util.ArrayList;  
  5. import java.util.List;  
  6.   
  7. import android.app.Activity;  
  8. import android.os.Bundle;  
  9.   
  10. public class LeakActivity extends Activity {  
  11.     private List<String> list = new ArrayList<String>();  
  12.       
  13.   
  14.     @Override  
  15.     protected void onCreate(Bundle savedInstanceState) {  
  16.         super.onCreate(savedInstanceState);  
  17.           
  18.         //模擬Activity一些其他的對象  
  19.         for(int i=0; i<10000;i++){  
  20.             list.add("Memory Leak!");  
  21.         }  
  22.           
  23.         //開啓線程  
  24.         new MyThread(this).start();  
  25.           
  26.     }  
  27.       
  28.       
  29.     public static class MyThread extends Thread{  
  30.         private WeakReference<LeakActivity> mLeakActivityRef;  
  31.           
  32.         public MyThread(LeakActivity activity){  
  33.             mLeakActivityRef = new WeakReference<LeakActivity>(activity);  
  34.         }  
  35.   
  36.         @Override  
  37.         public void run() {  
  38.             super.run();  
  39.               
  40.             //模擬耗時操作  
  41.             try {  
  42.                 Thread.sleep(10 * 60 * 1000);  
  43.             } catch (InterruptedException e) {  
  44.                 e.printStackTrace();  
  45.             }  
  46.               
  47.             //如果需要使用LeakActivity,我們需要添加一個判斷  
  48.             LeakActivity activity = mLeakActivityRef.get();  
  49.             if(activity != null){  
  50.                 //do something  
  51.             }  
  52.               
  53.         }  
  54.           
  55.           
  56.     }  
  57. }  

同理,Handler也存在同樣的問題,比如下面的代碼

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. package com.example.memoryleak;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.os.Handler;  
  6. import android.os.Message;  
  7.   
  8. public class LeakActivity extends Activity {  
  9.   
  10.     @Override  
  11.     protected void onCreate(Bundle savedInstanceState) {  
  12.         super.onCreate(savedInstanceState);  
  13.           
  14.         MyHandler handler = new MyHandler();  
  15.         handler.sendMessageDelayed(Message.obtain(), 10 * 60 * 1000);  
  16.     }  
  17.       
  18.       
  19.     public class MyHandler extends Handler{  
  20.   
  21.         @Override  
  22.         public void handleMessage(Message msg) {  
  23.             super.handleMessage(msg);  
  24.         }  
  25.     }  
  26.           
  27. }  
我們知道使用MyHandler發送消息的時候,Message會被加入到主線程的MessageQueue裏面,而每條Message的target會持有MyHandler對象,而MyHandler的this$0又會持有LeakActivity對象, 所以我們在旋轉屏幕的時候,由於每條Message被延遲了 10分鐘,所以必然會導致LeakActivity泄露,所以我們需要將代碼進行修改下

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. package com.example.memoryleak;  
  2.   
  3. import java.lang.ref.WeakReference;  
  4.   
  5. import android.app.Activity;  
  6. import android.os.Bundle;  
  7. import android.os.Handler;  
  8. import android.os.Message;  
  9.   
  10. public class LeakActivity extends Activity {  
  11.     MyHandler handler;  
  12.   
  13.     @Override  
  14.     protected void onCreate(Bundle savedInstanceState) {  
  15.         super.onCreate(savedInstanceState);  
  16.           
  17.         handler = new MyHandler(this);  
  18.         handler.sendMessageDelayed(Message.obtain(), 10 * 60 * 1000);  
  19.     }  
  20.       
  21.       
  22.     public static class MyHandler extends Handler{  
  23.         public WeakReference<LeakActivity> mLeakActivityRef;  
  24.           
  25.         public MyHandler(LeakActivity leakActivity) {  
  26.             mLeakActivityRef = new WeakReference<LeakActivity>(leakActivity);  
  27.         }  
  28.   
  29.         @Override  
  30.         public void handleMessage(Message msg) {  
  31.             super.handleMessage(msg);  
  32.               
  33.             if(mLeakActivityRef.get() != null){  
  34.                 //do something  
  35.             }  
  36.         }  
  37.     }  
  38.   
  39.   
  40.     @Override  
  41.     protected void onDestroy() {  
  42.         handler.removeCallbacksAndMessages(null);  
  43.         super.onDestroy();  
  44.     }  
  45.           
  46. }  

上面的代碼就能保證LeakActivity不會被泄露,注意我們在Activity的onDestory方法中使用了handler.removeCallbacksAndMessages(null),這樣子能保證LeakActivity退出的時候,每條Message的target  MyHandler也會被釋放, 所以我們在使用非static的內部類的時候,要注意該內部類的生命週期是否比外部類要長,如果是的話我們可以使用上面的解決方法。


常見的內存泄露問題

1.上面兩種情形

2.資源對象沒有關閉,比如數據庫操作中得Cursor,IO操作的對象

3.調用了registerReceiver註冊廣播後未調用unregisterReceiver()來取消

4.調用了View.getViewTreeObserver().addOnXXXListener ,而沒有調用View.getViewTreeObserver().removeXXXListener

5.Android 3.0以前,沒有對不在使用的Bitmap調用recycle(),當然在Android 3.0以後就不需要了,更詳細的請查看http://blog.csdn.net/xiaanming/article/details/41084843

6.Context的泄露,比如我們在單例類中使用Context對象,如下

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. import android.content.Context;  
  2.   
  3. public class Singleton {  
  4.     private Context context;  
  5.     private static Singleton mSingleton;  
  6.       
  7.     private Singleton(Context context){  
  8.         this.context = context;  
  9.     }  
  10.       
  11.     public static Singleton getInstance(Context context){  
  12.         if(mSingleton == null){  
  13.             synchronized (Singleton.class) {  
  14.                 if(mSingleton == null){  
  15.                     mSingleton = new Singleton(context);  
  16.                 }  
  17.             }  
  18.         }  
  19.           
  20.         return mSingleton;  
  21.     }  
  22.   
  23. }  

假如我們在某個Activity中使用Singleton.getInstance(this)或者該實例,那麼會造成該Activity一直被Singleton對象引用着,所以這時候我們應該使用getApplicationContext()來代替Activity的Context,getApplicationContext()獲取的Context是一個全局的對象,所以這樣就避免了內存泄露。相同的還有將Context成員設置爲static也會導致內存泄露問題。

7.不要重寫finalize()方法,我們有時候可能會在某個對象被回收前去釋放一些資源,可能會在finalize()方法中去做,但是實現了finalize的對象,創建和回收的過程都更耗時。創建時,會新建一個額外的Finalizer 對象指向新創建的對象。 而回收時,至少需要經過兩次GC,第一次GC檢測到對象只有被Finalizer引用,將這個對象放入 Finalizer.ReferenceQueue 此時,因爲Finalizer的引用,對象還無法被GC,
Finalizer$FinalizerThread 會不停的清理Queue的對象,remove掉當前元素,並執行對象的finalize方法,清理後對象沒有任何引用,在下一次GC被回收,所以說該對象存活時間更久,導致內存泄露。


發佈了23 篇原創文章 · 獲贊 4 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章