Android 內存泄漏總結

Android 內存泄漏總結

內存管理的目的就是讓我們在開發中怎麼有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,就是該被釋放的對象沒有釋放,一直被某個或某些實例所持有卻不再被使用導致 GC 不能回收。最近自己閱讀了大量相關的文檔資料,打算做個 總結 沉澱下來跟大家一起分享和學習,也給自己一個警示,以後 coding 時怎麼避免這些情況,提高應用的體驗和質量。

我會從 java 內存泄漏的基礎知識開始,並通過具體例子來說明 Android 引起內存泄漏的各種原因,以及如何利用工具來分析應用內存泄漏,最後再做總結。

篇幅有些長,大家可以分幾節來看!

Java 內存分配策略

Java 程序運行時的內存分配策略有三種,分別是靜態分配,棧式分配,和堆式分配,對應的,三種存儲策略使用的內存空間主要分別是靜態存儲區(也稱方法區)、棧區和堆區。

  • 靜態存儲區(方法區):主要存放靜態數據、全局 static 數據和常量。這塊內存在程序編譯時就已經分配好,並且在程序整個運行期間都存在。
  • 棧區 :當方法被執行時,方法體內的局部變量都在棧上創建,並在方法執行結束時這些局部變量所持有的內存將會自動被釋放。因爲棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
  • 堆區 : 又稱動態內存分配,通常就是指在程序運行時直接 new 出來的內存。這部分內存在不使用時將會由 Java 垃圾回收器來負責回收。

棧與堆的區別:

在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的。當在一段方法塊中定義一個變量時,Java 就會在棧中爲該變量分配內存空間,當超過該變量的作用域後,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間可以被重新使用。

堆內存用來存放所有由 new 創建的對象(包括該對象其中的所有成員變量)和數組。在堆中分配的內存,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者對象後,還可以在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,這個特殊的變量就是我們上面說的引用變量。我們可以通過這個引用變量來訪問堆中的對象或者數組。

舉個例子:

public class Sample() {

    int s1 = 0;

    Sample mSample1 = new Sample();


    public void method() {

        int s2 = 1;

        Sample mSample2 = new Sample();

    }

}


Sample mSample3 = new Sample();


Sample 類的局部變量 s2 和引用變量 mSample2 都是存在於棧中,但 mSample2 指向的對象是存在於堆上的。

mSample3 指向的對象實體存放在堆上,包括這個對象的所有成員變量 s1 和 mSample1,而它自己存在於棧中。

結論:

局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中。—— 因爲它們屬於方法中的變量,生命週期隨方法而結束。

成員變量全部存儲與堆中(包括基本數據類型,引用和引用的對象實體)—— 因爲它們屬於類,類對象終究是要被new出來使用的。

瞭解了 Java 的內存分配之後,我們再來看看 Java 是怎麼管理內存的。

Java是如何管理內存

Java的內存管理就是對象的分配和釋放問題。在 Java 中,程序員需要通過關鍵字 new 爲每個對象申請內存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由 GC 決定和執行的。在 Java 中,內存的分配是由程序完成的,而內存的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程序員的工作。但同時,它也加重了JVM的工作。這也是 Java 程序運行速度較慢的原因之一。因爲,GC 爲了能夠正確釋放對象,GC 必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC 都需要進行監控。

監視對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。

爲了更好理解 GC 的工作原理,我們可以將對象考慮爲有向圖的頂點,將引用關係考慮爲圖的有向邊,有向邊從引用者指向被引對象。另外,每個線程對象可以作爲一個圖的起始頂點,例如大多程序從 main 進程開始執行,那麼該圖就是以 main 進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖爲有向圖),那麼我們認爲這個(這些)對象不再被引用,可以被 GC 回收。

以下,我們舉一個例子說明如何用有向圖表示內存管理。對於程序的每一個時刻,我們都有一個有向圖表示JVM的內存分配情況。以下右圖,就是左邊程序運行到第6行的示意圖。


Java使用有向圖的方式進行內存管理,可以消除引用循環的問題,例如有三個對象,相互引用,只要它們和根進程不可達的,那麼GC也是可以回收它們的。這種方式的優點是管理內存的精度很高,但是效率較低。另外一種常用的內存管理技術是使用計數器,例如COM模型採用計數器方式管理構件,它與有向圖相比,精度行低(很難處理循環引用的問題),但執行效率很高。

什麼是Java中的內存泄露

在Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程序以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定爲Java中的內存泄漏,這些對象不會被GC所回收,然而它卻佔用內存。

在C++中,內存泄漏的範圍更大一些。有些對象被分配了內存空間,然後卻不可達,由於C++中沒有GC,這些內存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,因此程序員不需要考慮這部分的內存泄露。

通過分析,我們得知,對於C++,程序員需要自己管理邊和頂點,而對於Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了編程的效率。


因此,通過以上分析,我們知道在Java中也有內存泄漏,但範圍比C++要小一些。因爲Java從語言上保證,任何對象都是可達的,所有的不可達對象都由GC管理。

對於程序員來說,GC基本是透明的,不可見的。雖然,我們只有幾個函數可以訪問GC,例如運行GC的函數System.gc(),但是根據Java語言規範定義, 該函數不保證JVM的垃圾收集器一定會執行。因爲,不同的JVM實現者可能使用不同的算法管理GC。通常,GC的線程的優先級別較低。JVM調用GC的策略也有很多種,有的是內存使用到達一定程度時,GC纔開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對於基於Web的實時系統,如網絡遊戲等,用戶不希望GC突然中斷應用程序執行而進行垃圾回收,那麼我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存,例如將垃圾回收分解爲一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。

同樣給出一個 Java 內存泄漏的典型例子,

Vector v = new Vector(10);

for (int i = 1; i < 100; i++) {

    Object o = new Object();

    v.add(o);

    o = null;   

}

在這個例子中,我們循環申請Object對象,並將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身,那麼 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 後,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置爲 null。

Android中常見的內存泄漏彙總

  • 集合類泄漏
    集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被佔用。如果這個集合類是全局性的變量 (比如類中的靜態屬性,全局性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,很可能導致集合所佔用的內存只增不減。比如上面的典型例子就是其中一種情況,當然實際上我們在項目中肯定不會寫這麼 2B 的代碼,但稍不注意還是很容易出現這種情況,比如我們都喜歡通過 HashMap 做一些緩存之類的事,這種情況就要多留一些心眼。
  • 單例造成的內存泄漏
    由於單例的靜態特性使得其生命週期跟應用的生命週期一樣長,所以如果使用不恰當的話,很容易造成內存泄漏。比如下面一個典型的例子,

public class AppManager {

private static AppManager instance;

private Context context;

private AppManager(Context context) {

this.context = context;

}

public static AppManager getInstance(Context context) {

if (instance != null) {

instance = new AppManager(context);

}

return instance;

}

}


這是一個普通的單例模式,當創建這個單例的時候,由於需要傳入一個Context,所以這個Context的生命週期的長短至關重要:

1、如果此時傳入的是 Application 的 Context,因爲 Application 的生命週期就是整個應用的生命週期,所以這將沒有任何問題。

2、如果此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例對象所持有,其生命週期等於整個應用程序的生命週期,所以當前 Activity 退出時它的內存並不會被回收,這就造成泄漏了。

正確的方式應該改爲下面這種方式:

public class AppManager {

private static AppManager instance;

private Context context;

private AppManager(Context context) {

this.context = context.getApplicationContext();// 使用Application 的context

}

public static AppManager getInstance(Context context) {

if (instance != null) {

instance = new AppManager(context);

}

return instance;

}

}


或者這樣寫,連 Context 都不用傳進來了:

在你的 Application 中添加一個靜態方法,getContext() 返回 Application 的 context,


...


context = getApplicationContext();


...

   /**

     * 獲取全局的context

     * @return 返回全局context對象

     */

    public static Context getContext(){

        return context;

    }


public class AppManager {

private static AppManager instance;

private Context context;

private AppManager() {

this.context = MyApplication.getContext();// 使用Application 的context

}

public static AppManager getInstance() {

if (instance != null) {

instance = new AppManager();

}

return instance;

}

}


  • 匿名內部類/非靜態內部類和異步線程
    • 非靜態內部類創建靜態實例造成的內存泄漏
      有的時候我們可能會在啓動頻繁的Activity中,爲了避免重複創建相同的數據資源,可能會出現這種寫法:
      public class MainActivity extends AppCompatActivity {
    • private static TestResource mResource = null;
    • @Override
    • protected void onCreate(Bundle savedInstanceState) {
    • super.onCreate(savedInstanceState);
    • setContentView(R.layout.activity_main);
    • if(mManager == null){
    • mManager = new TestResource();
    • }
    • //...
    • }
    • class TestResource {
    • //...
    • }
    • }

這樣就在Activity內部創建了一個非靜態內部類的單例,每次啓動Activity時都會使用該單例的數據,這樣雖然避免了資源的重複創建,不過這種寫法卻會造成內存泄漏,因爲非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建了一個靜態的實例,該實例的生命週期和應用的一樣長,這就導致了該靜態實例一直會持有該Activity的引用,導致Activity的內存資源不能正常回收。正確的做法爲:

將該內部類設爲靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context,對於Application,Service,Activity三者的Context的應用場景如下:


其中: NO1表示 Application 和 Service 可以啓動一個 Activity,不過需要創建一個新的 task 任務隊列。而對於 Dialog 而言,只有在 Activity 中才能創建

  • 匿名內部類
    android開發經常會繼承實現Activity/Fragment/View,此時如果你使用了匿名類,並被異步線程持有了,那要小心了,如果沒有任何措施這樣一定會導致泄露
    public class MainActivity extends Activity {
  • ...
  • Runnable ref1 = new MyRunable();
  • Runnable ref2 = new Runnable() {
  •     @Override
  •     public void run() {
  •     }
  • };
  •   ...
  • }

ref1和ref2的區別是,ref2使用了匿名內部類。我們來看看運行時這兩個引用的內存:


可以看到,ref1沒什麼特別的。

但ref2這個匿名類的實現對象裏面多了一個引用:

this$0這個引用指向MainActivity.this,也就是說當前的MainActivity實例會被ref2持有,如果將這個引用再傳入一個異步線程,此線程和此Acitivity生命週期不一致的時候,就造成了Activity的泄露。

  • Handler 造成的內存泄漏
    Handler 的使用造成的內存泄漏問題應該說是最爲常見了,很多時候我們爲了避免 ANR 而不在主線程進行耗時操作,在處理網絡任務或者封裝一些請求回調等api都藉助Handler來處理,但 Handler 不是萬能的,對於 Handler 的使用代碼編寫一不規範即有可能造成內存泄漏。另外,我們知道 Handler、Message 和 MessageQueue 都是相互關聯在一起的,萬一 Handler 發送的 Message 尚未被處理,則該 Message 及發送它的 Handler 對象將被線程 MessageQueue 一直持有。
    由於 Handler 屬於 TLS(Thread Local Storage) 變量, 生命週期和 Activity 是不一致的。因此這種實現方式一般很難保證跟 View 或者 Activity 的生命週期保持一致,故很容易導致無法正確釋放。
    舉個例子:
    public class SampleActivity extends Activity {
  • private final Handler mLeakyHandler = new Handler() {
  • @Override
  • public void handleMessage(Message msg) {
  •   // ...
  • }
  • }
  • @Override
  • protected void onCreate(Bundle savedInstanceState) {
  • super.onCreate(savedInstanceState);
  • // Post a message and delay its execution for 10 minutes.
  • mLeakyHandler.postDelayed(new Runnable() {
  •   @Override
  •   public void run() { /* ... */ }
  • }, 1000 * 60 * 10);
  • // Go back to the previous Activity.
  • finish();
  • }
  • }

在該 SampleActivity 中聲明瞭一個延遲10分鐘執行的消息 Message,mLeakyHandler 將其 push 進了消息隊列 MessageQueue 裏。當該 Activity 被 finish() 掉時,延遲執行任務的 Message 還會繼續存在於主線程中,它持有該 Activity 的 Handler 引用,所以此時 finish() 掉的 Activity 就不會被回收了從而造成內存泄漏(因 Handler 爲非靜態內部類,它會持有外部類的引用,在這裏就是指 SampleActivity)。

修復方法:在 Activity 中避免使用非靜態內部類,比如上面我們將 Handler 聲明爲靜態的,則其存活期跟 Activity 的生命週期就無關了。同時通過弱引用的方式引入 Activity,避免直接將 Activity 作爲 context 傳進去,見下面代碼:

public class SampleActivity extends Activity {


  /**

   * Instances of static inner classes do not hold an implicit

   * reference to their outer class.

   */

  private static class MyHandler extends Handler {

    private final WeakReference<SampleActivity> mActivity;


    public MyHandler(SampleActivity activity) {

      mActivity = new WeakReference<SampleActivity>(activity);

    }


    @Override

    public void handleMessage(Message msg) {

      SampleActivity activity = mActivity.get();

      if (activity != null) {

        // ...

      }

    }

  }


  private final MyHandler mHandler = new MyHandler(this);


  /**

   * Instances of anonymous classes do not hold an implicit

   * reference to their outer class when they are "static".

   */

  private static final Runnable sRunnable = new Runnable() {

      @Override

      public void run() { /* ... */ }

  };


  @Override

  protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);


    // Post a message and delay its execution for 10 minutes.

    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);


    // Go back to the previous Activity.

    finish();

  }

}


綜述,即推薦使用靜態內部類 + WeakReference 這種方式。每次使用前注意判空。

前面提到了 WeakReference,所以這裏就簡單的說一下 Java 對象的幾種引用類型。

Java對引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。


在Android應用的開發中,爲了防止內存溢出,在處理一些佔用內存大而且聲明週期較長的對象時候,可以儘量應用軟引用和弱引用技術。

軟/弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而爲緩衝器清除已失效的軟/弱引用。

假設我們的應用會用到大量的默認圖片,比如應用中有默認的頭像,默認遊戲圖標等等,這些圖片很多地方會用到。如果每次都去讀取圖片,由於讀取文件需要硬件操作,速度較慢,會導致性能較低。所以我們考慮將圖片緩存起來,需要的時候直接從內存中讀取。但是,由於圖片佔用內存空間比較大,緩存很多圖片需要很多的內存,就可能比較容易發生OutOfMemory異常。這時,我們可以考慮使用軟/弱引用技術來避免這個問題發生。以下就是高速緩衝器的雛形:

首先定義一個HashMap,保存軟引用對象。

private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();

再來定義一個方法,保存Bitmap的軟引用到HashMap。


使用軟引用以後,在OutOfMemory異常發生之前,這些緩存的圖片資源的內存空間可以被釋放掉的,從而避免內存達到上限,避免Crash發生。

如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的性能更在意,想盡快回收一些佔用內存比較大的對象,則可以使用弱引用。

另外可以根據對象是否經常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經常使用的,就儘量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。

ok,繼續回到主題。前面所說的,創建一個靜態Handler內部類,然後對 Handler 持有的對象使用弱引用,這樣在回收時也可以回收 Handler 持有的對象,但是這樣做雖然避免了 Activity 泄漏,不過 Looper 線程的消息隊列中還是可能會有待處理的消息,所以我們在 Activity 的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息。

下面幾個方法都可以移除 Message:

public final void removeCallbacks(Runnable r);


public final void removeCallbacks(Runnable r, Object token);


public final void removeCallbacksAndMessages(Object token);


public final void removeMessages(int what);


public final void removeMessages(int what, Object object);


  • 儘量避免使用 static 成員變量
    如果成員變量被聲明爲 static,那我們都知道其生命週期將與整個app進程生命週期一樣。
    這會導致一系列問題,如果你的app進程設計上是長駐內存的,那即使app切到後臺,這部分內存也不會被釋放。按照現在手機app內存管理機制,佔內存較大的後臺進程將優先回收,yi'wei如果此app做過進程互保保活,那會造成app在後臺頻繁重啓。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。
    這裏修復的方法是:

不要在類初始時初始化靜態成員。可以考慮lazy初始化。

架構設計上要思考是否真的有必要這樣做,儘量避免。如果架構需要這麼設計,那麼此對象的生命週期你有責任管理起來。

  • 避免 override finalize()
    1、finalize 方法被執行的時間不確定,不能依賴與它來釋放緊缺的資源。時間不確定的原因是:
    • 虛擬機調用GC的時間不確定
    • Finalize daemon線程被調度到的時間不確定
  • 2、finalize 方法只會被執行一次,即使對象被複活,如果已經執行過了 finalize 方法,再次被 GC 時也不會再執行了,原因是:
    含有 finalize 方法的 object 是在 new 的時候由虛擬機生成了一個 finalize reference 在來引用到該Object的,而在 finalize 方法執行的時候,該 object 所對應的 finalize Reference 會被釋放掉,即使在這個時候把該 object 復活(即用強引用引用住該 object ),再第二次被 GC 的時候由於沒有了 finalize reference 與之對應,所以 finalize 方法不會再執行。
    3、含有Finalize方法的object需要至少經過兩輪GC纔有可能被釋放。
    詳情見這裏 深入分析過dalvik的代碼
  • 資源未關閉造成的內存泄漏
    對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄漏。
  • 一些不良代碼造成的內存壓力
    有些代碼並不造成內存泄露,但是它們,或是對沒使用的內存沒進行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內存。
    比如:
    • Bitmap 沒調用 recycle()方法,對於 Bitmap 對象在不使用時,我們應該先調用 recycle() 釋放內存,然後才它設置爲 null. 因爲加載 Bitmap 對象的內存空間,一部分是 java 的,一部分 C 的(因爲 Bitmap 分配的底層是通過 JNI 調用的 )。 而這個 recyle() 就是針對 C 部分的內存釋放。
    • 構造 Adapter 時,沒有使用緩存的 convertView ,每次都在創建新的 converView。這裏推薦使用 ViewHolder。

工具分析

Java 內存泄漏的分析工具有很多,但衆所周知的要數 MAT(Memory Analysis Tools) 和 YourKit 了。由於篇幅問題,我這裏就只對 MAT 的使用做一下介紹。--> MAT 的安裝

  • MAT分析heap的總內存佔用大小來初步判斷是否存在泄露
    打開 DDMS 工具,在左邊 Devices 視圖頁面選中“Update Heap”圖標,然後在右邊切換到 Heap 視圖,點擊 Heap 視圖中的“Cause GC”按鈕,到此爲止需檢測的進程就可以被監視。

    Heap視圖中部有一個Type叫做data object,即數據對象,也就是我們的程序中大量存在的類類型的對象。在data object一行中有一列是“Total Size”,其值就是當前進程中所有Java數據對象的內存總量,一般情況下,這個值的大小決定了是否會有內存泄漏。可以這樣判斷:
    進入某應用,不斷的操作該應用,同時注意觀察data object的Total Size值,正常情況下Total Size值都會穩定在一個有限的範圍內,也就是說由於程序中的的代碼良好,沒有造成對象不被垃圾回收的情況。
    所以說雖然我們不斷的操作會不斷的生成很多對象,而在虛擬機不斷的進行GC的過程中,這些對象都被回收了,內存佔用量會會落到一個穩定的水平;反之如果代碼中存在沒有釋放對象引用的情況,則data object的Total Size值在每次GC後不會有明顯的回落。隨着操作次數的增多Total Size的值會越來越大,直到到達一個上限後導致進程被殺掉。
  • MAT分析hprof來定位內存泄露的原因所在
    這是出現內存泄露後使用MAT進行問題定位的有效手段。
    A)Dump出內存泄露當時的內存鏡像hprof,分析懷疑泄露的類:

    B)分析持有此類對象引用的外部對象

    C)分析這些持有引用的對象的GC路徑

    D)逐個分析每個對象的GC路徑是否正常

    從這個路徑可以看出是一個antiRadiationUtil工具類對象持有了MainActivity的引用導致MainActivity無法釋放。此時就要進入代碼分析此時antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context導致節目退出後MainActivity無法銷燬,那一般都屬於內存泄露了)。
  • MAT對比操作前後的hprof來定位內存泄露的根因所在
    爲查找內存泄漏,通常需要兩個 Dump結果作對比,打開 Navigator History面板,將兩個表的 Histogram結果都添加到 Compare Basket中去
    A) 第一個HPROF 文件(usingFile > Open Heap Dump ).
    B)打開Histogram view.
    C)在NavigationHistory view裏 (如果看不到就從Window >show view>MAT- Navigation History ), 右擊histogram然後選擇Add to Compare Basket .

    D)打開第二個HPROF 文件然後重做步驟2和3.
    E)切換到Compare Basket view, 然後點擊Compare the Results (視圖右上角的紅色”!”圖標)。

    F)分析對比結果

    可以看出兩個hprof的數據對象對比結果。
    通過這種方式可以快速定位到操作前後所持有的對象增量,從而進一步定位出當前操作導致內存泄露的具體原因是泄露了什麼數據對象。
    注意:
    如果是用 MAT Eclipse 插件獲取的 Dump文件,不需要經過轉換則可在MAT中打開,Adt會自動進行轉換。
    而手機SDk Dump 出的文件要經過轉換才能被 MAT識別,Android SDK提供了這個工具 hprof-conv (位於 sdk/tools下)
    首先,要通過控制檯進入到你的 android sdk tools 目錄下執行以下命令:
    ./hprof-conv xxx-a.hprof xxx-b.hprof
    例如 hprof-conv input.hprof out.hprof
    此時才能將out.hprof放在eclipse的MAT中打開。

Ok,下面將給大家介紹一個屌炸天的工具 -- LeakCanary 。

使用 LeakCanary 檢測 Android 的內存泄漏

什麼是 LeakCanary 呢?爲什麼選擇它來檢測 Android 的內存泄漏呢?

別急,讓我來慢慢告訴大家!

LeakCanary 是國外一位大神 Pierre-Yves Ricau 開發的一個用於檢測內存泄露的開源類庫。一般情況下,在對戰內存泄露中,我們都會經過以下幾個關鍵步驟:

1、瞭解 OutOfMemoryError 情況。

2、重現問題。

3、在發生內存泄露的時候,把內存 Dump 出來。

4、在發生內存泄露的時候,把內存 Dump 出來。

5、計算這個對象到 GC roots 的最短強引用路徑。

6、確定引用路徑中的哪個引用是不該有的,然後修復問題。

很複雜對吧?

如果有一個類庫能在發生 OOM 之前把這些事情全部都搞定,然後你只要修復這些問題就好了。LeakCanary 做的就是這件事情。你可以在 debug 包中輕鬆檢測內存泄露。

一起來看這個例子(摘自 LeakCanary 中文使用說明,下面會附上所有的參考文檔鏈接):

class Cat {

}


class Box {

  Cat hiddenCat;

}

class Docker {

    // 靜態變量,將不會被回收,除非加載 Docker 類的 ClassLoader 被回收。

    static Box container;

}


// ...


Box box = new Box();


// 薛定諤之貓

Cat schrodingerCat = new Cat();

box.hiddenCat = schrodingerCat;

Docker.container = box;


創建一個RefWatcher,監控對象引用情況。

// 我們期待薛定諤之貓很快就會消失(或者不消失),我們監控一下

refWatcher.watch(schrodingerCat);


當發現有內存泄露的時候,你會看到一個很漂亮的 leak trace 報告:

  • GC ROOT static Docker.container
  • references Box.hiddenCat
  • leaks Cat instance

我們知道,你很忙,每天都有一大堆需求。所以我們把這個事情弄得很簡單,你只需要添加一行代碼就行了。然後 LeakCanary 就會自動偵測 activity 的內存泄露了。

public class ExampleApplication extends Application {

  @Override public void onCreate() {

    super.onCreate();

    LeakCanary.install(this);

  }

}


然後你會在通知欄看到這樣很漂亮的一個界面:


以很直白的方式將內存泄露展現在我們的面前。

Demo

一個非常簡單的 LeakCanary demo: 一個非常簡單的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo

接入

在 build.gradle 中加入引用,不同的編譯使用不同的引用:

 dependencies {

   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'

   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'

 }


如何使用

使用 RefWatcher 監控那些本該被回收的對象。

RefWatcher refWatcher = {...};


// 監控

refWatcher.watch(schrodingerCat);


LeakCanary.install() 會返回一個預定義的 RefWatcher,同時也會啓用一個 ActivityRefWatcher,用於自動監控調用 Activity.onDestroy() 之後泄露的 activity。

在Application中進行配置 :

public class ExampleApplication extends Application {


  public static RefWatcher getRefWatcher(Context context) {

    ExampleApplication application = (ExampleApplication) context.getApplicationContext();

    return application.refWatcher;

  }


  private RefWatcher refWatcher;


  @Override public void onCreate() {

    super.onCreate();

    refWatcher = LeakCanary.install(this);

  }

}


使用 RefWatcher 監控 Fragment:

public abstract class BaseFragment extends Fragment {


  @Override public void onDestroy() {

    super.onDestroy();

    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());

    refWatcher.watch(this);

  }

}


使用 RefWatcher 監控 Activity:

public class MainActivity extends AppCompatActivity {


    ......

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

            //在自己的應用初始Activity中加入如下兩行代碼

        RefWatcher refWatcher = ExampleApplication.getRefWatcher(this);

        refWatcher.watch(this);


        textView = (TextView) findViewById(R.id.tv);

        textView.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

                startAsyncTask();

            }

        });


    }


    private void async() {


        startAsyncTask();

    }


    private void startAsyncTask() {

        // This async task is an anonymous class and therefore has a hidden reference to the outer

        // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),

        // the activity instance will leak.

        new AsyncTask<Void, Void, Void>() {

            @Override

            protected Void doInBackground(Void... params) {

                // Do some slow work in background

                SystemClock.sleep(20000);

                return null;

            }

        }.execute();

    }



}


工作機制

1.RefWatcher.watch() 創建一個 KeyedWeakReference 到要被監控的對象。

2.然後在後臺線程檢查引用是否被清除,如果沒有,調用GC。

3.如果引用還是未被清除,把 heap 內存 dump 到 APP 對應的文件系統中的一個 .hprof 文件中。

4.在另外一個進程中的 HeapAnalyzerService 有一個 HeapAnalyzer 使用HAHA 解析這個文件。

5.得益於唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位內存泄露。

6.HeapAnalyzer 計算 到 GC roots 的最短強引用路徑,並確定是否是泄露。如果是的話,建立導致泄露的引用鏈。

7.引用鏈傳遞到 APP 進程中的 DisplayLeakService, 並以通知的形式展示出來。

ok,這裏就不再深入了,想要了解更多就到 作者 github 主頁 這去哈。

總結

  • 對 Activity 等組件的引用應該控制在 Activity 的生命週期之內; 如果不能就考慮使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部長生命週期的對象引用而泄露。
  • 儘量不要在靜態變量或者靜態內部類中使用非靜態外部成員變量(包括context ),即使要使用,也要考慮適時把外部成員變量置空;也可以在內部類中使用弱引用來引用外部類的變量。
  • 對於生命週期比Activity長的內部類對象,並且內部類中使用了外部類的成員變量,可以這樣做避免內存泄漏:
    • 將內部類改爲靜態內部類
    • 靜態內部類中使用弱引用來引用外部類的成員變量
  • Handler 的持有的引用對象最好使用弱引用,資源釋放時也可以清空 Handler 裏面的消息。比如在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 對象的 Message和 Runnable.
  • 在 Java 的實現過程中,也要考慮其對象釋放,最好的方法是在不使用某對象時,顯式地將此對象賦值爲 null,比如使用完Bitmap 後先調用 recycle(),再賦爲null,清空對圖片等資源有直接引用或者間接引用的數組(使用 array.clear() ; array = null)等,最好遵循誰創建誰釋放的原則。 
  • 正確關閉資源,對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷。
  • 保持對對象生命週期的敏感,特別注意單例、靜態對象、全局性集合等的生命週期。

以上部分圖片、實例代碼和文段都摘自或參考以下文章 :

支付寶:

Android怎樣coding避免內存泄露

支付寶錢包Android內存治理

IBM :

Java的內存泄漏

Android Design Patterns :

How to Leak a Context: Handlers & Inner Classes

伯樂在線團隊:

Android性能優化之常見的內存泄漏

我廠同學 :

Dalvik虛擬機 Finalize 方法執行分析

騰訊bugly :

內存泄露從入門到精通三部曲之基礎知識篇

內存泄露從入門到精通三部曲之排查方法篇

內存泄露從入門到精通三部曲之常見原因與用戶實踐

LeakCanary :

LeakCanary 中文使用說明

LeakCanary: 讓內存泄露無所遁形

https://github.com/square/leakcanary

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章