Android 一起來看看面試必問的消息機制

前言

談到 Android 的消息機制,相信大家應該不陌生,在日常開發中不可避免要接觸到這方面的內容,而且這也是面試中常被問到的內容,最近本著「Read the Fucking Source Code」的原則,每天花半個小時開始看源碼,從 Android 消息機制開始。本文的內容借鑒了「Android 開發藝術探索」,在此強烈向大家推薦這本書,可以說是 Android 進階必備,質量真的相當高。

一、Android 消息機制的概述


Android 消息機制的主要是指的是 Handler 的運行機制以及 Handler 所附帶的 MessageQueue 和 Looper 的工作過程,這三者實際上是一個整體,只不過我們在開發過程中比較多地接觸 Handler 而已。

Handler 的主要功能是將任務切換到某個指定的線程中去執行,那么 Android 為什么要提供這個功能呢?這是因為 Android 規定訪問 UI 只能在主線程中進行,如果在子線程中訪問 UI,那么程序就會拋出異常。

那為什么 Android 不允許子線程中訪問 UI 呢?這是因為 Android 的 UI 控件并不是線程安全的,如果在多線程中并發訪問可能會導致 UI 控件處于不可預期的狀態,那還有一個問題,為什么系統不對 UI 控件的訪問加上鎖機制呢?缺點有兩個:

  • 加上鎖機制會讓 UI 訪問的邏輯變得復雜
  • 鎖機制會降低 UI 訪問的效率,因為鎖機制會阻塞某些線程的執行

Handler 創建完畢后,這個時候其內部的 Looper 以及 MessageQueue 就可以和 Handler 一起協同工作了,然后通過 Handler 的 post() 方法將一個 Runnable 投遞到 Handler 的內部的 Looper 中去處理,也可以通過 Handler 的 send() 方法發送一個消息,這個消息同樣會在 Looper 中去處理。其實 post() 方法最終也是通過 send() 方法來完成的。

接下來談下 send() 的工作過程。當 Handler 的 send() 方法被調用時,它會調用 MessageQueue 的 enqueueMessage() 方法將這個消息放入消息隊列中,然后 Looper 發現有新消息到來時,就會處理這個消息,最終消息中的 Runnable 或 Handler 的 handleMessage() 就會被調用。注意 Looper 是運行在創建 Handler 所在的線程中去執行的,這樣一來 Handler 中的業務邏輯就被切換到創建 Handler 所在的線程中去執行了。

Android 消息機制流程圖.png

二、消息隊列的工作原理


消息隊列在 Anroid 中指的是 MessageQueue,MessageQueue 主要包含兩個操作:插入和讀取。讀取操作本身會伴隨著刪除操作,插入和刪除對應的方法分別為:enqueueMessage()next(),其中 enqueueMessage() 的作用是往消息隊列中插入一條消息,而 next() 的作用是從消息隊列中取出一條消息并將其從消息隊列中移除。盡管 MessageQueue 叫做消息隊列,但是它的內部實現并不是用的隊列,實際上它是通過一個單鏈表的數據結構來維護消息隊列的,因為單鏈表在插入和刪除上比較有優勢。

接下來看下它的 enqueueMessage()next() 方法的實現,enqueueMessage() 的源碼如下所示:

    boolean enqueueMessage(Message msg, long when) {

        synchronized (this) {
            ...
            msg.markInUse();
            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;
    }

可以看到 enqueueMessage() 中,主要就是進行了單鏈表的插入操作。

接下來看看 next() 的源碼:

    Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

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

            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;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    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.
                        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.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            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);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

可以看到 next() 方法是一個無限循環的方法,如果消息隊列中沒有消息,那么 next() 方法會一直阻塞在這里,當有新消息到來時,next() 方法會返回這條消息并將其從消息隊列中刪除。

三、Looper 的工作原理


Looper 在 Android 的消息機制中扮演著消息循環的角色,具體來說就是它會不停地從 MessageQueue 中查看是否有新的消息,如果有新消息就回立刻處理,否則就一直阻塞在那里。

先來看下它的構造方法:

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

在構造方法中,它會創造一個 MessageQueue 即消息隊列,然后將當前的線程對象進行保存。

我們知道,Handler 的工作需要 Looper,沒有 Looper 的線程就會報錯,那么如何為一個線程創建 Looper 呢?其實很簡單,通過 Looper.prepare() 就可以為當前線程創建一個 Looper,接著通過 Looper.loop() 來開啟消息循環。

Looper 除了 prepare() 方法外,還提供了 prepareMainLooper() 方法,這個方法主要是給主線程創建 Looper 使用的,其本質也是通過 prepare() 方法來實現的。Looper 還提供了 quit()quitSafely() 兩個方法來退出一個 Looper,兩者的區別是:quit() 會直接退出 Looper,而 quitSafely() 只是設定一個退出標識,然后把消息隊列中的消息處理完畢后才安全地退出。

    public void quitSafely() {
        mQueue.quit(true);
    }

Looper 最重要的一個方法是 loop() 方法,只有調用了 loop() 后,消息循環系統才會真正地起作用,Looper 的 loop() 方法的工作過程也比較好理解,loop() 方法是一個死循環,唯一跳出循環的方式是 MessageQueue 返回 null,當 Looper 的 quit() 被調用時,Looper 就會調用 MessageQueue 的 quit() 或者 quitSafely() 方法來通知消息隊列退出,當前消息隊列被標記為退出狀態時,它的 next 方法就回返回 null。

四、Handler 的工作原理


Handler 的工作主要包括消息的發送和接收過程。消息的發送可以通過 post() 的一系列方法以及 send() 的一系列方法來實現,post() 的一系列方法最終也是通過 send() 的一系列方法來實現的。發送一條消息的典型過程如下所示。

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

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

可以發現,Handler 發送消息的過程僅僅是向消息隊列中插入一條消息,MessageQueue 的 next() 方法就會返回這條消息給 Looper,Looper 收到消息就開始處理,最終消息由 Looper 交給 Handler 進行處理,即 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);
        }
    }

Handler 處理消息的過程如下:

首先檢查 Message 的 callback 是否為 null,不為 null,就通過 handleCallback() 來處理消息,Message 的 callback 是一個 Runnable 對象,實際上就是 Handler 的 post() 方法所傳遞的 Runnable 參數。handleCallback 的邏輯也比較簡單,如下所示。

    private static void handleCallback(Message message) {
        message.callback.run();
    }

其次,檢查 mCallback 是否為 null,不為 null 則調用 mCallback.handleMessage() 方法來處理消息。最后調用 Handler 的 handleMessage() 方法來處理消息。

總結


Android 的消息機制主要指 Handler 的運行機制,以及 Handler 所附帶的 MessageQueue 和 Looper 的工作過程,三者是一個整體。當我們要將任務切換到某個指定的線程(如 UI 線程)中執行的時候,會通過 Handler 的 send(Message message msg)post(Runnable r) 進行消息的發送,post()方法最終也是通過 send() 方法來完成的。

發送的消息會插入到 MessageQueue 中(MessageQueue 雖然叫做消息隊列,但是它的內部實現并不是隊列,而是單鏈表,因為單鏈表在插入和刪除上比較有優勢),然后 Looper 通過 loop() 方法進行無限循環,判斷 MessageQueue 是否有新的消息,有的話就立刻進行處理,否則就一直阻塞在那里,loop() 跳出無限循環的唯一條件是 MessageQueue 返回 null。

Looper 將處理后的消息交給 Handler 進行處理,然后 Handler 就進入了處理消息的階段,此時便將任務切換到 Handler 所在的線程,我們的目的也就達到了。


猜你喜歡

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

推薦閱讀更多精彩內容

  • 前言 Handler是Android消息機制的上層接口,平時使用起來很方便,我們可以通過它把一個任務切換到Hand...
    eagleRock閱讀 1,670評論 0 13
  • 1. ANR異常 Application No Response:應用程序無響應。在主線程中,是不允許執行耗時的操...
    JackChen1024閱讀 1,422評論 0 3
  • 引言 由于Android對消息機制的封裝,開發者在平常的開發過程中,直接使用Handler對象就能滿足大部分的應用...
    紅灰李閱讀 775評論 1 2
  • 本文出自 “阿敏其人” 簡書博客,轉載或引用請注明出處。 能簡單說得我們盡量不復雜: 為了避免ANR,我們會通常把...
    阿敏其人閱讀 33,905評論 5 110
  • 早上起來睜眼的時候,按掉了還沒來得及歌唱的鬧鐘,舒展了一下身體打算起床,在掀開被子的剎那,突然想起來昨天跟同事做好...
    廢材不廢閱讀 972評論 0 0