Android 記憶體洩漏總結

JosenQiao發表於2017-11-12

Android 記憶體洩漏總結

摘要: Android 記憶體洩漏總結 記憶體管理的目的就是讓我們在開發中怎麼有效的避免我們的應用出現記憶體洩漏的問題。記憶體洩漏大家都不陌生了,簡單粗俗的講,就是該被釋放的物件沒有釋放,一直被某個或某些例項所持有卻不再被使用導致 GC 不能回收。最近自己閱讀了大量相關的文件資料,打算做個 總結 沉澱下來跟大家一

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的應用場景如下:

img
img

其中: 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使用了匿名內部類。我們來看看執行時這兩個引用的記憶體:

img
img

可以看到,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 四種。

img
img

在Android應用的開發中,為了防止記憶體溢位,在處理一些佔用記憶體大而且宣告週期較長的物件時候,可以儘量應用軟引用和弱引用技術。

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

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

首先定義一個HashMap,儲存軟引用物件。

private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();複製程式碼

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

img
img

使用軟引用以後,在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”按鈕,到此為止需檢測的程式就可以被監視。

    img
    img

    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,分析懷疑洩露的類:

    img
    img

    B)分析持有此類物件引用的外部物件

    img
    img

    C)分析這些持有引用的物件的GC路徑

    img
    img

    D)逐個分析每個物件的GC路徑是否正常

    img
    img

    從這個路徑可以看出是一個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 .

    img
    img

    D)開啟第二個HPROF 檔案然後重做步驟2和3.

    E)切換到Compare Basket view, 然後點選Compare the Results (檢視右上角的紅色”!”圖示)。

    img
    img

    F)分析對比結果

    img
    img

    可以看出兩個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);
  }
}複製程式碼

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

img
img

以很直白的方式將記憶體洩露展現在我們的面前。

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銷燬時及時關閉或者登出。
  • 保持對物件生命週期的敏感,特別注意單例、靜態物件、全域性性集合等的生命週期。

相關文章