三劍客 Handler、Looper 和 MessageQueue

貓尾巴發表於2018-06-27

Handler

可以用 Handler 傳送處理與某執行緒的 MessageQueue 相關聯的 Message/Runnable 物件。每個 Handler 例項只能與一個執行緒和它的訊息佇列相關聯(建立 Handler 時並不一定是繫結到當前執行緒。)。Handler 將 Message 和 Runnable 傳遞給繫結的訊息佇列,並在它們從佇列裡被取出時執行對應邏輯。

Handler 主要有兩個用途:

  • 在未來某個時間點處理 Messages 或者執行 Runnables;
  • 將一段邏輯切換到另一個執行緒執行。

可以使用 Handler 的以下方法來排程 Messages 和 Runnables:

  • post(Runnable)
  • postAtTime(Runnable, long)
  • postDelayed(Runnable, Object, long)
  • sendEmptyMessage(int)
  • sendMessage(Message)
  • sendMessageAtTime(Message, long)
  • sendMessageDelayed(Message, long)

其中 postXXX 系列用於將 Runnable 物件加入佇列,sendXXX 系列用於將 Message 物件加入佇列,Message 物件通常會攜帶一些資料,可以在 Handler 的 handlerMessage(Message) 方法中處理(需要實現一個 Handler 子類)。

在呼叫 Handler 的 postXXX 和 sendXXX 時,可以指定當佇列準備好時立即處理它們,也可以指定延時一段時間後處理,或某個絕對時間點處理。後面這兩種能實現超時、延時、週期迴圈及其它基於時間的行為。

為應用程式建立一個程式時,其主執行緒專用於執行訊息佇列,該訊息佇列負責管理頂層應用程式物件(activities,broadcast receivers 等)以及它們建立的視窗。我們可以在主執行緒建立Handler,然後建立自己的執行緒,然後通過 Handler 與主執行緒進行通訊,方法是從新執行緒呼叫我們前面講到的 postXXX 或 sendXXX 方法,傳遞的 Runnable 或 Message 將被加入 Handler 關聯的訊息佇列中,並適時進行處理。

Looper

用於為執行緒執行訊息迴圈的類。執行緒預設沒有關聯的訊息迴圈,如果要建立一個,可以在執行訊息迴圈的執行緒裡面呼叫 prepare() 方法,然後呼叫 loop() 處理訊息,直到迴圈停止。

大多數與訊息迴圈的互動都是通過 Handler 類。

下面是實現一個 Looper 執行緒的典型例子,在 prepare() 和 loop() 之間初始化 Handler 例項,用於與 Looper 通訊:

class LooperThread extends Thread {
    public Handler mHandler;

    public void run() {
        Looper.prepare();

        mHandler = new Handler() {
            public void handleMessage(Message msg) {
                // 在這裡處理傳入的訊息
            }
        };

        Looper.loop();
    }
}
複製程式碼

MessageQueue

持有將被 Looper 分發的訊息列表的底層類。訊息都是通過與 Looper 關聯的 Handler 新增到 MessageQueue,而不是直接操作 MessageQueue。

可以用 Looper.myQueue() 獲取當前執行緒的 MessageQueue 例項。

Message

定義一個可以傳送給 Handler 的訊息,包含描述和任意資料物件。訊息物件有兩個額外的 int 欄位和一個 object 欄位,這可以滿足大部分場景的需求了。

雖然 Message 的構造方法是 public 的,但最推薦的得到一個訊息物件的方式是呼叫 Message.obtain() 或者 Handler.obtainMessage() 系列方法,這些方法會從一個物件回收池裡撿回能複用的物件。

Thread 與 Looper

執行緒預設是沒有訊息迴圈的,需要呼叫 Looper.prepare() 來達到目的,那麼我們對這個問題的探索就從 Looper.prepare() 開始。

/** 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));
}
複製程式碼

在有引數版本的 prepare 方法裡,我們可以得到兩個資訊:

  • 一個執行緒裡呼叫多次 Looper.prepare() 會丟擲異常,提示 Only one Looper may be created per thread,即 一個執行緒只能建立一個 Looper。
  • prepare 裡主要乾的事就是 sThreadLocal.set(new Looper(quitAllowed))。

原始碼裡是怎麼限制一個執行緒只能建立一個 Looper 的呢?呼叫多次 Looper.prepare() 並不會關聯多個 Looper,還會丟擲異常,那能不能直接 new 一個 Looper 關聯上呢?答案是不可以,Looper 的構造方法是 private 的。

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
複製程式碼

在概覽整個 Looper 的所有公開方法後,發現只有 prepare 和 prepareMainLooper 是做執行緒與 Looper 關聯的工作的,而 prepareMainLooper 是 Android 環境呼叫的,不是用來給應用主動呼叫的。所以從 Looper 原始碼裡掌握的資訊來看,想給一個執行緒關聯多個 Looper 的路不通。

另外我們從原始碼裡能觀察到,Looper 有一個 final 的 mThread 成員,在構造 Looper 物件的時候賦值為 Thread.currentThread(),原始碼裡再無可以修改 mThread 值的地方,所以可知 Looper 只能關聯到一個執行緒,且關聯之後不能改變。

說了這麼多,還記得 Looper.prepare() 裡乾的主要事情是 sThreadLocal.set(new Looper(quitAllowed)) 嗎?與之對應的,獲取本執行緒關聯的 Looper 物件是使用靜態方法 Looper.myLooper():

// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<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));
}

// ...

/**
 * Return the Looper object associated with the current thread.  Returns
 * null if the calling thread is not associated with a Looper.
 */
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}
複製程式碼

使用了 ThreadLocal 來確保不同的執行緒呼叫靜態方法 Looper.myLooper() 獲取到的是與各自執行緒關聯的 Looper 物件。

小結: Thread 若與 Looper 關聯,將會是一一對應的關係,且關聯後關係無法改變。

Looper 與 MessageQueue

public final class Looper {
    // ...
    final MessageQueue mQueue;

    // ...

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        // ...
    }
}
複製程式碼

Looper 物件裡有一個 MessageQueue 型別成員,在構造的時候 new 出的,並且它是一個 final,沒有地方能修改它的指向。

小結: Looper 與 MessageQueue 是一一對應的關係。

Handler 與 Looper

public class Handler {
    // ...
    
    /**
     * ...
     * @hide
     */
    public Handler(Callback callback, boolean async) {
        // ...
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                    "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        // ...
    }

    /**
     * ...
     * @hide
     */
    public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = mLooper.mQueue;
        // ...
    }

    // ...

    final Looper mLooper;
    final MessageQueue mQueue;
    // ...
}
複製程式碼

Handler 物件裡有 final Looper 成員,所以一個 Handler 只會對應一個固定的 Looper 物件。構造 Handler 物件的時候如果不傳 Looper 引數,會預設使用當前執行緒關聯的 Looper,如果當前執行緒沒有關聯 Looper,會丟擲異常。

那麼能不能繫結多個 Handler 到同一個 Looper 呢?答案是可以的。例如以下例子,就繫結了兩個 Handler 到主執行緒的 Looper 上,並都能正常使用(日誌 receive msg: 1 和 receive msg: 2 能依次輸出)。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    private Handler mHandler1;
    private Handler mHandler2;

    private Handler.Callback mCallback = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            Log.v(TAG, "receive msg: " + msg.what);
            return false;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mHandler1 = new Handler(mCallback);
        mHandler2 = new Handler(mCallback);

        mHandler1.sendEmptyMessage(1);
        mHandler2.sendEmptyMessage(2);
    }
}
複製程式碼

小結: Handler 與 Looper 是多對一的關係,建立 Handler 例項時要麼提供一個 Looper 例項,要麼當前執行緒有關聯的 Looper。

訊息如何分發到對應的 Handler

訊息的分發在是 Looper.loop() 這個過程中:

public static void loop() {
    // ...
    for (;;) {
        Message msg = queue.next(); // might block
        // ...
        try {
            msg.target.dispatchMessage(msg);
            // ...
        } finally {
            // ...
        }
        // ...
    }
}
複製程式碼

這個方法裡做的主要工作是從 MessageQueue 裡依次取出 Message,然後呼叫 Message.target.dispatchMessage 方法,Message 物件的這個 target 成員是一個 Handler,它最終會被設定成 sendMessage 的 Handler:

public class Handler {
    // 其它 Handler.sendXXX 方法最終都會呼叫到這個方法
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        // ...
        return enqueueMessage(queue, msg, uptimeMillis);
    }

    // ...
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this; // 就是這裡了
        // ...
    }
    // ...
}
複製程式碼

所以是用哪個 Handler.sendMessage,最終就會呼叫到它的 dispatchMessage 方法:

private static void handleCallback(Message message) {
    message.callback.run();
}
// ...
/**
 * 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);
    }
}
複製程式碼

訊息分發到這個方法以後,執行優先順序分別是 Message.callback、Handler.mCallback,最後才是 Handler.handleMesage 方法。

小結: 在 Handler.sendMessage 時,會將 Message.target 設定為該 Handler 物件,這樣從訊息佇列取出 Message 後,就能呼叫到該 Handler 的 dispatchMessage 方法來進行處理。

Handler 能用於執行緒切換的原理

小結: Handler 會對應一個 Looper 和 MessageQueue,而 Looper 與執行緒又一一對應,所以通過 Handler.sendXXX 和 Hanler.postXXX 新增到 MessageQueue 的 Message,會在這個對應的執行緒的 Looper.loop() 裡取出來,並就地執行 Handler.dispatchMessage,這就可以完成執行緒切換了。

Runnable 與 MessageQueue

Handler 的 postXXX 系列方法用於排程 Runnable 物件,那它最後也是和 Message 一樣被加到 MessageQueue 的嗎?可是 MessageQueue 是用一個元素型別為 Message 的連結串列來維護訊息佇列的,型別不匹配。

在 Handler 原始碼裡能找到答案,這裡就以 Handler.post(Runnable) 方法為例,其它幾個 postXXX 方法情形與此類似。

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

// ...

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}
複製程式碼

可以看到,post 系列方法最終也是呼叫的 send 系列方法,Runnable 物件是被封裝成 Message 物件後加入到訊息佇列的,Message.callback 被設定為 Runnable 本身。如果 Message.callback 不為空,則執行 Message.callback.run() 後就返回。

小結: Runnable 被封裝成 Message 之後新增到 MessageQueue。

能否建立關聯到其它執行緒的 Handler

建立 Handler 時會關聯到一個 Looper,而 Looper 是與執行緒一一繫結的,所以理論上講,如果能得到要關聯的執行緒的 Looper 例項,這是可以實現的。

public final class Looper {
    // ...
    private static Looper sMainLooper;  // guarded by Looper.class
    // ...
    /**
     * Returns the application's main looper, which lives in the main thread of the application.
     */
    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }
}
複製程式碼

可見獲取主執行緒的 Looper 是能實現的,平時寫程式碼過程中,如果要從子執行緒向主執行緒新增一段執行邏輯,也經常這麼幹,這是可行的:

// 從子執行緒建立關聯到主執行緒 Looper 的 Handler
Handler mHandler = new Handler(Looper.getMainLooper());

mHandler.post(() -> {
        // ...
        });
複製程式碼

從子執行緒建立關聯到其它子執行緒的 Looper :

new Thread() {
    @Override
    public void run() {
        setName("thread-one");
        Looper.prepare();

        final Looper threadOneLooper = Looper.myLooper();

        new Thread() {
            @Override
            public void run() {
                setName("thread-two");
                Handler handler = new Handler(threadOneLooper);

                handler.post(() -> {
                        Log.v("test", Thread.currentThread().getName());
                        });
            }
        }.start();

        Looper.loop();
    }
}.start();
複製程式碼

執行後日志輸出為 thread-one。

小結: 可以從一個執行緒建立關聯到另一個執行緒 Looper 的 Handler,只要能拿到對應執行緒的 Looper 例項。

訊息可以插隊嗎

答案是可以的,使用 Handler.sendMessageAtFrontOfQueue 和 Handler.postAtFrontOfQueue 這兩個方法,它們會分別將 Message 和 Runnable(封裝後)插入到訊息佇列的隊首。

小結: 訊息可以插隊,使用 Handler.xxxAtFrontOfQueue 方法。

訊息可以撤回嗎

可以用 Handler.hasXXX 系列方法判斷關聯的訊息佇列裡是否有等待中的符合條件的 Message 和 Runnable,用 Handler.removeXXX 系列方法從訊息佇列裡移除等待中的符合條件的 Message 和 Runnable。

小結: 尚未分發的訊息是可以撤回的,處理過的就沒法了。

找到主執行緒訊息迴圈原始碼

Looper.prepareMainLooper 是 Android 環境呼叫的,呼叫它就是為了初始化主執行緒 Looper。

public final class ActivityThread {
    public static void main(String[] args) {
        // ...
        Looper.prepareMainLooper();
        // ...
        Looper.loop();
        // ...
    }
}
複製程式碼

MessageQueue.next()

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

        // 呼叫JNI函式Poll訊息。nextPollTimeoutMillis是訊息佇列中沒訊息時的等待時間。
        // (01) nextPollTimeoutMillis = 0,不等待。
        // (02) nextPollTimeoutMillis = -1,無限等待。
        nativePollOnce(mPtr, nextPollTimeoutMillis);

        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            // 如果當前訊息非空,但是當前訊息的目標是空;則獲取下一則訊息。
            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // 如果訊息佇列中有訊息,並且當前時間小於於訊息中的執行時間,
                    // 則設定訊息的等待時間
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 如果訊息佇列中有訊息,並且當前時間大於/等於訊息中的執行時間,
                    // 則將該訊息返回給Looper。
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (false) Log.v("MessageQueue", "Returning message: " + msg);
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 如果訊息佇列中無訊息,則設定nextPollTimeoutMillis=-1;
                // 下次呼叫nativePollOnce()時,則會進入無窮等待狀態。
                nextPollTimeoutMillis = -1;
            }

            // 如主執行緒呼叫的quit()函式,則退出訊息迴圈。
            if (mQuitting) {
                dispose();
                return null;
            }

            // 檢視空閒等待(不是忙等待)對應的pendingIdleHandlerCount數量。
            // 如果pendingIdleHandlerCount=0,則繼續下一次迴圈。
            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)];
            }
            // 將mIdleHandlers轉換位陣列
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // 執行mPendingIdleHandlers中每一個IdleHandler的queueIdle(),
        // 即,進行空閒等待。
        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("MessageQueue", "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()的作用是獲取訊息佇列的下一條待處理訊息。方法中使用了 SystemClock.uptimeMillis() 方法獲取了當前的時間。

Android 中的時間

System.currentTimeMillis()

我們一般通過它來獲取手機系統的當前時間。事實上,它返回的值是系統時刻距離標準時刻(1970.01.01 00:00:00)的毫秒數。它相當於家裡的“掛鐘”一樣,並不是十分精準,而且可以隨意修改。所以它可能經常被網路或者使用者校準。正是由於這個原因,這個方法獲取的值不適合用來做時間間隔的統計。但是它適合用來獲取當前日期,時刻等時間點相關的邏輯。

SystemClock.upTimeMillis()

這個值記錄了系統啟動到當前時刻經過的時間。但是系統深度睡眠(CPU睡眠,黑屏,系統等待喚醒)之中的時間不算在內。這個值不受系統時間設定,電源策略等因素的影響,因此它是大多數時間間隔統計的基礎,例如Thread.sleep(long millis),Object.wait(long millis),System.nanoTime()等。系統保證了這個值只增長不下降,所以它適合所有的不包括系統睡眠時間的時間間隔統計。

SystemClock.elapsedRealtime() & SystemClock.elapsedRealtimeNanos()

這個值與SystemClock.upTimeMillis()類似。它是系統啟動到當前時刻經過的時間,包括了系統睡眠經過的時間。在CPU休眠之後,它依然保持增長。所以它適合做更加廣泛通用的時間間隔的統計。

小結

如果想要避免使用者修改時間,網路校準時間對時間間隔統計的影響,使用SystemClock類相關的方法就可以了,至於選擇upTimeMillis()還是elapsedRealtime()就要根據自己的需求確定了。 ##系統還提供了幾個時間控制相關的工具:

  • 標準方法Thread.sleep(long millis) 和 Object.wait(long millis)是基於SystemClock.upTimeMillis()的。所以在系統休眠之後它們的回撥也會延期,直到系統被喚醒才繼續計時。並且這兩個同步方法會響應InterruptException,所以在使用它們的時候必須要處理InterruptException異常。
  • SystemClock.sleep(long millis) 與 Thread.sleep(long millis) 方法是類似的,只不過SystemClock.sleep(long millis) 不響應InterruptException異常。
  • Handler類的 postDelay()方法也是基於SystemClock.upTimeMillis()方法的。
  • AlarmManager可以定時傳送訊息,即使在系統睡眠、應用停止的狀態下也可以傳送。我們在建立定時事件的時候有兩個引數可以選擇RTC和ELAPSED_REALTIME,它們對應的方法就是System.currentTimeMillis() ~ RTC,SystemClock.elapsedRealtime() ~ ELAPSED_REALTIME。這樣一對應,它們的區別也就非常明顯了。

相關文章