Android Handler機制之記憶體洩漏

AndyJennifer發表於2018-09-22

溢位啦啦.jpg

該文章屬於《Android Handler機制之》系列文章,如果想了解更多,請點選 《Android Handler機制之總目錄》

前言

整個Handler機制系列文章到此就結束了,相信大家基本已經將整個Handler機制消化的差不多了,現在就剩下最後一個知識點,在平時開發中使用Handler有可能會導致記憶體洩漏的問題。下面我們就一起去了解了解~~

記憶體洩漏

記憶體洩漏在官方的定義如下:

記憶體洩漏(Memory Leak)是指程式中己動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果。

那麼翻譯成人話,就是當一個物件不再被使用時,本該被系統回收,但卻因為有另外一個正在使用的物件持有它的引用,導致其不能被回收,造成記憶體的浪費。

那麼針對於Android系統來說,在Android系統中會為每個應用程式分配相應的記憶體(根據手機廠商的不同,分配的記憶體大小會有所差異)。也就是說對於每一個應用程式來說其記憶體是有限的。那麼當某個應用程式產生的記憶體洩漏較多時,導致達到應用總的記憶體閥值,那麼就會導致應用崩潰。

Handler記憶體洩漏的情況討論

為了分析Handler記憶體洩漏的具體情況,請參看以下示例程式碼:

public class HandlerLeakageActivity extends BaseActivity {

    public static final int UPDATE_UI = 1;
    
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == UPDATE_UI) {
                updateUI();
            }
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_leakage);
        Message message = Message.obtain();
        message.what = UPDATE_UI;
        mHandler.sendMessageDelayed(message, 1000 * 3600 * 24);//傳送延時24小時訊息
    }
	
	//更新ui
    private void updateUI() {...}
 }
複製程式碼

上述程式碼邏輯很簡單,我們在HandlerLeakageActivity 中建立了內部類Handler,同時傳送了一個延時為24小時的訊息。當HandlerLeakageActivity 收到這個延遲訊息後,那麼接著會來更新UI,同時我們可以得到以下引用鏈:

handler_refrence.png
其中的內部類Handler 擁有當前Activity的引用,是因為在Java中,非靜態內部類會持有外部類的引用,而Messagey擁有Handler的引用,是因為Message通過Looper的loop()方法取出後,需要相應的Handler來處理訊息(msg.target ==傳送訊息的Handler)。

那麼在整個Handler機制下的引用關係如下圖所示:

handler_leakage.png

參照上圖,我們設想一種情況,假設我們在程式啟動的時候,首先進入HandlerLeakageActivity ,然後又將其finish掉。那麼就會出現,因為延遲訊息的遲遲不能被取出執行,導致該Activity不能被系統回收。從而造成上文我們提到過的記憶體洩漏

那麼問題來了,什麼時候引用鏈會斷開?

在文章《Android Handler機制之Message及Message回收機制 》中,我們曾經提到過,當訊息被Looper通過Loop()方法取出並執行的時候,會執行recycleUnchecked()方法來重置訊息中的資料,具體程式碼如下:

void recycleUnchecked() {
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;//將關聯handler物件置為null
        callback = null;
        data = null;
		//省略部分程式碼
    }
複製程式碼

在該方法中將target =null,其中訊息中的target為當前傳送該訊息的Handler物件。也就說只有訊息被取出執行後,整個引用鏈才會斷開,那麼相應的Handler與使用該Handler的Activity才會被系統回收。

解決方法

通過上文Handler記憶體洩漏的問題分析,導致這種情況的發生的原因是內部類Handler擁有當前Activity的引用。那麼解決只要解決這個問題,我們就能處理Handler記憶體洩漏啦。

使用靜態內部類+弱引用的方式

public class HandlerLeakageActivity extends BaseActivity {
    public static final int UPDATE_UI = 1;
    
    private MyHandler mHandler = new MyHandler(this);
	//使用靜態內部類
    private static class MyHandler extends Handler {

        private final WeakReference<HandlerLeakageActivity> mWeakReference;
        MyHandler(HandlerLeakageActivity activity) {
            mWeakReference = new WeakReference<HandlerLeakageActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlerLeakageActivity activity = mWeakReference.get();
            if (activity != null) {
                activity.updateUI();
            }
        }
    }
    	//更新ui
    private void updateUI() {...}
   }
複製程式碼

為了保證不再持有當前Activity的引用,我們採用靜態內部類的方式(靜態內部類不會持有外部類引用),同時為了讓Handler在處理訊息的時候,能夠呼叫外部類Activity的方法,所以我們這裡採用弱引用的方式。

為什麼要使用弱引用?

在Java中判斷一個物件到底是不是需要回收,都跟引用相關。在Java中引用分為了4類。

  • 強引用:只要引用存在,垃圾回收器永遠不會回收Object obj = new Object();而這樣 obj物件對後面new Object的一個強引用,只有當obj這個引用被釋放之後,物件才會被釋放掉。
  • 軟引用:是用來描述,一些還有但並非必須的物件,對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。(SoftReference)
  • 弱引用:也是用來描述非必須的物件,但是它的強度要比軟引用更弱一些。被弱引用關聯的物件只能生存到下一次垃圾收集發生之前,當垃圾收集器工作時,無論當前記憶體是否足夠,都回回收掉被弱引用關聯的物件。(WeakReference)
  • 虛引用:也被稱為幽靈引用,它是最弱的一種關係。一個物件是否有引用的存在,完全不會對其生存時間構成影響,也無法通過一個虛引用來取得一個例項物件。

另建立一個類+弱引用的方式

如果你不想使用靜態內部類+弱引用的方式,你也可以採用新建一個Handler類檔案+弱引用的方式。這兩種程式碼基本差不多,這裡就不過多進行介紹了。

當外部類生命週期結束時,清空訊息

如果你不想採用上述的兩種方式,還有一種方法就是在當前Activity被finish掉的時候,移除掉整個訊息佇列中的所有訊息。這樣就能保證Activity與Handler沒有直接的引用關係啦。

關於訊息的刪除主要有三種方法,大家可以根據自己的專案需求來選擇相應的方法。具體如下所示:

(關於訊息的刪除,如果有同學不是很熟悉,請參看《Android Handler機制之Message及Message回收機制》

void removeMessages(Handler h, int what, Object object)
void removeMessages(Handler h, Runnable r, Object object)
void removeCallbacksAndMessages(Handler h, Object object)
void removeCallbacksAndMessages(Object token) 
複製程式碼

結合Activity的生命週期,具體程式碼如下所示:

 @Override
    protected void onDestroy() {
        super.onDestroy();
        //這裡token傳null,會移除訊息佇列中所有當前Handler傳送且未被執行的訊息
        mHandler.removeCallbacksAndMessages(null);
複製程式碼

這裡我使用removeCallbacksAndMessages(Object token) 方法來清空訊息,需要注意的是如果token=null,該方法會移除訊息佇列中所有當前Handler傳送且未被執行的訊息

總結

  • Handler在使用時,如果直接採用內部類,有可能會導致記憶體洩漏。
  • Handler記憶體洩漏的主要原因是,內部類Handler擁有外部類Activity的引用,且不能保證訊息的傳送是否有較長延時。
  • 解決Handler記憶體洩漏的主要方法有,採用靜態內部類+弱引用,當外部類生命週期結束時,清空訊息等。

相關文章