Android消息機制

引言

在Android系統(tǒng)中所有涉及UI元素的操作都是由消息驅(qū)動的,比如當你修改了某個View的尺寸嘁信,或者修改了TextView的文本內(nèi)容,這些操作最終都會在你不知情的情況下轉(zhuǎn)化成主線程消息隊列中的一條消息疏叨,在該消息被處理后你的操作才會出現(xiàn)相應的改變潘靖。主線程的消息隊列是在App進程啟動時在ActivityThread的main方法中創(chuàng)建的,與此同時還會開啟一個無限的循環(huán)不斷地從隊列中取出消息并處理蚤蔓,取消息的操作是一個阻塞操作卦溢,也就是說當隊列中無消息時主線程可以休眠,當有新消息被放入消息隊列時被喚醒并繼續(xù)工作秀又。這個流程構(gòu)成了Android消息系統(tǒng)的核心单寂。

核心概念

從上面的描述中,我們可以提煉幾個關(guān)鍵概念:消息隊列涮坐、消息凄贩、循環(huán)、處理袱讹。其實單單從名字來看疲扎,我們也不難想象它們是如何一起構(gòu)成一個可以運行的系統(tǒng)的,消息會被插入消息隊列尾部捷雕,而循環(huán)會不停地再從隊列頭部取出消息然后對它進行處理椒丧。這幾個詞在Android系統(tǒng)中對應的設(shè)計分別為:MessageQueue、Message救巷、Looper壶熏、Handler。我們先通過一段代碼看看它們是如何被創(chuàng)建并開始工作的浦译。

// ActivityThread.java
public static void main(String[] args) {
       ...
        //只保留我們關(guān)心的內(nèi)容
        //創(chuàng)建Looper棒假,并創(chuàng)建MessageQueue
        Looper.prepareMainLooper();
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        ...
        // End of event ActivityThreadMain.
        //開啟循環(huán)
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

上面的代碼片段很簡單溯职,它是ActivityThread的main方法,即所有App進程啟動的入口方法帽哑,從該片段中我們大致可以了解到它創(chuàng)建了一個Looper然后開啟了循環(huán)谜酒,基本符合我們最初的猜想,只不過還沒有消息處理的身影妻枕,不要著急僻族,接下來我們再一探究竟。

Looper

從ActivityThread的main方法中我們先是看到了Looper的身影屡谐,從名字看我們大概知道Looper是一個循環(huán)述么,接下來就以Looper為切入點看看這個循環(huán)是如何在消息系統(tǒng)中工作的。

/**
 * Initialize the current thread as a looper, marking it as an
 * application's main looper. The main looper for your application
 * is created by the Android environment, so you should never need
 * to call this function yourself.  See also: {@link #prepare()}
 */
public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }

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));
}
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

從上面的代碼中我們可以看出度秘,prepareMainLooper其實就是創(chuàng)建了一個不允許退出的Looper,然后創(chuàng)建了消息隊列以及記錄了創(chuàng)建該Looper的線程亭珍。構(gòu)造方法中傳入的變量大家不必關(guān)心,只是在MessageQueue的退出方法中根據(jù)這個變量做個檢查而已。
至此,Looper已經(jīng)創(chuàng)建出來,接下來我們看看Looper在開啟時做了些什么事情。

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            msg.recycleUnchecked();
        }
    }

loop()方法的邏輯也比較清晰,只是在for循環(huán)中不斷的取出消息然后交給msg的target(這個target就是Handler實例)來處理。注意取消息的注釋(might block)屎勘,也就是說這里的操作可能會block,block的原因是為了在沒有消息處理時進行休眠等待從而減少能耗立哑。其實現(xiàn)利用了linux的select/poll機制捂掰,涉及內(nèi)容較多吏垮,感興趣的同學可以看下Gityuan的博文源碼解讀poll/select內(nèi)核機制遗嗽。
Looper的for循環(huán)還有一個延伸知識點--為什么Looper里的死循環(huán)不會卡死主線程晴音,這個問題可能曾經(jīng)或仍在困擾著部分開發(fā)者。其實從前面的幾個代碼片段我們也能理解個大概扭屁,首先我們都知道Java程序在main方法執(zhí)行完成后JVM便會結(jié)束當前進程晰骑,因此我們在Java程序中測試多線程代碼時通常需要在mian方法最后插入一行代碼(Thread.sleep(long))讓進程等待測試代碼運行結(jié)束后再退出庐杨。Android的進程也是運行在JVM上的哮洽,因此也是同樣的道理鸟辅,為了能讓App進程一直存活自然不能讓主線程退出,這是為什么會有for循環(huán)的原因饺饭。那為什么這個for循環(huán)不會卡死主線程呢?其實在前面的介紹中我們已經(jīng)知道了登下,Android的主線程事件都是通過消息來處理的,那么既然for循環(huán)中在不斷的處理拋到主線程消息隊列中的消息,自然主線程不會卡死了趣兄。更加深入的分析可以參考Gityuan在知乎中的回答:https://www.zhihu.com/question/34652589?sort=created仙粱。

MessageQueue

MessageQueue其實就是一個存放消息的隊列尚胞,負責存儲消息拜轨,其核心也就是存和取,我們先來看看存操作,即消息的入隊怜森。

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) {
        throw new IllegalStateException(msg + " This message is already in use.");
    }
    synchronized (this) {
        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }
        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;
}

插入邏輯比較清晰培己,分兩種情況玷室,一是隊列為空零蓉,那么直接將新消息插入隊列,是否需要喚醒由mBlocked變量決定穷缤;二是隊列中有消息敌蜂,那么將根據(jù)新消息的執(zhí)行時間找到合適位置插入,并通過mBlocked變量以及新消息是否是異步消息來判斷是否需要喚醒(注意喚醒條件中p.target == null津肛,所有消息在入隊時都會先判斷target是否為null紊册,如果為null會拋出異常。既然如此快耿,為什么還會存在target為null的情況呢?其實有一種特殊的柵欄消息囊陡,其target為null,柵欄消息的作用是為了同步某些操作使其必須在滿足一定條件時才能執(zhí)行)掀亥。這里喚醒的便是取消息時因阻塞引起的休眠撞反。
看完了消息的插入,我們再看看消息的取出邏輯搪花。

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;
            }
            //idleHandler部分不做分析
        }
        // Run the idle handlers.
        // We only ever reach this code block during the first iteration.
        // idleHandler部分不做分析
        // 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;
    }
}

從隊列中取消息的操作是在C++層完成的(具體實現(xiàn)細節(jié)這里不做分析遏片,感興趣的同學可以自行查看)嘹害,取出后要先判斷消息的target是否為空,即判斷該消息是不是柵欄消息吮便,如果是柵欄消息則在該消息之后找到第一條異步消息笔呀,同步消息被忽略。柵欄+異步消息給Android消息機制增加了一種簡單的優(yōu)先級控制髓需,異步消息的優(yōu)先級要高于同步消息许师。ViewRootImpl便巧妙的使用了該方法來處理UI的刷新,使其優(yōu)先級提高僚匆。

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
}

Handler

handler是用來處理消息的微渠,從本文第一個代碼段就以初露端倪逞盆,除了dispatchMessage外松申,Handler還封裝了發(fā)送消息的操作贸桶。在了解Handler的功能之前舅逸,我們還是先看看其構(gòu)造方法吧,日常開發(fā)中我們使用最多的便是:

/**
 * Use the provided {@link Looper} instead of the default one.
 *
 * @param looper The looper, must not be null.
 */
public Handler(Looper looper) {
    this(looper, null, false);
}

/**
 * Use the provided {@link Looper} instead of the default one and take a callback
 * interface in which to handle messages.  Also set whether the handler
 * should be asynchronous.
 *
 * Handlers are synchronous by default unless this constructor is used to make
 * one that is strictly asynchronous.
 *
 * Asynchronous messages represent interrupts or events that do not require global ordering
 * with respect to synchronous messages.  Asynchronous messages are not subject to
 * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}.
 *
 * @param looper The looper, must not be null.
 * @param callback The callback interface in which to handle messages, or null.
 * @param async If true, the handler calls {@link Message#setAsynchronous(boolean)} for
 * each {@link Message} that is sent to it or {@link Runnable} that is posted to it.
 *
 * @hide
 */
public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

先看看構(gòu)造方法的3個參數(shù)的含義刨啸,在使用Handler時,我們必須為其提供一個Looper识脆,否則便沒有MessageQueue存儲你發(fā)出的消息设联。Callback是一個可選參數(shù),當你傳入Callback時便不會再回調(diào)Handler自己的handleMessage方法灼捂。async為true時离例,通過該Handler構(gòu)建發(fā)出的消息都是異步消息,默認該值為false悉稠。
我們在日常開發(fā)中大部分情況都是使用Handler將一個Runnable post到消息隊列中宫蛆,或者給其增加一個延遲,也就是下面的方法:

/**
 * Causes the Runnable r to be added to the message queue.
 * The runnable will be run on the thread to which this handler is 
 * attached. 
 *  
 * @param r The Runnable that will be executed.
 * 
 * @return Returns true if the Runnable was successfully placed in to the 
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.
 */
public final boolean post(Runnable r)
{
   return  sendMessageDelayed(getPostMessage(r), 0);
}

/**
 * Causes the Runnable r to be added to the message queue, to be run
 * after the specified amount of time elapses.
 * The runnable will be run on the thread to which this handler
 * is attached.
 * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
 * Time spent in deep sleep will add an additional delay to execution.
 *  
 * @param r The Runnable that will be executed.
 * @param delayMillis The delay (in milliseconds) until the Runnable
 *        will be executed.
 *        
 * @return Returns true if the Runnable was successfully placed in to the 
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.  Note that a
 *         result of true does not mean the Runnable will be processed --
 *         if the looper is quit before the delivery time of the message
 *         occurs then the message will be dropped.
 */
public final boolean postDelayed(Runnable r, long delayMillis)
{
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

其實它們最終都是調(diào)用相同的方法sendMessageAtTime的猛,然后調(diào)用MessageQueue的enqueueMessage將新消息插入到消息隊列中耀盗。

/**
 * Enqueue a message into the message queue after all pending messages
 * before the absolute time (in milliseconds) <var>uptimeMillis</var>.
 * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
 * Time spent in deep sleep will add an additional delay to execution.
 * You will receive it in {@link #handleMessage}, in the thread attached
 * to this handler.
 * 
 * @param uptimeMillis The absolute time at which the message should be
 *         delivered, using the
 *         {@link android.os.SystemClock#uptimeMillis} time-base.
 *         
 * @return Returns true if the message was successfully placed in to the 
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.  Note that a
 *         result of true does not mean the message will be processed -- if
 *         the looper is quit before the delivery time of the message
 *         occurs then the message will be dropped.
 */
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);
}

待在Looper循環(huán)中取出該消息時,再調(diào)用Handler的dispatchMessage進行回調(diào)卦尊,這樣插入消息隊列的消息便得到了調(diào)度和執(zhí)行叛拷。

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

總結(jié)

到這里,我們對Android消息機制的分析就結(jié)束了忿薇。通過對Android消息機制的梳理和幾個核心類的分析署浩,我們可以用下面圖對Android消息機制進行一個概括性的總結(jié)炊汤。

Android消息機制.png
  • 1、有任務(wù)需要執(zhí)行時氓栈,通過Handler將任務(wù)包裝為消息發(fā)送到消息隊列;
  • 2提完、根據(jù)任務(wù)執(zhí)行時間插入消息隊列不同位置;
  • 3打肝、通過Looper循環(huán)將消息從隊列中取出交給Handler處理;
  • 4断医、Handler通過給定的Callback或默認方式對消息進行處理鉴嗤。
?著作權(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)容