Android中handler機制原理詳解

1匹涮、handler的作用

handler是android線程之間的消息機制,主要的作用是將一個任務(wù)切換到指定的線程中去執(zhí)行,(準(zhǔn)確的說是切換到構(gòu)成handler的looper所在的線程中去出處理)android系統(tǒng)中的一個例子就是主線程中的所有操作都是通過主線程中的handler去處理的。

2伐弹、handler的架構(gòu)

Handler的運行需要底層的 messagequeue和 looper做支撐。

3榨为、handler原理

3 .1 惨好、首先說messagequeue,messagequeue 是 一 個 消 息 隊 列 随闺, 它是采用單鏈表的數(shù)據(jù)結(jié)構(gòu)來存儲消息的日川,因為單鏈表在插入刪除上 的效率非常高。(Meaasgequeue主要包含一個是插入消 息的 enqueuemessage方法矩乐,和一個取出一條消息的next方法逗鸣。)

3.2、然后說 looper绰精,looper在安卓的消息機制中是扮演著消息調(diào)度的角色撒璧,具體來說就是他會不停的從 messagequeue中查看 是否有新消息,如果有笨使,并且這個消息需要執(zhí)行卿樱,就從隊列中取出這個消息進行執(zhí)行,(死循環(huán)遍歷消息:取消息的線程會先阻塞一段時間(隊頭消息的執(zhí)行時間減去當(dāng)前時間)硫椰,然后從隊列中取出隊頭消息)繁调,否則會一直阻塞在messagequeue的next那里。(構(gòu)成 一個 looper是需要一個 messagequeue靶草,而構(gòu)成一個 handler則需 要一個 looper蹄胰,)另外looper一般是調(diào)用Looper.prepare()方法使用 threadlocal在線程的ThreadLocalMap中存儲一個looper的,線程中有了looper之后就可以在這個線程中創(chuàng)建一個 handler了奕翔。

3.2裕寨、然后說 looper,looper在安卓的消息機制中是扮演著消息調(diào)度的角色派继。

Looper取消息的過程是這樣的:

如果隊列中有消息:
1宾袜、判斷隊頭消息的執(zhí)行時間是否大于當(dāng)前時間,如果大于驾窟,就調(diào)用nativePollOnce阻塞一段時間(隊頭消息的執(zhí)行時間-當(dāng)前時間)然后取出隊頭消息進行執(zhí)行庆猫。
2、否則就立即取出隊頭消息進行執(zhí)行绅络。
3月培、如果隊列中沒有消息嘁字,就一直阻塞,直到下一個消息來到杉畜,才喚醒取消息的線程繼續(xù)上述循環(huán)拳锚。

Message next() {
         ...
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    //發(fā)現(xiàn)屏障消息,優(yōu)先執(zhí)行隊列中的異步消息
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                     //之所以要循環(huán)寻行,是因為隊列是按時間排序的,異步消息不一定位于隊頭
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                         //隊頭消息沒到執(zhí)行時間匾荆,休眠nextPollTimeoutMillis
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        //得到一個需要立即執(zhí)行的消息
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    //隊列中沒有消息了拌蜘,進入無限期休眠
                    nextPollTimeoutMillis = -1;
                }
            ...
        }
    }

nativePollOnce(ptr, nextPollTimeoutMillis),這是一個native方法牙丽,實際作用就是通過Native層的MessageQueue休眠nextPollTimeoutMillis毫秒的時間简卧。
1.如果nextPollTimeoutMillis=-1,一直休眠不會超時烤芦。
2.如果nextPollTimeoutMillis=0举娩,不會休眠,立即返回构罗。
3.如果nextPollTimeoutMillis>0铜涉,最長休眠nextPollTimeoutMillis毫秒(超時),如果期間有程序喚醒會立即返回遂唧。

3.3芙代、最后說 handler

handler通過handler.send (message)發(fā)送消息,實際上是往構(gòu)成他的 looper的 messagequeue中 插入了一條消息盖彭,在將這條消息插入 messagequeue中之前纹烹,他需 要將此消息的 target變量指向當(dāng)前發(fā)它的 handler,然后looper在適當(dāng)?shù)臅r機取出這個消息召边,(looper發(fā)現(xiàn)構(gòu)成它的 messagequeue中有消息時铺呵, looper的 loop方法就會從 messagequeue中取出這條消息),然后調(diào) 用這個消息對應(yīng)的handler的dispatchmessage方法來處理這個消息(即msg.target.dispatchmessage)隧熙,注意片挂,dispatchmessage 方法是在構(gòu)成 handler的 looper中的loop方法中調(diào)用的,所以處理消息的邏輯就切換到了構(gòu)成handler的looper所在的線程之中了贞盯。

messagequeue加入一條消息喚醒取消息線程的三種情況
 boolean enqueueMessage(Message msg, long when) {
            ...
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            //隊列為空|當(dāng)前消息不是延遲消息|當(dāng)前消息執(zhí)行時間小于隊頭消息執(zhí)行時間
            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 {
                //p.target == null && msg.isAsynchronous()代表這個消息是系統(tǒng)消息
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                //根據(jù)這個消息的執(zhí)行時間去將這個消息插入到適當(dāng)?shù)奈恢?                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    //隊列中有系統(tǒng)消息宴卖,不需要喚醒
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
            //需要的情況下喚醒取消息線程
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

1、messagequeue為空
2邻悬、當(dāng)前消息不是延時消息
3症昏、當(dāng)前消息執(zhí)行時間小于隊頭消息執(zhí)行時間

(hanlder的構(gòu)成是需要一個 looper,主線程 之中父丰,在activitythread的main方法中(程序入口)通過 looper.preparemainlooper在主線 程中存儲一個 looper肝谭,而在子線程之中掘宪,我們則需要手動的通過 looper的 prepare在子線程中存儲一個 looper,然后通過 looper.loop 開啟一個消息循環(huán))攘烛。

4魏滚、主線程中的handler

android系統(tǒng)中的一個例子就是主線程中的所有操作都是通過主線程中的handler去處理的。例如activity的生命周期方法調(diào)用就是通過主線程中的handler去處理的坟漱。
在app的主線程中有一個類是activitythread鼠次,這個類中有一個main方法是app程序的入口,在main方法中使用Looper.prepareMainLooper()芋齿,在主線程中設(shè)置了一個looper腥寇,然后創(chuàng)建了一個applicationthread的線程用于和server進程中applicationthreadproxy進行進程通信,最后調(diào)用了Looper.loop()開啟消息循環(huán)觅捆。
在activity的生命中期中赦役,比如說系統(tǒng)服務(wù)ActivityManagerService調(diào)用applicationthreadproxy通過Binder給當(dāng)前app進程中的applicationthread發(fā)送了一個暫停activity的操作。app進程中的applicationthread便會通過在主線程中的handler將這個暫停activity的消息插入到主線程的messagequeue中去處理栅炒。

5掂摔、主線程的死循環(huán)一直運行是不是特別消耗CPU資源呢?

這里就涉及到Linux pipe/epoll機制赢赊,在主線程的MessageQueue沒有消息時乙漓,主線程便阻塞在loop的queue.next()中的nativePollOnce()方法里,相當(dāng)于java層的線程waite機制释移,此時主線程會釋放CPU資源進入休眠狀態(tài)簇秒,直到下個消息到達時調(diào)用nativewake,通過往pipe管道寫端寫入數(shù)據(jù)來喚醒主線程工作秀鞭。相當(dāng)于java層的notify機制趋观,去喚醒主線程,然后處理消息锋边,所以主線程大多數(shù)時候都是處于休眠狀態(tài)皱坛,并不會消耗大量CPU資源。
從looper取消息的過程豆巨,可以看出取消息的線程大部分時候處于阻塞狀態(tài)剩辟,不會消耗cpu資源。

6往扔、 Android中為什么主線程不會因為Looper.loop()里的死循環(huán)導(dǎo)致(anr)卡死贩猎?

理解1:looper死循環(huán)是不斷的往構(gòu)成它的消息隊列中取消息,如果當(dāng)前隊列中沒有消息萍膛,或者隊列中的消息不需要現(xiàn)在立即處理吭服,looper所在的線程就進入wait狀態(tài),釋放cpu資源蝗罗,其他的線程仍可處理事件艇棕。比如說applicationthread仍可接收服務(wù)進程中的消息來處理蝌戒,如果這個消息需要在主線程中處理,它就會調(diào)用主線程中的handler沼琉,將這個消息加到主線程的消息隊列中去處理北苟。

理解2:looper取消息的過程是先wait一段時間(這段時間是messagequeue隊首消息的執(zhí)行時間減去當(dāng)前時間),然后醒來從messagequeue中取出隊首消息進行執(zhí)行打瘪,wait過程中主線程是釋放cpu資源的友鼻,其他的線程(applicationthread)仍可處理事件。

理解3:(首先主線程中的死循環(huán)不會導(dǎo)致app anr闺骚,它會使得主線程阻塞在messagequeue的next中的nativePollOnce()方法里彩扔,當(dāng)有消息來時就會喚醒主線程進行消息處理,即使主線程在休眠的時候也有其他的線程(applicationthread)在處理事件葛碧。)

首先Anr和死循環(huán)不是一個概念。
1过吻、主線程的工作就是處理主線程中的message进泼,所有需要到主線程執(zhí)行的操作都是通過主線程的向主線程的messagequeue加入一條消息,looper在適當(dāng)?shù)臅r機取出這條消息纤虽,來執(zhí)行的乳绕,如果不是一個死循環(huán),那么loop取出一條消息逼纸,執(zhí)行完一條消息后洋措,主線程就退出了,正是有這個死循環(huán)杰刽,它才保證了主線程不會退出菠发,并且能處理隊列中所有的消息。所以這個死循環(huán)是一個消息處理機制贺嫂。

2滓鸠、而anr原因是,主線程中messagequeue中一個message的處理時間過長第喳,導(dǎo)致接下來的某類消息無法處理糜俗,比如說一個消息的處理時間超過了5秒,導(dǎo)致用戶的輸入無法響應(yīng)曲饱,才會出現(xiàn)anr悠抹。

3、另外扩淀,從looper取消息的過程來看楔敌,只有當(dāng)此刻有需要執(zhí)行的消息時,主線程才將此消息取出來執(zhí)行驻谆,否則就進入休眠狀態(tài)梁丘,釋放cpu侵浸。(就算不進入休眠一直循環(huán),如果手機是多核氛谜,也是不會卡死的掏觉,只是主線程在不停的運行代碼,消耗了更多資源)

為什么主線程中會采用死循環(huán)呢值漫?

線程是一段可執(zhí)行的代碼澳腹,當(dāng)可執(zhí)行代碼執(zhí)行完成后,線程生命周期便該終止了杨何。而對于主線程酱塔,我們是絕不希望會被運行一段時間就退出,所以采用死循環(huán)保證它不會被退出危虱。

7羊娃、handler.postDelayed(Runnable r, long delayMillis)

handler.postDelayed在message加入到messagequeue中之前,會計算出這個消息的執(zhí)行時間SystemClock.uptimeMillis() + delayMillis埃跷,(SystemClock.uptimeMillis()是開機到現(xiàn)在的時間(毫秒))蕊玷,然后通過enqueueMessage 將message和其執(zhí)行的時間一起添加進messagequeue,在enqueueMessage方法中會根據(jù)這個消息的執(zhí)行時間去將這個消息插入到適當(dāng)?shù)奈恢妹直ⅲ唵蔚恼f垃帅,messagequeue是按消息的執(zhí)行時間message.when排序的。如果插在隊列中間剪勿,說明該消息不需要馬上處理贸诚,不需要由這個消息來喚醒隊列。 如果插在隊列頭部(或者when=0)厕吉,則表明可能要馬上處理這個消息酱固。如果當(dāng)前隊列正在堵塞,則需要喚醒它進行處理头朱。 通過nativeWake方法喚醒隊列媒怯。

8、如何保證延時消息精確執(zhí)行髓窜?

1扇苞、從looper取消息過程來看,
2寄纵、從加入一條新的消息過程來看鳖敷。
這兩個過程都不存在任何延遲

8、ThreadLocal

ThreadLocal 可以在多個線程內(nèi)存儲數(shù)據(jù)程拭,使用ThreadLocal存儲的數(shù)據(jù)在多個線程之間是隔離的定踱,因為它是將數(shù)據(jù)存儲在每個線程內(nèi)的ThreadLocalMap中。

9恃鞋、同步屏障崖媚,同步消息亦歉,異步消息

同步消息就是我們平時發(fā)送的消息,異步消息一般是系統(tǒng)發(fā)送的消息

同步屏障就是給消息隊列發(fā)送了一個屏障信息(target == null)畅哑,消息隊列在處理到這個屏障信息時就開啟了”同步屏障”模式肴楷。在該模式下,只返回異步消息給 Looper 處理荠呐,屏蔽同步消息赛蔫。

在處理完異步消息隊列后,即使消息隊列中還有同步消息也會通過nativePollOnce()進入線程阻塞狀態(tài)泥张。直到有新的異步消息進來呵恢。除非解除同步屏障模式,同步消息才能得到處理媚创。

簡單的說就是渗钉,發(fā)現(xiàn)了同步屏障消息,就只處理異步消息钞钙,不處理同步消息鳄橘,直到同步屏障被移除。

view的刷新用到了同步屏障歇竟,因為界面刷新事件應(yīng)處在第一優(yōu)先級挥唠。

參考文章

8.1抵恋、handler.postDelayed(Runnable r, long delayMillis):https://stevendxc.github.io/Blog/2015/01/18/postDelayed/
8.2焕议、handler流程:http://www.reibang.com/p/8862bd2b6a29
8.3、handler系列問題:https://www.zhihu.com/question/34652589
8.4弧关、https://segmentfault.com/a/1190000022551209
8.5https://peterxiaosa.github.io/2020/07/15/Handler%E6%9C%BA%E5%88%B6%E4%B9%8B%E6%B6%88%E6%81%AF%E7%9A%84%E5%90%8C%E6%AD%A5%E5%B1%8F%E9%9A%9C/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盅安,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子世囊,更是在濱河造成了極大的恐慌别瞭,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件株憾,死亡現(xiàn)場離奇詭異蝙寨,居然都是意外死亡,警方通過查閱死者的電腦和手機嗤瞎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門墙歪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贝奇,你說我怎么就攤上這事虹菲。” “怎么了掉瞳?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵毕源,是天一觀的道長浪漠。 經(jīng)常有香客問我,道長霎褐,這世上最難降的妖魔是什么址愿? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮瘩欺,結(jié)果婚禮上必盖,老公的妹妹穿的比我還像新娘。我一直安慰自己俱饿,他們只是感情好歌粥,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拍埠,像睡著了一般失驶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上枣购,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天嬉探,我揣著相機與錄音,去河邊找鬼棉圈。 笑死涩堤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的分瘾。 我是一名探鬼主播胎围,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼德召!你這毒婦竟也來了白魂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤上岗,失蹤者是張志新(化名)和其女友劉穎福荸,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肴掷,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡敬锐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了呆瞻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片台夺。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖栋烤,靈堂內(nèi)的尸體忽然破棺而出谒养,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布买窟,位于F島的核電站丰泊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏始绍。R本人自食惡果不足惜瞳购,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亏推。 院中可真熱鬧学赛,春花似錦、人聲如沸吞杭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽芽狗。三九已至绢掰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間童擎,已是汗流浹背滴劲。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留顾复,地道東北人班挖。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像芯砸,于是被迫代替她去往敵國和親萧芙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

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