這是“Android訊息機制”系列的第二篇文章,系列文章目錄如下:
訊息機制的故事
壽司
陳放在壽司碟
上,壽司碟
按先後順序被排成佇列
送上傳送帶
。傳送帶
被啟動後,壽司
挨個呈現到你面前,你有三種享用壽司的方法。
將Android概念帶入後,就變成了Android訊息機制的故事:
- 壽司碟 --->
訊息(Message)
- 佇列 --->
訊息佇列(MessageQueue)
- 傳送帶 --->
訊息泵 (Looper)
- 壽司 ---> 你關心的資料
- 享用壽司方法 ---> 處理資料方式
暫未找到 Handler
在此場景中對應的實體。它是一個更抽象的概念,它即可以生產壽司,又把壽司送上傳送帶,還定義了怎麼享用壽司。暫且稱它為訊息處理器
吧。
如果打算自己開一家回轉壽司店,下面的問題很關鍵:
- 如何生產壽司(如何構造訊息)
- 如何分發壽司(如何分發訊息)
關於如何構造訊息可以移步上一篇部落格回轉壽司你一定吃過!——Android訊息機制(構造)。這一篇從原始碼角度分析下“如何分發訊息”。
分發要解決的問題是如何將壽司從廚師運送到消費者。回轉壽司系統是這樣做的:將壽司挨個排好放在傳送帶上,然後讓傳送帶滾動起來。對應的,在Android訊息系統中也有類似的兩個步驟:1. 訊息入隊 2. 訊息泵
(ps: 下文中的 粗斜體字 表示引導原始碼閱讀的內心戲)
1. 訊息入隊
關於入隊需要提兩個基本問題:(1)什麼時候入隊(2)怎麼入隊。第二個問題其實是在問“訊息佇列的資料結構是什麼?”。特定資料結構對應特定插入方法。
對於訊息佇列一無所知的我完全沒有了頭緒,這原始碼該從哪裡開始讀起?沒有思路的時候我們還可以YY(YY是人類特有的強大技能)。憑藉著對資料結構殘存的記憶,我隱約覺得“入隊”應該是佇列提供的基本操作,那就先從MessageQueue
開始讀吧~
/**
* Low-level class holding the list of messages to be dispatched by a
* {@link Looper}. “Messages are not added directly to a MessageQueue,
* but rather through {@link Handler} objects associated with the Looper.”
* <p>
* <p>You can retrieve the MessageQueue for the current thread with
* {@link Looper#myQueue() Looper.myQueue()}.
*/
public final class MessageQueue
{
...
}
複製程式碼
註釋提供了關鍵提示,它說“訊息不是直接加到訊息佇列中的,而是通過Handler物件”。不急著去看Handler
,先找一下MessageQueue
是否有“入隊操作”。
//省略了一些非關鍵程式碼
boolean enqueueMessage(Message msg,
long when)
{
...
synchronized (this)
{
...
msg.markInUse();
msg.when = when;
//p指向訊息佇列頭結點
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
{
...
//從訊息佇列隊頭開始尋找合適的位置將訊息插入
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;
}
}
...
return true;
}
複製程式碼
- 不出所料,果然有一個入隊函式,訊息佇列的資料結構和訊息池一模一樣(訊息池的介紹可以點選這裡),都是連結串列。
- 看到這裡第二個問題基本解決了:訊息是通過連結串列的插入操作進入訊息佇列的。讓我們思考的再深入一點:新訊息插入到連結串列的什麼位置? 可以看到原始碼中有一個大大的if-else,判斷條件是傳入的引數when,沿著呼叫鏈往上搜尋,在Handler中會發現如下函式:
/**
* “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.”
* ...
*/
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);
}
複製程式碼
- 註釋中帶引號的那句話很關鍵:“將訊息插入到訊息佇列中,並且排在所有
uptimeMillis
之前產生的訊息後面”。uptimeMillis
表示訊息被髮送的時間。這麼看來,訊息是按時間先後順序排列的,最舊的訊息在隊頭,最新的訊息在隊尾。那訊息入隊就分兩種情況:1. 尾插入 2.中間插入。其中尾插入表示最新的訊息插入隊尾。回頭再看一遍MessageQueue.enqueueMessage()
,那個大大的if-else就實現了這兩種情況。 - 從
Handler.sendMessageAtTime()
沿著呼叫鏈繼續往上搜尋,就會找到下面這個熟悉的方法:
public final boolean sendMessage(Message msg)
{
return sendMessageDelayed(msg, 0);
}
複製程式碼
- 這不就是我們用來發訊息的
Handler.sendMessage()
嗎!至此,讓我們總結一下“訊息入隊”:Handler傳送訊息就是將訊息按時間順序插入到訊息佇列,訊息佇列是連結串列結構,鏈頭是最舊的訊息,鏈尾是最新的訊息
2. 訊息泵
壽司已經按時間順序排列好了,是時候按下按鈕啟動傳送帶讓壽司迴圈起來了。對於Android訊息機制來說,讓訊息迴圈起來就表現為不斷從訊息佇列中拿訊息。MessageQueue
中有入隊操作,必然有出隊操作 :
//省略大量非關鍵程式碼
Message next() {
...
for (;;) {
...
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
//msg指向訊息佇列隊頭
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;
}
...
}
}
}
複製程式碼
- 這個函式很長,省略了一些和主題不相關的細節,比如:佇列空閒等待,非同步訊息。去掉了這些特殊情況後,出隊操作就是取訊息佇列的頭(佇列頭是最舊的訊息,佇列尾是最新的訊息),這符合佇列先進先出的特性,越早的訊息越先被分發。
- 必然有一個迴圈會不停的呼叫
MessageQueue.next()
,從訊息佇列中不斷的取訊息進行分發,經過一頓搜尋,果然在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.
//nandian
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;
}
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null)
{
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
//分發訊息
msg.target.dispatchMessage(msg);
if (logging != null)
{
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn‘t corrupted.
//nandian
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent)
{
Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass()
.getName() + " " + msg.callback + " what=" + msg.what);
}
//回收訊息
msg.recycleUnchecked();
}
複製程式碼
- 這個函式是Android訊息機制中構造並分發訊息的終點,處理訊息的起點。Looper通過無限迴圈從訊息佇列中取出最舊的訊息,並分發給訊息對應的訊息處理器,最後回收訊息。至此,上一篇文章回轉壽司你一定吃過!——Android訊息機制(構造)中留下的疑問就解決了:訊息是在被分發後立馬回收的。
- 當訊息佇列中沒有訊息時,
queue.next()
就會阻塞當前執行緒。再也不用擔心Looper.loop()
中的額無限迴圈會消耗CPU資源了。 - Looper.loop()什麼時候會被呼叫? 我們都知道主執行緒自帶Looper,雖然這是一個很好的切入點,但其中牽涉到太多和主題無關的內容。所以換一個更純粹的切入點,
HandlerThread
:
/**
* Handy class for starting a new thread that has a looper. The looper can then be
* used to create handler classes. Note that start() must still be called.
*/
public class HandlerThread extends Thread {
int mPriority;
int mTid = -1;
Looper mLooper;
private @Nullable Handler mHandler;
@Override
public void run() {
mTid = Process.myTid();
//1.準備Looper
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
//2. Looper開始迴圈
Looper.loop();
mTid = -1;
}
}
複製程式碼
- 註釋又一次給了我們很多提示:“該類用於建立帶有
Looper
的執行緒”。 難道並不是所有的執行緒都帶有Looper
?(想想也是:執行緒是一個Java概念,Looper
是一個Android概念) 。所以我們需要線上程啟動的時候特意做些什麼才能得到帶有Looper
的執行緒。在Thread.run()
中看到了兩個關鍵方法,其中Looper.loop()
已經分析過了,在它之前還有一個Looper.prepare()
,點進去看看:
/**
* “Class used to run a message loop for a thread. Threads by default do
* not have a message loop associated with them; to create one, call
* {@link #prepare} in the thread that is to run the loop, and then
* {@link #loop} to have it process messages until the loop is stopped.”
*/
public final class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
final MessageQueue mQueue;
/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
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用於為執行緒建立訊息迴圈系統,預設情況下執行緒沒有和它相關聯的訊息迴圈系統。可以通過線上程中呼叫Looper.prepare()來啟動一個訊息迴圈系統,接著呼叫
Looper.prepare()
來迴圈處理訊息。 - 在
prepare()
中,新建了Looper
例項,並且設定給ThreadLocal
物件,這個類用於保證執行緒物件和自定義型別物件一對一的關係。這個一個很大的主題,就不展開了。當下只要知道Looper
通過它將自己的例項和某一個執行緒繫結,即一個執行緒只有一個Looper
物件。所以Android訊息系統的層級結構是這樣的:1個Thread
對應 1個Looper
,1個Looper
有1個MessageQueue
,1個MessageQueue
有若干Message
。
總結
Android訊息機制中的“分發訊息”部分講完了,總結一下:傳送訊息時,訊息按時間先後順序插入到訊息佇列中,Looper
遍歷訊息佇列取出訊息分發給對應的Handler處理
故事還沒有結束,下一篇會繼續講解處理訊息。