Android異步通信:詳解 Handler 內(nèi)存泄露的原因


前言

Android開發(fā)中署驻,內(nèi)存泄露十分常見。本文將詳細(xì)講解內(nèi)存泄露的其中一種情況:在Handler中發(fā)生的內(nèi)存泄露

Anroid異步通信Handler系列文章
Android異步通信:Handler機(jī)制學(xué)習(xí)攻略
Android異步通信:Handler使用教程
Android異步通信:Handler工作原理
Android異步通信:Handler源碼分析
Android異步通信:詳解Handler內(nèi)存泄露的原因


目錄

示意圖

背景知識(shí)

  • 內(nèi)存泄露的定義:本該被回收的對(duì)象不能被回收而停留在堆內(nèi)存中
  • 內(nèi)存泄露出現(xiàn)的原因:當(dāng)一個(gè)對(duì)象已經(jīng)不再被使用時(shí)健霹,本該被回收但卻因?yàn)橛辛硗庖粋€(gè)正在使用的對(duì)象持有它的引用從而導(dǎo)致它不能被回收旺上。這就導(dǎo)致了內(nèi)存泄漏。

1. 問題描述

Handler的一般用法 = 新建Handler子類(內(nèi)部類) 糖埋、匿名Handler內(nèi)部類宣吱,具體如下所示。

   /** 
     * 方式1:新建Handler子類(內(nèi)部類)
     */  
    public class MainActivity extends AppCompatActivity {

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

            // 主線程創(chuàng)建時(shí)便自動(dòng)創(chuàng)建Looper & 對(duì)應(yīng)的MessageQueue
            // 之后執(zhí)行Loop()進(jìn)入消息循環(huán)
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);

                //1. 實(shí)例化自定義的Handler類對(duì)象->>分析1
                //注:此處并無指定Looper瞳别,故自動(dòng)綁定當(dāng)前線程(主線程)的Looper征候、MessageQueue
                showhandler = new FHandler();

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

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

            }

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

                // 通過復(fù)寫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內(nèi)部類
     */ 
     public class MainActivity extends AppCompatActivity {

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

        // 主線程創(chuàng)建時(shí)便自動(dòng)創(chuàng)建Looper & 對(duì)應(yīng)的MessageQueue
        // 之后執(zhí)行Loop()進(jìn)入消息循環(huán)
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            //1. 通過匿名內(nèi)部類實(shí)例化的Handler類對(duì)象
            //注:此處并無指定Looper,故自動(dòng)綁定當(dāng)前線程(主線程)的Looper祟敛、MessageQueue
            showhandler = new  Handler(){
                // 通過復(fù)寫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. 啟動(dòng)子線程1
            new Thread() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // a. 定義要發(fā)送的消息
                    Message msg = Message.obtain();
                    msg.what = 1;// 消息標(biāo)識(shí)
                    msg.obj = "AA";// 消息存放
                    // b. 傳入主線程的Handler & 向其MessageQueue發(fā)送消息
                    showhandler.sendMessage(msg);
                }
            }.start();

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

        }
}
  • 測(cè)試結(jié)果


    示意圖
  • 上述例子雖可運(yùn)行成功疤坝,但代碼會(huì)出現(xiàn)嚴(yán)重警告:
  1. 警告的原因 = 該Handler類由于無設(shè)置為 靜態(tài)類,從而導(dǎo)致了內(nèi)存泄露
  2. 最終的內(nèi)存泄露發(fā)生在Handler類的外部類:MainActivity
示意圖

那么馆铁,該Handler在無設(shè)置為靜態(tài)類時(shí)跑揉,為什么會(huì)造成內(nèi)存泄露呢?


2. 原因講解

2.1 儲(chǔ)備知識(shí)

  • 主線程的Looper對(duì)象的生命周期 = 該應(yīng)用程序的生命周期
  • Java中埠巨,非靜態(tài)內(nèi)部類 & 匿名內(nèi)部類都默認(rèn)持有 外部類的引用

2.2 泄露原因描述

從上述示例代碼可知:

  • 上述的Handler實(shí)例的消息隊(duì)列有2個(gè)分別來自線程1历谍、2的消息(分別延遲1s6s
  • Handler消息隊(duì)列 還有未處理的消息 / 正在處理消息時(shí)辣垒,消息隊(duì)列中的Message持有Handler實(shí)例的引用
  • 由于Handler = 非靜態(tài)內(nèi)部類 / 匿名內(nèi)部類(2種使用方式)扮饶,故又默認(rèn)持有外部類的引用(即MainActivity實(shí)例),引用關(guān)系如下圖:
示意圖
  • 上述的引用關(guān)系會(huì)一直保持乍构,直到Handler消息隊(duì)列中的所有消息被處理完畢甜无。在Handler消息隊(duì)列 還有未處理的消息 / 正在處理消息時(shí)扛点,此時(shí)若需銷毀外部類MainActivity,但由于上述引用關(guān)系岂丘,垃圾回收器(GC)無法回收MainActivity陵究,從而造成內(nèi)存泄漏。如下圖:
示意圖

2.3 總結(jié)

  • 當(dāng)Handler消息隊(duì)列 還有未處理的消息 / 正在處理消息時(shí)奥帘,存在引用關(guān)系: “未被處理 / 正處理的消息 -> Handler實(shí)例 -> 外部類”
  • 若出現(xiàn) Handler的生命周期 > 外部類的生命周期 時(shí)(Handler消息隊(duì)列 還有未處理的消息 / 正在處理消息 而 外部類需銷毀時(shí))铜邮,將使得外部類無法被垃圾回收器(GC)回收,從而造成 內(nèi)存泄露

3. 解決方案

從上面可看出寨蹋,造成內(nèi)存泄露的原因有2個(gè)關(guān)鍵條件:

  1. 存在 “未被處理 / 正處理的消息 -> Handler實(shí)例 -> 外部類” 的引用關(guān)系
  2. Handler的生命周期 > 外部類的生命周期

Handler消息隊(duì)列 還有未處理的消息 / 正在處理消息 而 外部類需銷毀

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

解決方案1:靜態(tài)內(nèi)部類

  • 原理:靜態(tài)內(nèi)部類不默認(rèn)持有外部類的引用,從而使得 “未被處理 / 正處理的消息 -> Handler實(shí)例 -> 外部類” 的引用關(guān)系 不存在已旧。

  • 具體方案:將Handler的子類設(shè)置成靜態(tài)內(nèi)部類秸苗。此外,還可使用WeakReference弱引用持有外部類运褪,保證外部類能被回收惊楼。因?yàn)椋喝跻玫膶?duì)象擁有短暫的生命周期,在垃圾回收器線程掃描時(shí)秸讹,一旦發(fā)現(xiàn)了具有弱引用的對(duì)象檀咙,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存

  • 解決代碼

public class MainActivity extends AppCompatActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 實(shí)例化自定義的Handler類對(duì)象->>分析1
        // 注:
            // a. 此處并無指定Looper璃诀,故自動(dòng)綁定當(dāng)前線程(主線程)的Looper弧可、MessageQueue;
            // b. 定義時(shí)需傳入持有的Activity實(shí)例(弱引用)
        showhandler = new FHandler(this);

        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // a. 定義要發(fā)送的消息
                Message msg = Message.obtain();
                msg.what = 1;// 消息標(biāo)識(shí)
                msg.obj = "AA";// 消息存放

                showhandler.sendMessage(msg);
            }
        }.start();

    }

    // 設(shè)置為:靜態(tài)內(nèi)部類
    private static class FHandler extends Handler{

        // 定義 弱引用實(shí)例
        private WeakReference<Activity> reference;

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

        // 通過復(fù)寫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:當(dāng)外部類結(jié)束生命周期時(shí)劣欢,清空Handler內(nèi)消息隊(duì)列

  • 原理:不僅使得 “未被處理 / 正處理的消息 -> Handler實(shí)例 -> 外部類” 的引用關(guān)系 不復(fù)存在棕诵,同時(shí) 使得 Handler的生命周期(即 消息存在的時(shí)期) 與 外部類的生命周期 同步

  • 具體方案:當(dāng) 外部類(此處以Activity為例) 結(jié)束生命周期時(shí)(此時(shí)系統(tǒng)會(huì)調(diào)用onDestroy()),清除 Handler消息隊(duì)列里的所有消息(調(diào)用removeCallbacksAndMessages(null)

  • 具體代碼

@Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
        // 外部類Activity生命周期結(jié)束時(shí)氧秘,同時(shí)清空消息隊(duì)列 & 結(jié)束Handler生命周期
    }

使用建議

為了保證Handler中消息隊(duì)列中的所有消息都能被執(zhí)行年鸳,此處推薦使用解決方案1解決內(nèi)存泄露問題趴久,即 靜態(tài)內(nèi)部類 + 弱引用的方式


4. 總結(jié)

  • 本文主要講解了 Handler 造成 內(nèi)存泄露的相關(guān)知識(shí):原理 & 解決方案
  • 下一篇文章我將對(duì)講解Android Handler的相關(guān)知識(shí)丸相,感興趣的同學(xué)可以繼續(xù)關(guān)注Carson_Ho的簡(jiǎn)書

Anroid異步通信Handler系列文章
Android異步通信:Handler機(jī)制學(xué)習(xí)攻略
Android異步通信:Handler使用教程
Android異步通信:Handler工作原理
Android異步通信:Handler源碼分析
Android異步通信:詳解Handler內(nèi)存泄露的原因


歡迎關(guān)注Carson_Ho的簡(jiǎn)書

不定期分享關(guān)于安卓開發(fā)的干貨,追求短彼棍、平灭忠、快,但卻不缺深度座硕。


請(qǐng)點(diǎn)贊弛作!因?yàn)槟愕墓膭?lì)是我寫作的最大動(dòng)力!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末华匾,一起剝皮案震驚了整個(gè)濱河市映琳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖萨西,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件有鹿,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡谎脯,警方通過查閱死者的電腦和手機(jī)葱跋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來源梭,“玉大人娱俺,你說我怎么就攤上這事》下椋” “怎么了荠卷?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)脑溢。 經(jīng)常有香客問我僵朗,道長(zhǎng),這世上最難降的妖魔是什么屑彻? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任验庙,我火速辦了婚禮,結(jié)果婚禮上社牲,老公的妹妹穿的比我還像新娘粪薛。我一直安慰自己,他們只是感情好搏恤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布违寿。 她就那樣靜靜地躺著,像睡著了一般熟空。 火紅的嫁衣襯著肌膚如雪藤巢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天息罗,我揣著相機(jī)與錄音掂咒,去河邊找鬼。 笑死迈喉,一個(gè)胖子當(dāng)著我的面吹牛绍刮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挨摸,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼孩革,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了得运?” 一聲冷哼從身側(cè)響起膝蜈,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤锅移,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后饱搏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帆啃,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年窍帝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了努潘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡坤学,死狀恐怖疯坤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情深浮,我是刑警寧澤压怠,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站飞苇,受9級(jí)特大地震影響菌瘫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜布卡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一雨让、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧忿等,春花似錦栖忠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至薛匪,卻和暖如春捐川,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背逸尖。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工古沥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冷溶。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓渐白,卻偏偏與公主長(zhǎng)得像尊浓,于是被迫代替她去往敵國(guó)和親逞频。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容