1. 程式人生 > 實用技巧 >Android Handler記憶體洩露

Android Handler記憶體洩露

前言

由於Android採取了單執行緒UI模型,開發者無法在子執行緒中更新UI,為此Android為我們提供了Handler這個工具,幫助開發者切換到主執行緒更新UI。在我們開發Android應用程式進行非同步操作時,我們經常會使用到Handler類。通常會寫出如下程式碼
  1. private Handler mHandler = new Handler(){
  2. @Override
  3. public void handleMessage(Message msg){
  4. //do something
  5. }
  6. }
其實上面的程式碼是會產生記憶體洩漏的,如果你有使用Android lint工具的話,它會給我們提示一個警告
In Android, Handler classes should be static or leaks might occur。
翻譯過來就是:在android中,Handler這個類應該被定義成靜態的,否則可能出現記憶體洩漏的情況
先說下什麼是記憶體洩漏:
記憶體洩漏(Memory Leak):是指程式中己動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果。

發生記憶體洩漏的原因

說是記憶體洩漏,那到底如何發生記憶體洩漏的呢?又在哪裡發生的記憶體洩漏?接下來我們探究一下到底是如何發生記憶體洩漏的。
1.當一個Android應用程式啟動的時候,frameworks會自動為這個應用程式在主執行緒建立一個Looper物件。這個被建立的Looper物件它的主要的工作就是不斷地處理訊息佇列中的訊息物件。在Android應用程式中,所有主要的框架事件(例如Activity的生命週期方法,按鈕的點選事件等等)都包含在訊息物件裡面,然後被新增到Looper要處理的訊息佇列中,主執行緒的Looper一直存在於整個應用程式的生命週期中。
2.當一個Handler在主執行緒中被初始化。那它就一直都和Looper的訊息佇列相關聯著。當訊息被髮送到Looper關聯的訊息佇列的時候,會持有一個Handler的引用,以便於當Looper處理訊息的時候,框架可以呼叫Handler的handleMessage(Message msg)。
3.在java中,非靜態的內部類和匿名內部類都會隱式的持有一個外部類的引用。靜態內部類則不會持有外部類的引用。
上面的程式碼確實是很難以發現記憶體洩漏的問題的。那我們來看看下面的程式碼,會更加容易發現問題。
  1. public
    class MainActivity extends AppCompatActivity {
  2. private Handler mLeakHandler = new Handler(){
  3. @Override
  4. public void handleMessage(Message msg) {
  5. }
  6. };
  7. @Override
  8. protected void onCreate(Bundle savedInstanceState) {
  9. super.onCreate(savedInstanceState);
  10. setContentView(R.layout.activity_main);
  11. //傳送延時訊息
  12. mLeakHandler.postDelayed(new Runnable() {
  13. @Override
  14. public void run() {
  15. }
  16. }, 1000 *60 *10);
  17. finish();
  18. }
  19. }

解決辦法

要解決這樣的一個問題,有如下兩種方式:
方法一:通過程式
1.在關閉Activity的時候停掉你的後臺執行緒。執行緒停掉了,就相當於切斷了Handler和外部連線的線,Activity自然會在合適的時候被回收。

2.如果你的Handler是被delay的Message持有了引用,那麼使用相應的Handler方法:removeCallbacks(Runnable r)和removeMessages(int what),在頁面銷燬時把訊息物件從訊息佇列移除就行了

程式碼如下:

  1. @Override
  2. public void onDestroy() {
  3. // 移除所有訊息
  4. handler.removeCallbacksAndMessages(null);
  5. // 或者移除單條訊息
  6. handler.removeMessages(what);
  7. }

方法二:將Handler宣告為靜態類
靜態類不持有外部類的物件,所以你的Activity可以隨意被GC回收。程式碼如下:

  1. static class NoLeakHander extends Handler {
  2. @Override
  3. public void handleMessage(Message msg) {
  4. }
  5. }
以上程式碼中Handler不再持有外部類物件的引用,導致程式不允許你在Handler中操作Activity中的物件了。所以就沒法在Hander中操作UI了,你需要在Handler中增加一個對Activity的弱引用(WeakReference):
首先理解一下相關概念: 強引用(Strong Reference):預設引用。如果一個物件具有強引用,垃圾回收器絕不會回收它。在記憶體空 間不足時, Java虛擬機器寧願 丟擲OutOfMemory的錯誤,使程式異常終止,也不會強引用的物件來解決記憶體不足問題。 如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體。 弱引用(WeakReference):在垃圾回收器一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。 虛引用(PhantomReference):如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

加入弱引用後的程式碼:
  1. public class MainActivity extends AppCompatActivity {
  2. private NoLeakHandler mHandler;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. mHandler = new NoLeakHandler(this);
  7. Message message = Message.obtain();
  8. mHandler.sendMessageDelayed(message, 10 * 60 * 1000);
  9. }
  10. private static class NoLeakHandler extends Handler {
  11. //持有弱引用MainActivity,GC回收時會被回收掉.
  12. private WeakReference<MainActivity> mActivity;
  13. public NoLeakHandler(MainActivity activity) {
  14. mActivity = new WeakReference<>(activity);
  15. }
  16. @Override
  17. public void handleMessage(Message msg) {
  18. super.handleMessage(msg);
  19. }
  20. }
  21. }

閱讀目錄

回到頂部

一、什麼是記憶體洩露?

  Java使用有向圖機制,通過GC自動檢查記憶體中的物件(什麼時候檢查由虛擬機器決定),如果GC發現一個或一組物件為不可到達狀態,則將該物件從記憶體中回收。也就是說,一個物件不被任何引用所指向,則該物件會在被GC發現的時候被回收;另外,如果一組物件中只包含互相的引用,而沒有來自它們外部的引用(例如有兩個物件A和B互相持有引用,但沒有任何外部物件持有指向A或B的引用),這仍然屬於不可到達,同樣會被GC回收。

  Android中使用Handler造成記憶體洩露的原因

private Handler handler = new Handler()
 {
      public void handleMessage(android.os.Message msg)
     { if (msg.what == 1)
        { noteBookAdapter.notifyDataSetChanged(); } } };

  上面是一段簡單的Handler的使用。當使用內部類(包括匿名類)來建立Handler的時候,Handler物件會隱式地持有一個外部類物件(通常是一個Activity)的引用(不然你怎麼可能通過Handler來操作Activity中的View?)。而Handler通常會伴隨著一個耗時的後臺執行緒(例如從網路拉取圖片)一起出現,這個後臺執行緒在任務執行完畢(例如圖片下載完畢)之後,通過訊息機制通知Handler,然後Handler把圖片更新到介面。然而,如果使用者在網路請求過程中關閉了Activity,正常情況下,Activity不再被使用,它就有可能在GC檢查時被回收掉,但由於這時執行緒尚未執行完,而該執行緒持有Handler的引用(不然它怎麼發訊息給Handler?),這個Handler又持有Activity的引用,就導致該Activity無法被回收(即記憶體洩露),直到網路請求結束(例如圖片下載完畢)。另外,如果你執行了Handler的postDelayed()方法,該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,那麼在你設定的delay到達之前,會有一條MessageQueue -> Message -> Handler -> Activity的鏈,導致你的Activity被持有引用而無法被回收。

回到頂部

二、記憶體洩露的危害

  記憶體洩露的危害就是會使虛擬機器佔用記憶體過高,導致OOM(記憶體溢位),程式出錯。

  對於Android應用來說,就是你的使用者開啟一個Activity,使用完之後關閉它,記憶體洩露;又開啟,又關閉,又洩露;幾次之後,程式佔用記憶體超過系統限制,FC。

回到頂部

三、解決方案

使用Handler導致記憶體洩露的解決方法

方法一:通過程式邏輯來進行保護。
1.在關閉Activity的時候停掉你的後臺執行緒。執行緒停掉了,就相當於切斷了Handler和外部連線的線,Activity自然會在合適的時候被回收。
2.如果你的Handler是被delay的Message持有了引用,那麼使用相應的Handler的removeCallbacks()方法,把訊息物件從訊息佇列移除就行了。

方法二:將Handler宣告為靜態類。
PS:在Java 中,非靜態的內部類和匿名內部類都會隱式地持有其外部類的引用,靜態的內部類不會持有外部類的引用。
靜態類不持有外部類的物件,所以你的Activity可以隨意被回收。由於Handler不再持有外部類物件的引用,導致程式不允許你在Handler中操作Activity中的物件了。所以你需要在Handler中增加一個對Activity的弱引用(WeakReference)。

程式碼如下:

static class MyHandler extends Handler
    {
        WeakReference<Activity> mWeakReference;
        public MyHandler(Activity activity) 
        {
            mWeakReference=new WeakReference<Activity>(activity);
        }
        @Override
        public void handleMessage(Message msg)
        {
            final Activity activity=mWeakReference.get();
            if(activity!=null)
            {
                if (msg.what == 1)
                {
                    noteBookAdapter.notifyDataSetChanged();
                }
            }
        }
    }

  PS:什麼是WeakReference?
  WeakReference弱引用,與強引用(即我們常說的引用)相對,它的特點是,GC在回收時會忽略掉弱引用,即就算有弱引用指向某物件,但只要該物件沒有被強引用指向(實際上多數時候還要求沒有軟引用,但此處軟引用的概念可以忽略),該物件就會在被GC檢查到時回收掉。對於上面的程式碼,使用者在關閉Activity之後,就算後臺執行緒還沒結束,但由於僅有一條來自Handler的弱引用指向Activity,所以GC仍然會在檢查的時候把Activity回收掉。這樣,記憶體洩露的問題就不會出現了。

回到頂部

四、總結 

 android中的很多記憶體洩露都是由於在Activity中使用了非靜態內部類導致的,我們在使用非靜態內部類一定要格外注意,如果該靜態內部類的例項物件的生命週期大於外部物件,那麼就有可能導致記憶體洩露,推薦使用上面介紹的靜態類和弱引用的方法解決這種問題。