深入理解MessageQueue

在上一篇文章中我們分析了Handler 胧弛、Looper员凝、 MessageQueue 摇锋、線程之間的關(guān)系苇侵,簡單的說就是:一個(gè)線程綁定一個(gè)Looper油狂,一個(gè)Looper維護(hù)一個(gè)MessageQueue隊(duì)列历恐,而一個(gè)線程可以對應(yīng)多個(gè)Handler。而在Handler的消息機(jī)制中专筷,MessageQueue可能算是最重要的弱贼,今天我們就來分析這個(gè)類。
在分析之前磷蛹,先提出兩個(gè)問題:
1.Handler.sendMessageDelayed()怎么實(shí)現(xiàn)延遲的吮旅?
2.Looper.loop是一個(gè)死循環(huán),拿不到需要處理的Message就會(huì)阻塞味咳,那在UI線程中為什么不會(huì)導(dǎo)致ANR庇勃?

現(xiàn)在,我們帶著這兩個(gè)問題進(jìn)入MessageQueue的分析中槽驶。首先看第一個(gè)责嚷,Handler.sendMessageDelayed()的源碼如下:

public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

可以看出 : 消息被處理的時(shí)間 = 當(dāng)前時(shí)間+延遲的時(shí)間

至于這個(gè)地方為什么要用SystemClock.uptimeMillis() 而不用SystemClock. currentTimeMillis(),這里可以看兩者的區(qū)別(摘自網(wǎng)上):

System.currentTimeMillis() 方法產(chǎn)生一個(gè)標(biāo)準(zhǔn)的自1970年1月1號0時(shí)0分0秒所差的毫秒數(shù)掂铐。該時(shí)間可以通過調(diào)用setCurrentTimeMillis(long)方法來手動(dòng)設(shè)置罕拂,也可以通過網(wǎng)絡(luò)來自動(dòng)獲取。這個(gè)方法得到的毫秒數(shù)為“1970年1月1號0時(shí)0分0秒 到 當(dāng)前手機(jī)系統(tǒng)的時(shí)間”的差堡纬。因此如果在執(zhí)行時(shí)間間隔的值期間用戶更改了手機(jī)系統(tǒng)的時(shí)間聂受,那么得到的結(jié)果是不可預(yù)料的蒿秦。因此它不適合用在需要時(shí)間間隔的地方烤镐,如Thread.sleep, Object.wait等,因?yàn)樗闹悼赡軙?huì)被改變棍鳖。

SystemClock.uptimeMillis()方法用來計(jì)算自開機(jī)啟動(dòng)到目前的毫秒數(shù)炮叶。如果系統(tǒng)進(jìn)入了深度睡眠狀態(tài)(CPU停止運(yùn)行碗旅、顯示器息屏、等待外部輸入設(shè)備)該時(shí)鐘會(huì)停止計(jì)時(shí)镜悉,但是該方法并不會(huì)受時(shí)鐘刻度祟辟、時(shí)鐘閑置時(shí)間亦或其它節(jié)能機(jī)制的影響。因此SystemClock.uptimeMillis()方法也成為了計(jì)算間隔的基本依據(jù)侣肄,比如Thread.sleep()旧困、Object.wait()、System.nanoTime()以及Handler都是用SystemClock.uptimeMillis()方法稼锅。這個(gè)時(shí)鐘是保證單調(diào)性,適用于計(jì)算不跨越設(shè)備的時(shí)間間隔吼具。

Handler.sendMessageDelayed()方法最終會(huì)調(diào)用enqueueMessage方法進(jìn)入MessageQueue的enqueueMessage方法中:

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

MessageQueue中最重要的就是兩個(gè)方法:
1.enqueueMessage向隊(duì)列中插入消息
2.next 從隊(duì)列中取出消息

先分析enqueueMessage:

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {//msg.target就是發(fā)送此消息的Handler
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {//表示此消息正在被使用
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {//表示此消息隊(duì)列已經(jīng)被放棄了
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;//將延遲時(shí)間封裝到msg內(nèi)部
            Message p = mMessages;//消息隊(duì)列的第一個(gè)元素
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
               //如果此隊(duì)列中頭部元素是null(空的隊(duì)列,一般是第一次)矩距,或者此消息不是延時(shí)的消息拗盒,則此消息需要被立即處理,此時(shí)會(huì)將這個(gè)消息作為新的頭部元素锥债,并將此消息的next指向舊的頭部元素陡蝇,然后判斷如果Looper獲取消息的線程如果是阻塞狀態(tài)則喚醒它,讓它立刻去拿消息處理
          
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                //如果此消息是延時(shí)的消息哮肚,則將其添加到隊(duì)列中登夫,原理就是鏈表的添加新元素,按照when绽左,也就是延遲的時(shí)間來插入的悼嫉,延遲的時(shí)間越長,越靠后拼窥,這樣就得到一條有序的延時(shí)消息鏈表戏蔑,取出消息的時(shí)候,延遲時(shí)間越小的鲁纠,就被先獲取了总棵。插入延時(shí)消息不需要喚醒Looper線程。
  
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                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;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {//喚醒線程
                nativeWake(mPtr);
            }
        }
        return true;
    }

源碼中主要的地方我給了注釋改含,可以參考參考情龄。
由此可以看出:
MessageQueue中enqueueMessage方法的目的有兩個(gè):
1.插入消息到消息隊(duì)列
2.喚醒Looper中等待的線程(如果是及時(shí)消息并且線程是阻塞狀態(tài))
同時(shí)我們知道了MessageQueue的底層數(shù)據(jù)結(jié)構(gòu)是單向鏈表,MessageQueue中的成員變量mMessages指向的就是該鏈表的頭部元素捍壤。

接下來我們再來分析一下取出消息的方法next():

next()方法代碼比較多骤视,下面是主要部分,后面省略了一部分IdleHandler的處理邏輯鹃觉,用于空閑的時(shí)候處理不緊急事件用的专酗,有興趣的自行分析。

Message next() {
    
        final long ptr = mPtr;
        if (ptr == 0) {
           //從注釋可以看出盗扇,只有l(wèi)ooper被放棄的時(shí)候(調(diào)用了quit方法)才返回null祷肯,mPtr是MessageQueue的一個(gè)long型成員變量沉填,關(guān)聯(lián)的是一個(gè)在C++層的MessageQueue,阻塞操作就是通過底層的這個(gè)MessageQueue來操作的佑笋;當(dāng)隊(duì)列被放棄的時(shí)候其變?yōu)?翼闹。
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            //阻塞方法,主要是通過native層的epoll監(jiān)聽文件描述符的寫入事件來實(shí)現(xiàn)的蒋纬。
           //如果nextPollTimeoutMillis=-1猎荠,一直阻塞不會(huì)超時(shí)。
           //如果nextPollTimeoutMillis=0蜀备,不會(huì)阻塞法牲,立即返回。
           //如果nextPollTimeoutMillis>0琼掠,最長阻塞nextPollTimeoutMillis毫秒(超時(shí))拒垃,如果期間有程序喚醒會(huì)立即返回。
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
           
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    //msg.target == null表示此消息為消息屏障(通過postSyncBarrier方法發(fā)送來的)
                    //如果發(fā)現(xiàn)了一個(gè)消息屏障瓷蛙,會(huì)循環(huán)找出第一個(gè)異步消息(如果有異步消息的話)悼瓮,所有同步消息都將忽略(平常發(fā)送的一般都是同步消息)
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // 如果消息此刻還沒有到時(shí)間,設(shè)置一下阻塞時(shí)間nextPollTimeoutMillis艰猬,進(jìn)入下次循環(huán)的時(shí)候會(huì)調(diào)用nativePollOnce(ptr, nextPollTimeoutMillis)進(jìn)行阻塞横堡;
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //正常取出消息
                        //設(shè)置mBlocked = false代表目前沒有阻塞
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    //沒有消息,會(huì)一直阻塞冠桃,直到被喚醒
                    nextPollTimeoutMillis = -1;
                }

                if (mQuitting) {
                    dispose();
                    return null;
                }

            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }


由此可以看出:
1.當(dāng)首次進(jìn)入或所有消息隊(duì)列已經(jīng)處理完成命贴,由于此刻隊(duì)列中沒有消息(mMessages為null),這時(shí)nextPollTimeoutMillis = -1 食听,然后會(huì)處理一些不緊急的任務(wù)(IdleHandler)胸蛛,之后線程會(huì)一直阻塞,直到被主動(dòng)喚醒(插入消息后根據(jù)消息類型決定是否需要喚醒)樱报。
2.讀取列表中的消息葬项,如果發(fā)現(xiàn)消息屏障,則跳過后面的同步消息迹蛤。
3.如果拿到的消息還沒有到時(shí)間民珍,則重新賦值nextPollTimeoutMillis = 延時(shí)的時(shí)間,線程會(huì)阻塞盗飒,直到時(shí)間到后自動(dòng)喚醒
4.如果消息是及時(shí)消息或延時(shí)消息的時(shí)間到了嚷量,則會(huì)返回此消息給looper處理。

通過enqueueMessage和next兩個(gè)方法的分析我們不難得出:
消息的入列和出列是一個(gè)生產(chǎn)-消費(fèi)者模式逆趣,Looper.loop()在一個(gè)線程中調(diào)用next()不斷的取出消息蝶溶,另外一個(gè)線程則通過enqueueMessage向隊(duì)列中插入消息,所以在這兩個(gè)方法中使用了synchronized (this) {}同步機(jī)制汗贫,其中this為MessageQueue對象身坐,不管在哪個(gè)線程,這個(gè)對象都是同一個(gè)落包,因?yàn)镠andler中的mQueue指向的是Looper中的mQueue部蛇,這樣防止了多個(gè)線程對同一個(gè)隊(duì)列的同時(shí)操作。

現(xiàn)在咐蝇,我們對開篇的第一個(gè)問題做個(gè)回答:
Handler.sendMessageDelayed()怎么實(shí)現(xiàn)延遲的涯鲁?
前面我們分析了如果拿到的消息還沒有到時(shí)間,則會(huì)重新設(shè)置超時(shí)時(shí)間并賦值給nextPollTimeoutMillis有序,然后調(diào)用nativePollOnce(ptr, nextPollTimeoutMillis)進(jìn)行阻塞抹腿,這是一個(gè)本地方法,會(huì)調(diào)用底層C++代碼旭寿,C++代碼最終會(huì)通過Linux的epoll監(jiān)聽文件描述符的寫入事件來實(shí)現(xiàn)延遲的警绩。

對于第二個(gè)問題:
Looper.loop是一個(gè)死循環(huán),拿不到需要處理的Message就會(huì)阻塞盅称,那在UI線程中為什么不會(huì)導(dǎo)致ANR肩祥?

首先我們來看造成ANR的原因:
1.當(dāng)前的事件沒有機(jī)會(huì)得到處理(即主線程正在處理前一個(gè)事件,沒有及時(shí)的完成或者looper被某種原因阻塞住了)
2.當(dāng)前的事件正在處理缩膝,但沒有及時(shí)完成

我們再來看一下APP的入口ActivityThread的main方法:

public static void main(String[] args) {
  
        ...

        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

顯而易見的混狠,如果main方法中沒有l(wèi)ooper進(jìn)行死循環(huán),那么主線程一運(yùn)行完畢就會(huì)退出疾层,會(huì)導(dǎo)致直接崩潰将饺,還玩什么!

現(xiàn)在我們知道了消息循環(huán)的必要性痛黎,那為什么這個(gè)死循環(huán)不會(huì)造成ANR異常呢予弧?

我們知道Android 的是由事件驅(qū)動(dòng)的,looper.loop() 不斷地接收事件湖饱、處理事件桌肴,每一個(gè)點(diǎn)擊觸摸或者說Activity的生命周期都是運(yùn)行在 Looper的控制之下,如果它停止了琉历,應(yīng)用也就停止了坠七。只能是某一個(gè)消息或者說對消息的處理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它旗笔,這也就是我們?yōu)槭裁床荒茉赨I線程中處理耗時(shí)操作的原因彪置。
主線程Looper從消息隊(duì)列讀取消息,當(dāng)讀完所有消息時(shí)蝇恶,主線程阻塞拳魁。子線程往消息隊(duì)列發(fā)送消息,喚醒主線程撮弧,主線程被喚醒只是為了讀取消息潘懊,當(dāng)消息讀取完畢姚糊,再次睡眠。因此loop的循環(huán)并不會(huì)對CPU性能有過多的消耗授舟。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末救恨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子释树,更是在濱河造成了極大的恐慌肠槽,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奢啥,死亡現(xiàn)場離奇詭異秸仙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)桩盲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門寂纪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赌结,你說我怎么就攤上這事弊攘。” “怎么了姑曙?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵襟交,是天一觀的道長。 經(jīng)常有香客問我伤靠,道長捣域,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任宴合,我火速辦了婚禮焕梅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘卦洽。我一直安慰自己贞言,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布阀蒂。 她就那樣靜靜地躺著该窗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蚤霞。 梳的紋絲不亂的頭發(fā)上酗失,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音昧绣,去河邊找鬼规肴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拖刃。 我是一名探鬼主播删壮,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼兑牡!你這毒婦竟也來了央碟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤发绢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后垄琐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體边酒,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年狸窘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了墩朦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡翻擒,死狀恐怖氓涣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情陋气,我是刑警寧澤劳吠,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站巩趁,受9級特大地震影響痒玩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜议慰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一蠢古、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧别凹,春花似錦草讶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拍霜,卻和暖如春践啄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背沉御。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工屿讽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓伐谈,卻偏偏與公主長得像烂完,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子诵棵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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