Android面試里常見的Handler相關問題

一忽刽、Handler涯雅、MessageQueue外构、Looper 的關系
  1. 模型職責

    • Handler: 負責向MQ里入隊消息(sendMessage)、刪除消息(removeMessage)域那、處理消息(handleMessage)
    • MessageQueue: 負責投遞消息(enqueueMessage)蜕依,取消息(next)
    • Looper: 負責輪詢MQ,將取出的消息分發(fā)給對應的 handler 處理(loop)
    • Message: 單鏈表結構(next)琉雳,綁定目標Handler(target)样眠,記錄發(fā)送時間(when),發(fā)送內容(obj)
  2. 調用關系

    • Handler: 需要指定 Looper翠肘,Looper 中會有對應的 MQ
    • MessageQueue: 對應一個待處理的 Message 鏈表
    • Looper: 對應一個 MQ
    • Message: 對應處理自己的 Handler
Handler 消息隊列機制的使用過程如下

```
    class HandlerThread extends Thread {
        private Handler mHandler;    
        
        public void run() {
            Looper.prepare();
            mHandler = new Handler(Looper.myLooper());
            Looper.loop();
        }
        
        public Handler getThreadHandler() {
            return mHandler;
        }
    }
    
    class ActivityThread {
        
        public static void main() {
            //...
            
            HandlerThread ht = new HandlerThread();
            ht.start();
            
            ht.getThreadHandler().post(
                ()-> {
                    //do xxx
                }
            );
        }
    }
```
  1. 關系確立時機

    • Handler Looper

      class Handler {
          public Handler(Looper looper, Callback callback, boolean async) {
              mLooper = looper;
              mQueue = looper.mQueue;
              mCallback = callback;
              mAsynchronous = async;
          }
      }
      

      初始化 Handler 的時候檐束,會指定 Looper,同時會將 Looper 中的 MessageQueue 也綁定到Handler上

    • Handler Message

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

      在發(fā)送消息的時候束倍,Handler 會和 Message 發(fā)生綁定被丧,msg.target=this,可以理解為誰發(fā)的消息就要誰去處理

    • Looper Thread

      class Looper {
          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));
          }
      }
      

      sThreadLocal.set(new Looper(quitAllowed))绪妹,借助 ThreadLocal 將 Looper 和 Thread 進行綁定甥桂,所以一個 Thread 只會對應一個 Looper,是 ThreadLocal 決定的邮旷,想知道ThreadLocal原理的同學可以自己查查相關資料黄选。

二、有關消息隊列的一些問題
  1. 如何保障消息隊列中的消息的時間順序婶肩?
  2. 延時消息如何實現(xiàn)的?
  3. 消息的分發(fā)過程?
  4. 消息屏障是什么办陷?

解決這些問題的關鍵在于MQ入隊和出隊消息的邏輯

首先,解決前兩個問題律歼,如何保障消息隊列中的消息的時間順序民镜?延時消息如何實現(xiàn)的?

這需要分析enqueueMessage()

boolean enqueueMessage(Message msg, long when) 

這個方法是在Handler#sendMessageAtTime()中調用的,whenSystemClock.uptimeMillis() + delayMillis的結果险毁,即系統(tǒng)正常運行時間(不算阻塞制圈、休眠)+消息延時時長

去除特殊情況们童,和加鎖邏輯后的代碼如下

    boolean enqueueMessage(Message msg, long when) {
            msg.when = when;
            Message p = mMessages;//之前的消息鏈
            if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.
                msg.next = p; //新 msg 作為鏈表頭
                mMessages = msg; //更新消息鏈
                needWake = mBlocked;
            } else {
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) { //走到鏈表尾部或找到正確時間線位置
                        break;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg; //將msg插入到消息鏈
            }  
    }

具體插入邏輯簡化為,向隊列頭部插入消息鲸鹦,比如調用了handler#sendMessageAtFrontOfQueue慧库,按時間線插入消息,如下圖所示亥鬓,假設系統(tǒng)當前運行時間定格在+500的狀態(tài),開始向隊列插入消息的狀況

enqueue.jpg

特殊說明: 在+4100插入了一個延時500的消息域庇,在入隊的時候需要調整鏈表位置嵌戈,所以+4100的消息會插入到+4200的后面,保證了消息的時間順序听皿,同時巧妙的實現(xiàn)了延時消息熟呛。

然后,分析一下取出正常消息(同步消息)的過程尉姨,精簡代碼如下

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

            nativePollOnce(ptr, nextPollTimeoutMillis); //阻塞 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) {
                    if (now < msg.when) { //沒到發(fā)消息的時間庵朝,隊列需要阻塞
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); //此時會給 nextPollTimeoutMillis 賦值,在循環(huán)開始時會阻塞
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) { 
                            prevMsg.next = msg.next;
                        } else { //調整消息鏈表
                            mMessages = msg.next;
                        }
                        msg.next = null; //斷開后面的消息鏈
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse(); //標記為消息已使用
                        return msg;// 返回得到的消息
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
                
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
        }
    }

正常從消息隊列取消息的過程可以理解為如下過程


dequeue.jpg

假設系統(tǒng)時間為+4300又厉,此時取出next為鏈表中的最后一個消息九府,when=+4500,此時因為還未到執(zhí)行消息的時候覆致,就會給nextPollTimeoutMillis賦值侄旬,然后會檢查pendingIdleHandlerCount,此時一定沒有IdleHandler要處理任務煌妈,進入下一次循環(huán)儡羔,進入阻塞狀態(tài)

第三個問題,消息的分發(fā)過程?
在上面剛剛分析了取消息的過程 MessageQueue#next()璧诵,現(xiàn)在看看是誰調用了 next 并且得到了當前的 Message 對象汰蜘,這個問題答案在Looper#loop()中,簡化邏輯后的代碼如下

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

邏輯很簡單之宿,就是死循環(huán)調用next()族操,如果得到msg,就調用 msg.target.dispatchMessage 即回到 Handler#dispatchMessage

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

邏輯也很簡單比被,就是指定了一個回調的優(yōu)先級坪创,先看 msg 是否指定了 Callback,如果沒有就查看是否給 Handler 設置了 Callback姐赡,如果也沒有就執(zhí)行 Handler#handleMessage莱预,這個方法需要自己重寫

第四,消息屏障是什么?

簡單來說项滑,消息屏障就是個標志依沮,標志開啟時會優(yōu)先處理異步消息
這個問題也需要在消息的入隊出隊時尋找答案,由于應用開發(fā)的時候使用場景比較少,所以放到最后解答危喉,現(xiàn)在看一下有關消息屏障的源碼

   Message next() {
       Message prevMsg = null;
       Message msg = mMessages;
       if (msg != null && msg.target == null) { 
          do {// Stalled by a barrier.  Find the next asynchronous message in the queue.
              prevMsg = msg;
              msg = msg.next;
          } while (msg != null && !msg.isAsynchronous());
       }
       //后面走上面分析過的流程宋渔,但此時的msg已經(jīng)是一個異步消息了
   }

判斷有無消息屏障的條件是 msg.target == null,在有消息屏障下辜限,會不斷循環(huán)鏈表找到異步消息皇拣,條件為!msg.isAsynchronous(),現(xiàn)在再來看看如何設置消息屏障MessageQueue#postSyncBarrier

    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

    private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;
            //此處沒設置target薄嫡,所以target是null

            Message prev = null; //找到合適的位置插入屏障消息
            Message p = mMessages;
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

這個方法就是在指定時間處插入一個屏障消息氧急,如果是0,就是在消息隊列最頭部插入毫深,然后消息隊列在取消息的時候發(fā)現(xiàn)有消息屏障就會向后遍歷直到找到異步消息(msg.isAsynchronous())吩坝,將消息分發(fā)處理。

個人理解:
消息屏障的作用其實并不是名字定義的同步異步的意思哑蔫,而是給消息定了優(yōu)先級钉寝,給異步消息開了后門(只要有屏障消息在,之后讀取的消息都是異步消息)闸迷。

三嵌纲、小結

  • 消息隊列中的消息按時間順序排序和延時消息的實現(xiàn),都是依靠系統(tǒng)時間的推移實現(xiàn)的
  • 如果下一個消息的時間超過系統(tǒng)當前時間腥沽,則會阻塞
  • 屏障消息的判斷條件是 msg.target == null疹瘦,正常使用Handler發(fā)的消息是無法將 target 置為 null 的,需要手動調用postSyncBarrier

補充:

IdleHandler的作用是什么巡球?

IdleHandler 可以用來提升提升性能言沐,主要用在我們希望能夠在當前線程消息隊列空閑時做些事情(譬如UI線程在顯示完成后,如果線程空閑我們就可以提前準備其他內容)的情況下酣栈,不過最好不要做耗時操作险胰。

參考文章 https://mp.weixin.qq.com/s/kpl8X9ZjOO_DewitoT7j-w

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市矿筝,隨后出現(xiàn)的幾起案子起便,更是在濱河造成了極大的恐慌,老刑警劉巖窖维,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件榆综,死亡現(xiàn)場離奇詭異,居然都是意外死亡铸史,警方通過查閱死者的電腦和手機鼻疮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來琳轿,“玉大人判沟,你說我怎么就攤上這事耿芹。” “怎么了挪哄?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵吧秕,是天一觀的道長。 經(jīng)常有香客問我迹炼,道長砸彬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任斯入,我火速辦了婚禮砂碉,結果婚禮上,老公的妹妹穿的比我還像新娘咱扣。我一直安慰自己绽淘,他們只是感情好涵防,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布闹伪。 她就那樣靜靜地躺著,像睡著了一般壮池。 火紅的嫁衣襯著肌膚如雪偏瓤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天椰憋,我揣著相機與錄音厅克,去河邊找鬼。 笑死橙依,一個胖子當著我的面吹牛证舟,可吹牛的內容都是我干的。 我是一名探鬼主播窗骑,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼女责,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了创译?” 一聲冷哼從身側響起抵知,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎软族,沒想到半個月后刷喜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡立砸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年掖疮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颗祝。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡氮墨,死狀恐怖纺蛆,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情规揪,我是刑警寧澤桥氏,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站猛铅,受9級特大地震影響字支,放射性物質發(fā)生泄漏。R本人自食惡果不足惜奸忽,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一堕伪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧栗菜,春花似錦欠雌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至而咆,卻和暖如春霍比,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背暴备。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工悠瞬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涯捻。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓浅妆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親障癌。 傳聞我的和親對象是個殘疾皇子凌外,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內容