【Android】Handler原理解析與Handler相關(guān)面試題

一、Handler原理

1. Looper和消息隊列機(jī)制

Handler持有了一個消息隊列MessageQueue對象mQueue趁啸。這個對象是Handler實例構(gòu)造的時候巨缘,通過Looper傳遞過來的尿赚。當(dāng)使用無參構(gòu)造方法時散庶,這個Looper為Looper.myLooper()

    public Handler() {
        // 凌净。悲龟。。
        mLooper = Looper.myLooper();
        mQueue = mLooper.mQueue;
    }

而Looper類又是通過 ThreadLocal 來實現(xiàn)線程和Looper對象一一對應(yīng)的冰寻。Looper.myLooper()即是當(dāng)前線程所對應(yīng)的Looper须教。

也就是說,Handler中的消息隊列斩芭,其實是當(dāng)前線程對應(yīng)的Looper的消息隊列轻腺。

那么,要理解Handler的原理秒旋,就要先理解Looper和消息隊列的原理约计。

1.1 Looper

事情又要回到ActivityThread中說起,這相當(dāng)于是一個Android程序的主線程迁筛,main方法就在其中。

  • ActivityThread.main()
    public static void main(String[] args) {
        // ...
        Looper.prepareMainLooper();
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

在這里耕挨,調(diào)用了Looper的兩個方法细卧,prepareloop,啟動了主線程的Looper筒占。Looper的結(jié)構(gòu)相對來說還是比較簡單的贪庙,其中最主要的就是這兩個方法了。

Looper.prepare()

    public static void prepare() {
        prepare(true);
    }

    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));
    }

在Looper內(nèi)部翰苫,有一個ThreadLocal<Looper>類型的靜態(tài)變量sThreadLocal止邮。
而prepare的過程很簡單,就是將當(dāng)前這個Looper對象奏窑,保存到這個sThreadLocal中导披。即:sThreadLocal為媒介,建立當(dāng)前線程與當(dāng)前Looper對象的對應(yīng)關(guān)系埃唯。

Looper.loop()

這是Android程序中最重要的機(jī)制之一撩匕,也是Android程序能夠一直運行的原因。以下是Looper.loop()源碼墨叛,刪去了絕大部分止毕,只保留了最關(guān)鍵的幾行:

    public static void loop() {
        final Looper me = myLooper();
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); // might block
            try {
                msg.target.dispatchMessage(msg);
            } // ...
        }
    }

可以發(fā)現(xiàn)模蜡,loop()函數(shù)其實是一個死循環(huán);在這個循環(huán)中扁凛,不停地從消息隊列中取下一條消息忍疾,然后分發(fā)給對應(yīng)的Handler(msg.target)進(jìn)行處理。
“Looper”就是循環(huán)的意思谨朝,這正對應(yīng)了loop()方法中的這個死循環(huán)膝昆。在這個死循環(huán)里面,可以無限讀取消息隊列中的消息叠必。使用quit()方法可以退出這個循環(huán)荚孵;如果在主線程的Looper退出,也就意味著程序的結(jié)束——將會得到 java.lang.IllegalStateException: Main thread not allowed to quit. 的報錯信息纬朝。

1.2 消息隊列 - MessageQueue

  • Message
    Message消息是Handler傳遞信息的基本單位收叶。在Handler中,不管是調(diào)用post(Runnable)還是sendMessage(Message)還是其他的什么共苛,最終都會構(gòu)建一個Message對象判没,并添加到消息隊列mQueue中。Message中保存了消息的類型隅茎、參數(shù)澄峰、對應(yīng)的Handler等一些少量的數(shù)據(jù)。

1.1中說到辟犀,Looper.loop()會發(fā)起一個死循環(huán)俏竞,在消息隊列中不停取值。那么堂竟,為什么這里不會卡死呢魂毁?
Looper#loop()

    public static void loop() {
        final Looper me = myLooper();
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); // might block
            try {
                msg.target.dispatchMessage(msg);
            } // ...
        }
    }

跳轉(zhuǎn)到MessageQueue.next(),看一下消息隊列是怎么取下一條消息的出嘹。這里刪去了大部分代碼席楚,只剩下核心的部分,方便梳理流程:

    Message next() {
        int nextPollTimeoutMillis = 0;
        for (;;) {
            nativePollOnce(ptr, nextPollTimeoutMillis); // 會阻塞線程直到下一個消息到來
            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message msg = mMessages;
                if (msg != null) {
                    if (now < msg.when) { // 時候未到
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        mMessages = msg.next;
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                }
            }
        }
    }

nativePollOnce是一個本地方法税稼,它會阻塞線程直到下一個有消息來臨烦秩。這類似于Java中的DelayQueue郎仆,不同點是DelayQueue是通過優(yōu)先隊列+鎖實現(xiàn)的只祠。
在MessageQueue中有一個Message類型的對象mMessage,它保存著消息隊列中最早的一條消息丸升。當(dāng)阻塞的線程被喚醒時铆农,next()方法會返回mMessage,并且mMessage重新賦值為mMessage.next
很容易發(fā)現(xiàn)墩剖,在消息隊列中猴凹,所有未讀的消息是通過鏈表的形式來保存的,每一個Message都是鏈表中的節(jié)點岭皂,Message#next指向了鏈表中的下一個節(jié)點郊霎。

小結(jié)

  • Looper和擁有Looper的線程一一對應(yīng),通過ThreadLocal來實現(xiàn)爷绘。Looper和消息隊列也是一一對應(yīng)的书劝。
  • Looper.loop()中是一個死循環(huán),會無限調(diào)用對應(yīng)消息隊列的next()方法來獲取下一個消息土至。
  • 消息隊列的next()方法會調(diào)用native方法nativePollOnce阻塞線程购对,直到有新的消息來臨將其返回。
  • 消息隊列實質(zhì)上是以Message為節(jié)點的單向鏈表陶因,其頭節(jié)點為mMessage骡苞。鏈表是按照Message的觸發(fā)時間,即msg.when楷扬,從早到晚排序的解幽。

2. Handler傳遞消息的過程

從發(fā)送消息開始。使用Handler的post(runnalbe)烘苹、sendMessage(msg)躲株、sendMessageDelayed(msg, delay)、Message的sendToTarget()等等許多方法都可以發(fā)送消息镣衡。最終霜定,這些方法都會進(jìn)入Handler#enqueueMessage方法。

  • Handler#enqueueMessage(MessageQueue, Message, long)

enqueueMessage方法的作用是將Message的target設(shè)置為本Handler捆探,然后調(diào)用MessageQueue的enqueueMessage方法然爆。

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

這個queue即是Looper.mQueue,即當(dāng)前線程的消息隊列黍图。在這里,會使用msg.target = this將Message與當(dāng)前Handler綁定奴烙。
這里的uptimeMillis是Message預(yù)定送達(dá)的時間助被,如果沒有設(shè)置延遲,那么這個時間是SystemClock.uptimeMillis()切诀。

  • MessageQueue#enqueueMessage(Message, long)

顧名思義揩环,enqueueMessage表示將新到來的消息入隊。
刪去了部分幅虑,只保留關(guān)鍵代碼:

    boolean enqueueMessage(Message msg, long when) {
        synchronized (this) {
            msg.when = when;
            Message p = mMessages; // 鏈表頭
            boolean needWake;
            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 {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                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;
    }

這里做了兩件事:
一丰滑、將這個Message按照傳達(dá)時間的順序插入到消息隊列中;
二倒庵、如果消息隊列當(dāng)前是阻塞的褒墨,并且這個消息的傳達(dá)時間成為了消息隊列中最早的一個炫刷,那么就將線程喚醒。

當(dāng)消息入隊之后郁妈,消息發(fā)送的過程就已經(jīng)完畢了浑玛,接下來就是等待取出消息了。

  • MessageQueue#next()
  • Looper#loop()

第1小節(jié)提到了噩咪,Looper會無限循環(huán)從MessageQueue中讀取消息顾彰。

    public static void loop() {
        for (;;) {
            Message msg = queue.next(); // might block
            try {
                msg.target.dispatchMessage(msg);
            } // ...
        }
    }

當(dāng)一個消息msg到達(dá)指定時間并被讀取到之后,會調(diào)用msg.target.dispatchMessage()方法胃碾;而這個target對象就是上面Handler#enqueueMessage()方法中傳遞進(jìn)去的Handler涨享。
也就是說,消息被讀取到之后仆百,會調(diào)用對應(yīng)Handler的dispatchMessage方法厕隧。

  • Handler#dispatchMessage()
    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

這里的邏輯很簡單,如果這個Handler設(shè)置了Callback的話儒旬,就調(diào)用handleCallback回調(diào)栏账,否則就調(diào)用handleMessage回調(diào)≌辉矗可以看到挡爵,如果設(shè)置了Callback的話,Handler的handleMessage就不生效了甚垦,這一點很容易證實茶鹃。
此時,就完成了從消息發(fā)送到消息處理的流程艰亮。

小結(jié)

  1. Handler消息的傳遞流程:
    -> Handler#enqueueMessage(MessageQueue, Message, long) 所有發(fā)送消息最終調(diào)用該方法
    -> MessageQueue#enqueueMessage(Message, long) 消息入隊
    -> MessageQueue#next() 等待消息到時闭翩,取出消息
    -> Handler#dispatchMessage(msg) 發(fā)送消息到對應(yīng)Handler
    -> Handler#handleMessage(msg)Handler.Callback#handleCallback(msg):處理消息

  2. Handler相當(dāng)于一個前臺的工具人,只做了發(fā)送消息和接收消息的工作迄埃,消息處理的主要傳遞和分發(fā)過程都交給了Looper和MessageQueue疗韵。

二、Handler相關(guān)問題

1. 為什么Looper中的死循環(huán)不會阻塞主線程侄非?

回到標(biāo)題中的問題蕉汪。
從上面的分析中,可以看到逞怨,在Looper.loop()的死循環(huán)中者疤,主線程的確是被阻塞了;并且如果沒有消息叠赦,將會一直阻塞下去——這一點毫無疑問驹马。所以這個問題本身就是不嚴(yán)謹(jǐn)?shù)模旱孟葐柺遣皇牵賳枮槭裁础?/p>

    public static void loop() {
        final Looper me = myLooper();
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); // might block
            try {
                msg.target.dispatchMessage(msg);
            } // ...
        }
    }

這個問題正確的問法是,為什么主線程Looper中的死循環(huán)不會造成ANR/卡頓糯累?

雖然這個問題仍然沒有什么邏輯性——死循環(huán)和ANR/卡頓似乎并沒有什么太大的聯(lián)系算利,并且可以說是完全沒有關(guān)系。要回答這個問題寇蚊,首先要分析ANR/卡頓的原因笔时,這是一個綜合性的問題。

首先要明確的是仗岸,卡頓和ANR不是一回事兒允耿。
卡頓指手機(jī)不能在肉眼無法察覺的時間內(nèi)完成一幀的繪制。對于一個60hz的屏幕扒怖,每一幀需要在16ms內(nèi)完成繪制较锡,否則就會丟幀。丟幀越多盗痒,卡頓就越嚴(yán)重蚂蕴。卡頓的主要原因是主線程干了太多的事情,導(dǎo)致了16ms之內(nèi)無法完成一幀的繪制俯邓,可能是布局層次太深導(dǎo)致繪圖耗費時間長骡楼,可能是進(jìn)行了復(fù)雜的運算,還有可能是頻繁GC引發(fā)卡頓……總之稽鞭,就是主線程負(fù)擔(dān)太重了鸟整。
ANR指的是Application Not Responding,也就是應(yīng)用無響應(yīng)朦蕴。這里ANR特指輸入無響應(yīng)ANR篮条,也就是應(yīng)用對觸摸屏幕或者按鍵的響應(yīng)時間超過5秒。也就是說吩抓,ANR的發(fā)生的必要條件是涉茧,需要有輸入,也就是用戶點擊了屏幕之類的疹娶,否則是不會發(fā)生ANR的伴栓。當(dāng)用戶點擊了屏幕,InputManagerService通過epoll機(jī)制在硬件層面讀取到這個事件后雨饺,會使用InputDispatcher使用InputChannel通過Socket通知對應(yīng)的ViewRootImpl挣饥。而ANR是在InputDispatcher.cpp中的handleTargetsNotReadyLocked函數(shù)檢測的,在這里沛膳,當(dāng)一個事件分發(fā)下去之后,會設(shè)置一個超時時間汛聚,也就5秒之后锹安;在下一個事件到來時,如果時間大于超時時間,并且上一個事件還沒有處理完畢的話叹哭,就要走ANR流程了忍宋。(見《ANR是如何產(chǎn)生的?》
當(dāng)然风罩,究其根本原因糠排,造成ANR的原因很多情況下與卡頓是類似的,但是二者是完全不同性質(zhì)的兩個事件超升。

回到問題上入宦,卡頓是因為出于某種原因?qū)е碌睦L制時間過長,而ANR的原因是對用戶的操作響應(yīng)超時室琢。
而Looper中的死循環(huán)是為了讀取消息乾闰,要知道Android應(yīng)用本質(zhì)上是消息驅(qū)動的,不管是卡頓還是ANR盈滴,本質(zhì)上都是對應(yīng)Handler或者Handler.Callback的handleMessage()處理消息方法的執(zhí)行時間太長涯肩;而Looper中的死循環(huán)是在體系之外的,不在某個Handler的handleMessage()方法體之中巢钓,自然也就不會引起卡頓和ANR了病苗。

2. Handler只能在主線程創(chuàng)建嗎?如果不是症汹,那Handler可以在任意線程創(chuàng)建嗎硫朦?

否。
Handler的作用是作為一個終端發(fā)送和處理消息烈菌,需要配套的消息隊列才能發(fā)揮作用阵幸。所以,Handler只能在調(diào)用了Looper.prepare()的線程中使用芽世,并且在最后加上Looper.loop()使其生效挚赊。如果在沒有調(diào)用Looper.prepare()的線程創(chuàng)建Handler,會出現(xiàn) "Can't create handler inside thread that has not called Looper.prepare()" 的錯誤济瓢。
同時荠割,這個線程的所有代碼都要寫在Looper.prepare()Looper.loop()之間,因為Looper.loop()會阻塞線程旺矾,后面的代碼沒法執(zhí)行到蔑鹦。

3. View.post()方法和Handler.post()是一樣的嗎?

View#post()方法本質(zhì)上也是調(diào)用了Handler#post()箕宙,這個Handler保存在View的mAttachInfo中嚎朽,通過父容器調(diào)用View的dispatchAttachedToWindow(attachInfo, visibility)方法傳遞過來。
這個Handler最終是指向ViewRootImpl中的mHandler對象柬帕,類型是繼承自Handler類的ViewRootHandler哟忍。這個類中定義了一系列View需要用到的消息并進(jìn)行了處理狡门,如INVALIDATE等。

4. 獲取Message的方式有哪些锅很?哪種最好其馏?

  1. 直接new
  2. 調(diào)用Message.obtain()或者Handler#obtain()

第二種方法好,因為使用了消息池復(fù)用爆安。因為Message類本身就可以作為一個鏈表的節(jié)點叛复,所以消息池的數(shù)據(jù)結(jié)構(gòu)是一個鏈表,每次復(fù)用取出頭節(jié)點扔仓。

5. Handler是怎樣起到切換線程作用的褐奥?是怎樣在子線程發(fā)送消息然后在主線程處理的?

Handler發(fā)送消息的過程僅僅只是把消息放進(jìn)消息隊列里当辐,這是在子線程里完成的抖僵。
主線程的Looper是一直在主線程運行的,當(dāng)發(fā)現(xiàn)有新消息之后缘揪,就會提取出來耍群,然后再在主線程把消息傳遞給Handler進(jìn)行處理;這樣就完成了線程切換找筝。

6. 消息隊列的數(shù)據(jù)結(jié)構(gòu)是什么蹈垢?

鏈表。

7. Handler為什么會造成內(nèi)存泄漏袖裕?如何解決曹抬?

內(nèi)存泄漏的原因是類成員的生命周期大于類對象的生命周期,換句話說就是一個需要銷毀的對象由于成員被外部引用而無法銷毀急鳄。
比如谤民,一個Activity中有一個Handler成員對象。如果這個Handler發(fā)送了一個延時很長的消息疾宏,那么這個Handler在很長一段時間內(nèi)都不能銷毀张足,因為發(fā)送的消息的Message引用了這個Handler(msg.target),而這個Message還在消息隊列中存活坎藐。這樣为牍,即使finish了這個Activity,它仍然會在內(nèi)存中存活岩馍,造成內(nèi)存泄漏碉咆。
解決方式一是使用static修飾Handler。如果Handler需要引用Activity蛀恩,那么使用WeakReference弱引用疫铜。二是在Activity銷毀的時候,在onDestroy()回調(diào)中清除Handler的所有回調(diào)双谆。但是注意如果發(fā)送的消息周期的確是長于本Activity的块攒,那么就不能使用方法二了励稳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市囱井,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌趣避,老刑警劉巖庞呕,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異程帕,居然都是意外死亡住练,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門愁拭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讲逛,“玉大人,你說我怎么就攤上這事岭埠≌祷欤” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵惜论,是天一觀的道長许赃。 經(jīng)常有香客問我,道長馆类,這世上最難降的妖魔是什么混聊? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮乾巧,結(jié)果婚禮上句喜,老公的妹妹穿的比我還像新娘。我一直安慰自己沟于,他們只是感情好咳胃,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著社裆,像睡著了一般拙绊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上泳秀,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天标沪,我揣著相機(jī)與錄音,去河邊找鬼嗜傅。 笑死金句,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吕嘀。 我是一名探鬼主播违寞,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼贞瞒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了趁曼?” 一聲冷哼從身側(cè)響起军浆,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挡闰,沒想到半個月后乒融,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡摄悯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年赞季,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奢驯。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡申钩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瘪阁,到底是詐尸還是另有隱情撒遣,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布罗洗,位于F島的核電站愉舔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏伙菜。R本人自食惡果不足惜轩缤,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贩绕。 院中可真熱鬧火的,春花似錦、人聲如沸淑倾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娇哆。三九已至湃累,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間碍讨,已是汗流浹背治力。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留勃黍,地道東北人宵统。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像覆获,于是被迫代替她去往敵國和親马澈。 傳聞我的和親對象是個殘疾皇子瓢省,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

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