Android學(xué)習(xí)之Handler

Handler內(nèi)存泄露

sendMessage方法內(nèi)存泄露

有這么一個需求闸衫,延遲執(zhí)行一段邏輯,先看第一種方式诽嘉,直接讓線程sleep:

 private val handler2 = object : Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            startActivity(Intent(this@HandlerActivity, XXXActivity::class.java))
        }
    }
    
     private fun test() {
        thread {
            val message = Message()

            SystemClock.sleep(3000) //1

            message.apply {
                what = 3
                obj = "hahaha"
            }
            handler2.sendMessage(message)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("XX", "onDestroy")
        handler2.removeMessages(3) //2
    }

從上面的代碼里看到在代碼1處讓程序休眠3秒蔚出,然后點擊返回按鈕銷毀此Activity,那么即使在onDestroy方法里移除掉這個message,也是沒有效果的虫腋。原因在于此時這個message還沒有添加到MessageQueue里骄酗,所以移除的是null。
那么該如何解決呢悦冀,使用以下代碼:

 private fun test() {
        thread {
            val message = Message()
            message.apply {
                what = 3
                obj = "hahaha"
            }
            handler2.sendMessageDelayed(message,3000)
        }
    }

使用sendMessageDelayed方法執(zhí)行延遲操作趋翻,即可以避免帶來的內(nèi)存泄露。

為什么不能在子線程new Handler

我們在單獨的線程里初始化一個Handler

private fun test() {
        thread {
           Handler()
        }
    }

然后在onCreate方法里調(diào)用盒蟆,發(fā)現(xiàn)會閃退踏烙,報了一個錯誤,如下:

java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-4,5,main] that has not called Looper.prepare()

那么為什么我們在子線程中new Handler會報這個異常呢历等,原因是我們的Handler需要初始化一個loop對象讨惩,而我們沒有做。那么為什么在Android的主線程中我們可以直接使用Handler而不報錯呢寒屯,是因為在應(yīng)用啟動的時候已經(jīng)幫我們調(diào)用了初始化loop荐捻。在ActivityThread類里main方法中已經(jīng)幫我們初始化好了loop,這個loop綁定的是主線程。

 public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
        ......

        Looper.prepareMainLooper();

        ......
 }

Looper.prepareMainLooper();就是這行代碼完成了loop的初始化工作靴患。那么我們再進(jìn)入到這個方法中看看:

public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

可以看到這里先調(diào)用了prepare方法仍侥,然后初始化sMainLooper對象,并且可以看出sMainLooper只能被初始化一次鸳君,否則被拋出異常农渊。
再進(jìn)入到prepare方法里看看:

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
    
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

從代碼里可以看出,prepare方法初始化了looper對象或颊,并且在looper中綁定了當(dāng)前線程和new除了一個MessageQueue砸紊。并且把這個looper對象放入了sThreadLocal中。然后通過調(diào)用myLooper()方法從sThreadLocal中取得looper對象囱挑,至此完成looper的初始化工作醉顽。

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

那么,我們該如何在子線程中使用Handler呢平挑,很簡單游添,在我們使用Handler之前調(diào)用一下Looper的prepare方法即可:

    private fun test() {
        thread {
            Looper.prepare();
            Handler()
        }
    }

這樣的話,這個Handler就是運行在我們new出來的子線程中了通熄,當(dāng)然這個Handler也不能去更改UI了唆涝。

更改UI只能在主線程中操作嗎

我們學(xué)習(xí)Android的時候就知道,不能在非UI線程中去更新UI唇辨,否則會報錯廊酣,那么真的是絕對的嗎,看下面的代碼:

    private fun test() {
        thread {
            btnTxt.text = "我哦喔喔"
        }
    }

我們在子線程中將一個Button控件的text值修改赏枚,并且成功運行沒有報錯亡驰,但是在某些手機或者某些系統(tǒng)上就會拋出異常。那么是為什么呢饿幅?原因是我們在調(diào)用setText的時候會調(diào)用requestLayout();方法凡辱,這個方法里又會調(diào)用ViewRootImpl的requestLayout方法:

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

在這個方法中就會檢查線程是否正確,即調(diào)用checkThread方法:

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

這里就會做線程的檢查栗恩,如果不是主線程煞茫,就會拋出異常,但是在執(zhí)行requestLayout方法時還會并行的執(zhí)行invalidate方法摄凡,所以续徽,有可能頁面invalidate方法先執(zhí)行了,然后才觸發(fā)checkThread方法亲澡,那么就不會拋出異常钦扭。
我們可以修改一下上面的代碼驗證一下:

    private fun test() {
        thread {
            SystemClock.sleep(1000)
            btnTxt.text = "我哦喔喔"
        }
    }

我們讓修改的代碼延遲一秒執(zhí)行,可以發(fā)現(xiàn)床绪,程序就閃退了客情,并且拋出了上面的異常其弊。


屏幕快照 2019-08-11 17.38.54.png

Handler的dispatchMessage方法分析

我們在通過Handler去發(fā)送消息,并執(zhí)行的時候可以有三種方式:

    //方式一
    private val handler = Handler(Handler.Callback {
        when (it.what) {
            3 -> {}
            else -> {}
        }
        false
    })
    
    //方式二
    private val handler2 = object : Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            startActivity(Intent(this@HandlerActivity, AopActivity::class.java))
        }
    }
    
    //方式三
    handler.post(){
            btnTxt.text = "handler"
    }
    

那么這三種方式有什么區(qū)別呢膀斋,答案就在Handler的dispatchMessage方法源碼里:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);//1
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {//2
                    return;
                }
            }
            handleMessage(msg);//3
        }
    }

注釋1處的代碼

讓我先看注釋1處的代碼梭伐,也就是dispatchMessage首先判斷了msg里的callback是否是null,如果不為null仰担,那么就會調(diào)用handleCallback方法糊识。那么這個callback是什么呢,就是我們調(diào)用Handler.post時傳進(jìn)來的Runnable摔蓝。

    public final boolean post(Runnable r){
       return  sendMessageDelayed(getPostMessage(r), 0);
    }
    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

可以看到我們調(diào)用post方法時赂苗,會將我們傳進(jìn)來的Runnable封裝到Message對象里并且返回,那么在dispatchMessage方法里這個最先被判斷的就不會為空贮尉,也就會執(zhí)行handleCallback方法:

    private static void handleCallback(Message message) {
        message.callback.run();
    }

這個handleCallback方法也就是我們傳進(jìn)Runnable的run方法拌滋。

注釋2處的代碼

dispatchMessage方法里,如果callback為null了又進(jìn)行判斷mCallback是否為null猜谚。那這個mCallback是什么呢败砂,就是我們初始化Handler對象是在構(gòu)造方法中傳進(jìn)來的Handler.Callback對象,它是一個被定義在Handler中的接口:

    public interface Callback {
        /**
         * @param msg A {@link android.os.Message Message} object
         * @return True if no further handling is desired
         */
        public boolean handleMessage(Message msg);
    }

如果我們在初始化中傳進(jìn)來這個CallBack魏铅,那么將執(zhí)行它里面的handleMessage方法昌犹。

注釋3處的代碼

那么,如果以上這兩個對象都為null的話沦零,將調(diào)用Handler內(nèi)部的handleMessage方法祭隔,這個方法是個空實現(xiàn)货岭,也就是我們自己實現(xiàn)的handleMessage方法路操。

Handler的發(fā)送消息和執(zhí)行消息過程

Handler的發(fā)送消息

當(dāng)我們調(diào)用了Handler的sentXXX方法時,到最終都會調(diào)用enqueueMessage方法千贯,這個方法代碼如下:

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;//1
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);//2
    }

從上面的代碼中可以看出屯仗,這個方法做了兩件事

  1. 將當(dāng)前的Handler對象賦值給msg.target對象
  2. 調(diào)用MessageQueue中的enqueueMessage方法
    那么,我們再到enqueueMessage方法中看一下邏輯:
boolean enqueueMessage(Message msg, long when) {
        ......
     
        synchronized (this) {

            ......
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
               ......
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            ......
        }
        return true;
    }

代碼很長搔谴,其中最重要的一句就是mMessages = msg;即將傳進(jìn)來的msg賦值給全局的mMessages魁袜。這個過程就是Handler的發(fā)送消息的過程。

Handler的執(zhí)行消息

那么我們在發(fā)送消息的時候最終將msg賦值到了MessageQueue中的全局對象mMessages中敦第,是如何將它取出執(zhí)行的呢峰弹。
首先是通過Looper.loop();方法:

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
        
        ......

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            ......
            
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
          ......
    }

這段代碼很長,我截取了比較關(guān)鍵的部分芜果,可以看出loop方法先是取出looper對象鞠呈,然后從looper對象中取出MessageQueue對象,接著在一個死循環(huán)中取出queue中的msg右钾,如果為null蚁吝,就返回旱爆。否則就調(diào)用msg.target的dispatchMessage方法,那么這里的msg.target就是剛才發(fā)送消息時綁定的Handler對象窘茁,所以最終會通過Handler的dispatchMessage方法調(diào)用我們的回調(diào)方法怀伦。
至此,Handler的發(fā)送消息和執(zhí)行消息就分析完了山林。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末房待,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子捌朴,更是在濱河造成了極大的恐慌吴攒,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砂蔽,死亡現(xiàn)場離奇詭異洼怔,居然都是意外死亡,警方通過查閱死者的電腦和手機左驾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門镣隶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诡右,你說我怎么就攤上這事安岂。” “怎么了帆吻?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵域那,是天一觀的道長。 經(jīng)常有香客問我猜煮,道長次员,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任王带,我火速辦了婚禮淑蔚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘愕撰。我一直安慰自己刹衫,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布搞挣。 她就那樣靜靜地躺著带迟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪囱桨。 梳的紋絲不亂的頭發(fā)上仓犬,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機與錄音蝇摸,去河邊找鬼婶肩。 笑死办陷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的律歼。 我是一名探鬼主播民镜,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼险毁!你這毒婦竟也來了制圈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤畔况,失蹤者是張志新(化名)和其女友劉穎鲸鹦,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跷跪,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡馋嗜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吵瞻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葛菇。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖橡羞,靈堂內(nèi)的尸體忽然破棺而出眯停,到底是詐尸還是另有隱情,我是刑警寧澤卿泽,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布莺债,位于F島的核電站迄本,受9級特大地震影響功舀,放射性物質(zhì)發(fā)生泄漏饮六。R本人自食惡果不足惜删顶,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衙吩。 院中可真熱鬧,春花似錦、人聲如沸煌妈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽璧诵。三九已至,卻和暖如春仇冯,著一層夾襖步出監(jiān)牢的瞬間之宿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工苛坚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留比被,地道東北人色难。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像等缀,于是被迫代替她去往敵國和親枷莉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351

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