Android 多線程:你的 Handler 內存泄露 了嗎?


前言

  • Android開發中,內存泄露 十分常見
  1. 內存泄露的定義:本該被回收的對象不能被回收而停留在堆內存中
  2. 內存泄露出現的緣由:當一個對象已經再也不被使用時,本該被回收但卻由於有另一個正在使用的對象持有它的引用從而致使它不能被回收。 這就致使了內存泄漏。
  • 本文將詳細講解內存泄露的其中一種狀況:在Handler中發生的內存泄露

閱讀本文前,建議先閱讀文章:Android開發:Handler異步通訊機制全面解析(包含Looper、Message Queue)bash


目錄

示意圖


1. 問題描述

  • Handler的通常用法 = 新建Handler子類(內部類) 、匿名Handler內部類
/** 
     * 方式1:新建Handler子類(內部類)
     */  
    public class MainActivity extends AppCompatActivity {

            public static final String TAG = "carson:";
            private Handler showhandler;

            // 主線程建立時便自動建立Looper & 對應的MessageQueue
            // 以後執行Loop()進入消息循環
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);

                //1. 實例化自定義的Handler類對象->>分析1
                //注:此處並沒有指定Looper,故自動綁定當前線程(主線程)的Looper、MessageQueue
                showhandler = new FHandler();

                // 2. 啓動子線程1
                new Thread() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // a. 定義要發送的消息
                        Message msg = Message.obtain();
                        msg.what = 1;// 消息標識
                        msg.obj = "AA";// 消息存放
                        // b. 傳入主線程的Handler & 向其MessageQueue發送消息
                        showhandler.sendMessage(msg);
                    }
                }.start();

                // 3. 啓動子線程2
                new Thread() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // a. 定義要發送的消息
                        Message msg = Message.obtain();
                        msg.what = 2;// 消息標識
                        msg.obj = "BB";// 消息存放
                        // b. 傳入主線程的Handler & 向其MessageQueue發送消息
                        showhandler.sendMessage(msg);
                    }
                }.start();

            }

            // 分析1:自定義Handler子類
            class FHandler extends Handler {

                // 經過複寫handlerMessage() 從而肯定更新UI的操做
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case 1:
                            Log.d(TAG, "收到線程1的消息");
                            break;
                        case 2:
                            Log.d(TAG, " 收到線程2的消息");
                            break;


                    }
                }
            }
        }

   /** 
     * 方式2:匿名Handler內部類
     */ 
     public class MainActivity extends AppCompatActivity {

        public static final String TAG = "carson:";
        private Handler showhandler;

        // 主線程建立時便自動建立Looper & 對應的MessageQueue
        // 以後執行Loop()進入消息循環
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            //1. 經過匿名內部類實例化的Handler類對象
            //注:此處並沒有指定Looper,故自動綁定當前線程(主線程)的Looper、MessageQueue
            showhandler = new  Handler(){
                // 經過複寫handlerMessage()從而肯定更新UI的操做
                @Override
                public void handleMessage(Message msg) {
                        switch (msg.what) {
                            case 1:
                                Log.d(TAG, "收到線程1的消息");
                                break;
                            case 2:
                                Log.d(TAG, " 收到線程2的消息");
                                break;
                        }
                    }
            };

            // 2. 啓動子線程1
            new Thread() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // a. 定義要發送的消息
                    Message msg = Message.obtain();
                    msg.what = 1;// 消息標識
                    msg.obj = "AA";// 消息存放
                    // b. 傳入主線程的Handler & 向其MessageQueue發送消息
                    showhandler.sendMessage(msg);
                }
            }.start();

            // 3. 啓動子線程2
            new Thread() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // a. 定義要發送的消息
                    Message msg = Message.obtain();
                    msg.what = 2;// 消息標識
                    msg.obj = "BB";// 消息存放
                    // b. 傳入主線程的Handler & 向其MessageQueue發送消息
                    showhandler.sendMessage(msg);
                }
            }.start();

        }
}
複製代碼
  • 測試結果 微信

    示意圖

  • 上述例子雖可運行成功,但代碼會出現嚴重警告:異步

  1. 警告的緣由 = 該Handler類因爲無設置爲 靜態類,從而致使了內存泄露
  2. 最終的內存泄露發生在Handler類的外部類:MainActivity

示意圖

那麼,該Handler在無設置爲靜態類時,爲何會形成內存泄露呢?ide


2. 緣由講解

2.1 儲備知識

  • 主線程的Looper對象的生命週期 = 該應用程序的生命週期
  • Java中,非靜態內部類 & 匿名內部類都默認持有 外部類的引用

2.2 泄露緣由描述

從上述示例代碼可知:oop

  • 上述的Handler實例的消息隊列有2個分別來自線程一、2的消息(分別 爲延遲1s6s
  • Handler消息隊列 還有未處理的消息 / 正在處理消息時,消息隊列中的Message持有Handler實例的引用
  • 因爲Handler = 非靜態內部類 / 匿名內部類(2種使用方式),故又默認持有外部類的引用(即MainActivity實例),引用關係以下圖

上述的引用關係會一直保持,直到Handler消息隊列中的全部消息被處理完畢測試

示意圖

  • Handler消息隊列 還有未處理的消息 / 正在處理消息時,此時若需銷燬外部類MainActivity,但因爲上述引用關係,垃圾回收器(GC)沒法回收MainActivity,從而形成內存泄漏。以下圖:

示意圖

2.3 總結

  • Handler消息隊列 還有未處理的消息 / 正在處理消息時,存在引用關係: 「未被處理 / 正處理的消息 -> Handler實例 -> 外部類」
  • 若出現 Handler的生命週期 > 外部類的生命週期 時(Handler消息隊列 還有未處理的消息 / 正在處理消息 而 外部類需銷燬時),將使得外部類沒法被垃圾回收器(GC)回收,從而形成 內存泄露

3. 解決方案

從上面可看出,形成內存泄露的緣由有2個關鍵條件:ui

  1. 存在「未被處理 / 正處理的消息 -> Handler實例 -> 外部類」 的引用關係
  2. Handler的生命週期 > 外部類的生命週期

Handler消息隊列 還有未處理的消息 / 正在處理消息 而 外部類需銷燬this

解決方案的思路 = 使得上述任1條件不成立 便可。spa

解決方案1:靜態內部類+弱引用

  • 原理 靜態內部類 不默認持有外部類的引用,從而使得 「未被處理 / 正處理的消息 -> Handler實例 -> 外部類」 的引用關係 的引用關係 不復存在。線程

  • 具體方案 將Handler的子類設置成 靜態內部類

  • 同時,還可加上 使用WeakReference弱引用持有Activity實例
  • 緣由:弱引用的對象擁有短暫的生命週期。在垃圾回收器線程掃描時,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存
  • 解決代碼
public class MainActivity extends AppCompatActivity {

    public static final String TAG = "carson:";
    private Handler showhandler;

    // 主線程建立時便自動建立Looper & 對應的MessageQueue
    // 以後執行Loop()進入消息循環
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //1. 實例化自定義的Handler類對象->>分析1
        //注:
            // a. 此處並沒有指定Looper,故自動綁定當前線程(主線程)的Looper、MessageQueue;
            // b. 定義時需傳入持有的Activity實例(弱引用)
        showhandler = new FHandler(this);

        // 2. 啓動子線程1
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // a. 定義要發送的消息
                Message msg = Message.obtain();
                msg.what = 1;// 消息標識
                msg.obj = "AA";// 消息存放
                // b. 傳入主線程的Handler & 向其MessageQueue發送消息
                showhandler.sendMessage(msg);
            }
        }.start();

        // 3. 啓動子線程2
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // a. 定義要發送的消息
                Message msg = Message.obtain();
                msg.what = 2;// 消息標識
                msg.obj = "BB";// 消息存放
                // b. 傳入主線程的Handler & 向其MessageQueue發送消息
                showhandler.sendMessage(msg);
            }
        }.start();

    }

    // 分析1:自定義Handler子類
    // 設置爲:靜態內部類
    private static class FHandler extends Handler{

        // 定義 弱引用實例
        private WeakReference<Activity> reference;

        // 在構造方法中傳入需持有的Activity實例
        public FHandler(Activity activity) {
            // 使用WeakReference弱引用持有Activity實例
            reference = new WeakReference<Activity>(activity); }

        // 經過複寫handlerMessage() 從而肯定更新UI的操做
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    Log.d(TAG, "收到線程1的消息");
                    break;
                case 2:
                    Log.d(TAG, " 收到線程2的消息");
                    break;


            }
        }
    }
}
複製代碼

解決方案2:當外部類結束生命週期時,清空Handler內消息隊列

  • 原理 不只使得 「未被處理 / 正處理的消息 -> Handler實例 -> 外部類」 的引用關係 不復存在,同時 使得 Handler的生命週期(即 消息存在的時期) 與 外部類的生命週期 同步

  • 具體方案 當 外部類(此處以Activity爲例) 結束生命週期時(此時系統會調用onDestroy()),清除 Handler消息隊列裏的全部消息(調用removeCallbacksAndMessages(null)

  • 具體代碼

@Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
        // 外部類Activity生命週期結束時,同時清空消息隊列 & 結束Handler生命週期
    }
複製代碼

使用建議

爲了保證Handler中消息隊列中的全部消息都能被執行,此處推薦使用解決方案1解決內存泄露問題,即 靜態內部類 + 弱引用的方式


4. 總結

  • 本文主要講解了 Handler 形成 內存泄露的相關知識:原理 & 解決方案
  • 接下來,我會繼續講解 Android開發中關於內存泄露的知識,有興趣能夠繼續關注Carson_Ho的安卓開發筆記

請點贊!由於你的鼓勵是我寫做的最大動力!


歡迎關注carson_ho的微信公衆號

示意圖

示意圖
相關文章
相關標籤/搜索