(Android面試必知必會系列)Handler消息機制

這篇文章應(yīng)該是除夕之前的最后一篇文章壹罚,寫文章的一個很深的體會,就是一個知識點雖然自己能理解恋日,可以說出來,但是在寫的時候要花很多時間嘹狞,因為要讓讀者可以很好理解岂膳,不然寫文章就沒有意義了。

進入正題磅网,Android消息機制基本是面試必問的知識點谈截,今天結(jié)合源碼和面試中常問的點,進行一個分析和總結(jié)涧偷。

開始源碼分析

Handler的使用

我們一般使用Handler是這樣

        Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == 1){
                    //
                }
            }
        };

        Message message = Message.obtain();
        message.what = 1;
        
        //1
        handler.sendMessage(message);
        //或者2
        handler.sendMessageDelayed(message,1000);
        //或者3
        handler.post(new Runnable() {
            @Override
            public void run() {
                
            }
        });

接下來開始分析簸喂,首先看下 Handler構(gòu)造方法

Handler 構(gòu)造方法

    public Handler() {
        this(null, false);
    }
    
    public Handler(Callback callback, boolean async) {
        ...
        mLooper = Looper.myLooper();
        //1
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

注釋1:主線程默認有一個Looper,而子線程需要手動調(diào)用 Looper.prepare() 去創(chuàng)建一個Looper燎潮,不然會報錯喻鳄。

看下主線程Looper在哪初始化的,應(yīng)用的入口是 ActivityThread 的main方法

ActivityThread#main

    public static void main(String[] args) {
        ...
        //1
        Looper.prepareMainLooper();
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        Trace.traceEnd(64L);
        //2
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

注釋1:Looper.prepareMainLooper()确封,
注釋2:Looper開啟消息循環(huán)
接下來分析下

Looper.prepareMainLooper()

    public static void prepareMainLooper() {
        //1
        prepare(false);
        Class var0 = Looper.class;
        synchronized(Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            } else {
                sMainLooper = myLooper();
            }
        }
    }

可以看到調(diào)用prepare(false)除呵,參數(shù)false表示不允許退出。然后是給sMainLooper賦值,看下prepare方法

Looper.prepare()

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal();
...
    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");
        } else {
            //1
            sThreadLocal.set(new Looper(quitAllowed));
        }
    }

可以看到 prepare 是創(chuàng)建一個Looper爪喘,并放到 ThreadLocal里竿奏。

準備工作做好了,看下Looper.loop()

Looper.loop()


    public static void loop() {
        //1 從ThreadLocal 獲取Looper
        Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        } else {
            //2 Looper里面有一個消息隊列
            MessageQueue queue = me.mQueue;
            Binder.clearCallingIdentity();
            long ident = Binder.clearCallingIdentity();

            while(true) {
                //3 死循環(huán)獲取消息
                Message msg = queue.next();
                if (msg == null) {
                    return;
                }
                ...
                long end;
                try {
                    // 4,獲取到消息腥放,處理
                    msg.target.dispatchMessage(msg);
                    end = slowDispatchThresholdMs == 0L ? 0L : SystemClock.uptimeMillis();
                } finally {
                    if (traceTag != 0L) {
                        Trace.traceEnd(traceTag);
                    }

                }
                ...
                // 5.消息回收泛啸,可復用
                msg.recycleUnchecked();
            }
        }
    }
    
    public static Looper myLooper() {
        return (Looper)sThreadLocal.get();
    }

注釋1: 從ThreadLocal 獲取Looper,子線程需要調(diào)用Looper.prepare,不然會報錯秃症,上面說過候址。
注釋2:獲取消息隊列
注釋3:queue.next(),獲取一個消息
注釋4:處理消息

接下來分成幾個小點分析一下

1.mQueue 是什么

    private Looper(boolean quitAllowed) {
        this.mQueue = new MessageQueue(quitAllowed);
        this.mThread = Thread.currentThread();
    }

mQueue是一個消息隊列种柑,在創(chuàng)建 Looper 的時候創(chuàng)建的岗仑。

2.MessageQueue#next

    Message next() {
        ...
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            // 1 native方法,應(yīng)該是喚醒線程的
            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.
                    do {
                        prevMsg = msg;
                        //2.取出下一條消息
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    // 3.延時的處理聚请,計算定時喚醒時間
                    if (now < msg.when) {
                        // 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);
                    } else {
                        // Got a message.
                        //4.獲取到一條消息荠雕,因為是消息隊列是鏈表結(jié)構(gòu),所以需要調(diào)整一下鏈表
                        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;
                }

                // Process the quit message now that all pending messages have been handled.

                ...
                //5.這里提到 IdleHandler驶赏,是個什么東西炸卑?下面會分析
                for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

        }
    }

next方法,取出一條消息煤傍,如果有設(shè)置延時盖文,計算一下多久后執(zhí)行,然后在下一次循環(huán)蚯姆,注釋1處五续,調(diào)用native方法進行一個類似鬧鐘的設(shè)置洒敏,時間到的話會喚醒next方法。消息隊列是單鏈表的數(shù)據(jù)結(jié)構(gòu)疙驾,所以取出一條消息之后鏈表需要移動凶伙,注釋4處。

消息取出來之后就要處理了它碎,即
msg.target.dispatchMessage(msg);
msg.target是一個Handler

注釋5是擴展分析镊靴,面試映客直播時候被問到Handler中的Idle是什么,這里就寫一下
在獲取不到message的時候才會走注釋5的代碼链韭,也就可以理解為 IdleHandler是消息隊列空閑(或者主線程空閑)時候才會執(zhí)行的Handler偏竟。
IdleHandler 是一個接口,只有一個方法 queueIdle() 敞峭,調(diào)用addIdleHandler(IdleHandler handler) 這個方法的時候會將handler添加到list中

public void addIdleHandler(@NonNull IdleHandler handler) {
        synchronized (this) {
            mIdleHandlers.add(handler);
        }
    }

然后在消息隊列空閑的時候會遍歷這個list踊谋,執(zhí)行里面IdleHandler的queueIdle方法。

有什么應(yīng)用場景呢旋讹?
leakcanary 中使用到這個

// 類:AndroidWatchExecutor
private void waitForIdle(final Retryable retryable, final int failedAttempts) {
    // This needs to be called from the main thread.
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override public boolean queueIdle() {
        postToBackgroundWithDelay(retryable, failedAttempts);
        return false;
      }
    });
  }

leakcanary中檢測內(nèi)存泄漏的耗時任務(wù)會等到主線程空閑才執(zhí)行

3.Handler#dispatchMessage

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

    public void handleMessage(Message msg) {
    }

一般走的是注釋3殖蚕,也就是我們重寫的 handleMessage方法,
注釋1 :調(diào)用 Message.obtain(Handler h, Runnable callback) 傳的callback沉迹。
注釋2:創(chuàng)建Handler的時候使用這個構(gòu)造Handler(Callback callback)

Looper開啟循環(huán)睦疫,從MessageQueue取消息并處理的流程分析完了,還差一個消息的入隊

Handler#sendMessage

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

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

sendMessage 調(diào)用了sendMessageDelayed鞭呕,最終調(diào)用了enqueueMessage蛤育,進行入隊

Handler#enqueueMessage

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

最終調(diào)用了 MessageQueue 的 enqueueMessage

MessageQueue#enqueueMessage

    boolean enqueueMessage(Message msg, long when) {
        ...

        synchronized (this) {
            ...

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            // 1.滿足3個條件則插到鏈表頭部
            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 {
                // 2.否則插到鏈表中間,需要移動鏈表
                // 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) {
                //如果需要瓦糕,調(diào)用native方法喚醒
                nativeWake(mPtr);
            }
        }
        return true;
    }

注釋1:p是隊列頭部,滿足3個條件則把消息放到隊列頭部

1.隊列中沒有消息腋么,p==null
2.入隊的消息沒有延時
3.入隊的消息的延時比隊列頭部的消息延時短

注釋2:消息插入到鏈表中咕娄,需要移動鏈表,對比消息的延時珊擂,插入到合適的位置

好了圣勒,消息機制的分析大概就是這些了,接下來結(jié)合面試中的問題再回顧一下

面試中相關(guān)的問題

1.說一下Handler機制

1.在應(yīng)用啟動的時候摧扇,也就是ActivityThread的main方法里面圣贸,創(chuàng)建了Looper和MessageQueue,然后調(diào)用Looper.loop 開啟消息循環(huán)
2.消息循環(huán)是這樣的扳剿,調(diào)用MessageQueue的next方法旁趟,循環(huán)從消息隊列中取出一條消息昼激,然后交給Handler去處理庇绽,一般是回調(diào)handleMessage方法锡搜,取不到消息就阻塞,直到下一個消息入隊或者其它延時消息到時間了就喚醒消息隊列瞧掺。
3.消息入隊耕餐,通過調(diào)用handler的sendMessage方法,內(nèi)部是調(diào)用MessageQueue的enqueueMessage方法辟狈,進行消息入隊肠缔,入隊的規(guī)制是:隊列沒有消息,或者要入隊的消息沒有設(shè)置delay哼转,或者delay時間比隊列頭的消息delay時間短明未,則將要入隊的消息放到隊列頭,否則就插到隊列中間壹蔓,需要移動鏈表趟妥。

2.發(fā)送延時消息是怎么處理的

這個通過上面的分析應(yīng)該很容易答出來了

根據(jù)消息隊列入隊規(guī)制,如果隊列中沒消息佣蓉,那么不管要入隊的消息有沒有延時披摄,都放到隊列頭。如果隊列不空勇凭,那么要跟隊列頭的消息比較一下延時疚膊,如果要入隊的消息延時短,則放隊列頭虾标,否則寓盗,放到隊列中去,需要移動鏈表璧函。

入隊的規(guī)制的好處是贞让,延時越長的消息在隊列越后面,所以next方法取到一個延時消息時柳譬,如果判斷時間沒有到喳张,就進行阻塞,不用管后面的消息美澳,因為隊列后面的消息延遲時間更長销部。

ok,關(guān)于Handler消息機制的分析到此結(jié)束制跟,有問題歡迎留言交流舅桩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市雨膨,隨后出現(xiàn)的幾起案子擂涛,更是在濱河造成了極大的恐慌,老刑警劉巖聊记,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件撒妈,死亡現(xiàn)場離奇詭異恢暖,居然都是意外死亡,警方通過查閱死者的電腦和手機杰捂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來棋蚌,“玉大人嫁佳,你說我怎么就攤上這事」饶海” “怎么了湿弦?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵竟秫,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮澳化,結(jié)果婚禮上井濒,老公的妹妹穿的比我還像新娘瑞你。我一直安慰自己撞蚕,他們只是感情好寇钉,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布撵溃。 她就那樣靜靜地躺著,像睡著了一般鹅心。 火紅的嫁衣襯著肌膚如雪旭愧。 梳的紋絲不亂的頭發(fā)上用押,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天收夸,我揣著相機與錄音,去河邊找鬼闪朱。 笑死称诗,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的糯钙。 我是一名探鬼主播粪狼,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼任岸!你這毒婦竟也來了再榄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤享潜,失蹤者是張志新(化名)和其女友劉穎困鸥,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剑按,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡疾就,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了艺蝴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猬腰。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖猜敢,靈堂內(nèi)的尸體忽然破棺而出姑荷,到底是詐尸還是另有隱情盒延,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布鼠冕,位于F島的核電站添寺,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏懈费。R本人自食惡果不足惜计露,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望憎乙。 院中可真熱鬧票罐,春花似錦、人聲如沸寨闹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽繁堡。三九已至,卻和暖如春乡数,著一層夾襖步出監(jiān)牢的瞬間椭蹄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工净赴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绳矩,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓玖翅,卻偏偏與公主長得像翼馆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子金度,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

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