【Android】Handler原理解析與Handler相關面試題

一、Handler原理

1. Looper和消息隊列機制

Handler持有了一個消息隊列MessageQueue對象mQueue。這個對象是Handler實例構造的時候,通過Looper傳遞過來的。當使用無參構造方法時,這個Looper為Looper.myLooper()。

    public Handler() {
        // 。。。
        mLooper = Looper.myLooper();
        mQueue = mLooper.mQueue;
    }

而Looper類又是通過 ThreadLocal 來實現線程和Looper對象一一對應的。Looper.myLooper()即是當前線程所對應的Looper。

也就是說,Handler中的消息隊列,其實是當前線程對應的Looper的消息隊列。

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

1.1 Looper

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

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

在這里,調用了Looper的兩個方法,prepareloop,啟動了主線程的Looper。Looper的結構相對來說還是比較簡單的,其中最主要的就是這兩個方法了。

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內部,有一個ThreadLocal<Looper>類型的靜態變量sThreadLocal
而prepare的過程很簡單,就是將當前這個Looper對象,保存到這個sThreadLocal中。即:sThreadLocal為媒介,建立當前線程與當前Looper對象的對應關系。

Looper.loop()

這是Android程序中最重要的機制之一,也是Android程序能夠一直運行的原因。以下是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);
            } // ...
        }
    }

可以發現,loop()函數其實是一個死循環;在這個循環中,不停地從消息隊列中取下一條消息,然后分發給對應的Handler(msg.target)進行處理。
“Looper”就是循環的意思,這正對應了loop()方法中的這個死循環。在這個死循環里面,可以無限讀取消息隊列中的消息。使用quit()方法可以退出這個循環;如果在主線程的Looper退出,也就意味著程序的結束——將會得到 java.lang.IllegalStateException: Main thread not allowed to quit. 的報錯信息。

1.2 消息隊列 - MessageQueue

  • Message
    Message消息是Handler傳遞信息的基本單位。在Handler中,不管是調用post(Runnable)還是sendMessage(Message)還是其他的什么,最終都會構建一個Message對象,并添加到消息隊列mQueue中。Message中保存了消息的類型、參數、對應的Handler等一些少量的數據。

1.1中說到,Looper.loop()會發起一個死循環,在消息隊列中不停取值。那么,為什么這里不會卡死呢?
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);
            } // ...
        }
    }

跳轉到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是通過優先隊列+鎖實現的。
在MessageQueue中有一個Message類型的對象mMessage,它保存著消息隊列中最早的一條消息。當阻塞的線程被喚醒時,next()方法會返回mMessage,并且mMessage重新賦值為mMessage.next。
很容易發現,在消息隊列中,所有未讀的消息是通過鏈表的形式來保存的,每一個Message都是鏈表中的節點,Message#next指向了鏈表中的下一個節點。

小結

  • Looper和擁有Looper的線程一一對應,通過ThreadLocal來實現。Looper和消息隊列也是一一對應的。
  • Looper.loop()中是一個死循環,會無限調用對應消息隊列的next()方法來獲取下一個消息。
  • 消息隊列的next()方法會調用native方法nativePollOnce阻塞線程,直到有新的消息來臨將其返回。
  • 消息隊列實質上是以Message為節點的單向鏈表,其頭節點為mMessage。鏈表是按照Message的觸發時間,即msg.when,從早到晚排序的。

2. Handler傳遞消息的過程

從發送消息開始。使用Handler的post(runnalbe)、sendMessage(msg)、sendMessageDelayed(msg, delay)、Message的sendToTarget()等等許多方法都可以發送消息。最終,這些方法都會進入Handler#enqueueMessage方法。

  • Handler#enqueueMessage(MessageQueue, Message, long)

enqueueMessage方法的作用是將Message的target設置為本Handler,然后調用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,即當前線程的消息隊列。在這里,會使用msg.target = this將Message與當前Handler綁定。
這里的uptimeMillis是Message預定送達的時間,如果沒有設置延遲,那么這個時間是SystemClock.uptimeMillis()

  • MessageQueue#enqueueMessage(Message, long)

顧名思義,enqueueMessage表示將新到來的消息入隊。
刪去了部分,只保留關鍵代碼:

    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按照傳達時間的順序插入到消息隊列中;
二、如果消息隊列當前是阻塞的,并且這個消息的傳達時間成為了消息隊列中最早的一個,那么就將線程喚醒。

當消息入隊之后,消息發送的過程就已經完畢了,接下來就是等待取出消息了。

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

第1小節提到了,Looper會無限循環從MessageQueue中讀取消息。

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

當一個消息msg到達指定時間并被讀取到之后,會調用msg.target.dispatchMessage()方法;而這個target對象就是上面Handler#enqueueMessage()方法中傳遞進去的Handler。
也就是說,消息被讀取到之后,會調用對應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設置了Callback的話,就調用handleCallback回調,否則就調用handleMessage回調??梢钥吹?,如果設置了Callback的話,Handler的handleMessage就不生效了,這一點很容易證實。
此時,就完成了從消息發送到消息處理的流程。

小結

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

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

二、Handler相關問題

1. 為什么Looper中的死循環不會阻塞主線程?

回到標題中的問題。
從上面的分析中,可以看到,在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);
            } // ...
        }
    }

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

雖然這個問題仍然沒有什么邏輯性——死循環和ANR/卡頓似乎并沒有什么太大的聯系,并且可以說是完全沒有關系。要回答這個問題,首先要分析ANR/卡頓的原因,這是一個綜合性的問題。

首先要明確的是,卡頓和ANR不是一回事兒。
卡頓指手機不能在肉眼無法察覺的時間內完成一幀的繪制。對于一個60hz的屏幕,每一幀需要在16ms內完成繪制,否則就會丟幀。丟幀越多,卡頓就越嚴重。卡頓的主要原因是主線程干了太多的事情,導致了16ms之內無法完成一幀的繪制,可能是布局層次太深導致繪圖耗費時間長,可能是進行了復雜的運算,還有可能是頻繁GC引發卡頓……總之,就是主線程負擔太重了。
ANR指的是Application Not Responding,也就是應用無響應。這里ANR特指輸入無響應ANR,也就是應用對觸摸屏幕或者按鍵的響應時間超過5秒。也就是說,ANR的發生的必要條件是,需要有輸入,也就是用戶點擊了屏幕之類的,否則是不會發生ANR的。當用戶點擊了屏幕,InputManagerService通過epoll機制在硬件層面讀取到這個事件后,會使用InputDispatcher使用InputChannel通過Socket通知對應的ViewRootImpl。而ANR是在InputDispatcher.cpp中的handleTargetsNotReadyLocked函數檢測的,在這里,當一個事件分發下去之后,會設置一個超時時間,也就5秒之后;在下一個事件到來時,如果時間大于超時時間,并且上一個事件還沒有處理完畢的話,就要走ANR流程了。(見《ANR是如何產生的?》
當然,究其根本原因,造成ANR的原因很多情況下與卡頓是類似的,但是二者是完全不同性質的兩個事件。

回到問題上,卡頓是因為出于某種原因導致的繪制時間過長,而ANR的原因是對用戶的操作響應超時。
而Looper中的死循環是為了讀取消息,要知道Android應用本質上是消息驅動的,不管是卡頓還是ANR,本質上都是對應Handler或者Handler.Callback的handleMessage()處理消息方法的執行時間太長;而Looper中的死循環是在體系之外的,不在某個Handler的handleMessage()方法體之中,自然也就不會引起卡頓和ANR了。

2. Handler只能在主線程創建嗎?如果不是,那Handler可以在任意線程創建嗎?

否。
Handler的作用是作為一個終端發送和處理消息,需要配套的消息隊列才能發揮作用。所以,Handler只能在調用了Looper.prepare()的線程中使用,并且在最后加上Looper.loop()使其生效。如果在沒有調用Looper.prepare()的線程創建Handler,會出現 "Can't create handler inside thread that has not called Looper.prepare()" 的錯誤。
同時,這個線程的所有代碼都要寫在Looper.prepare()Looper.loop()之間,因為Looper.loop()會阻塞線程,后面的代碼沒法執行到。

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

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

4. 獲取Message的方式有哪些?哪種最好?

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

第二種方法好,因為使用了消息池復用。因為Message類本身就可以作為一個鏈表的節點,所以消息池的數據結構是一個鏈表,每次復用取出頭節點。

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

Handler發送消息的過程僅僅只是把消息放進消息隊列里,這是在子線程里完成的。
主線程的Looper是一直在主線程運行的,當發現有新消息之后,就會提取出來,然后再在主線程把消息傳遞給Handler進行處理;這樣就完成了線程切換。

6. 消息隊列的數據結構是什么?

鏈表。

7. Handler為什么會造成內存泄漏?如何解決?

內存泄漏的原因是類成員的生命周期大于類對象的生命周期,換句話說就是一個需要銷毀的對象由于成員被外部引用而無法銷毀。
比如,一個Activity中有一個Handler成員對象。如果這個Handler發送了一個延時很長的消息,那么這個Handler在很長一段時間內都不能銷毀,因為發送的消息的Message引用了這個Handler(msg.target),而這個Message還在消息隊列中存活。這樣,即使finish了這個Activity,它仍然會在內存中存活,造成內存泄漏。
解決方式一是使用static修飾Handler。如果Handler需要引用Activity,那么使用WeakReference弱引用。二是在Activity銷毀的時候,在onDestroy()回調中清除Handler的所有回調。但是注意如果發送的消息周期的確是長于本Activity的,那么就不能使用方法二了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容