Android Handler機制的工作原理
寫在前面
上一次寫完Binder學習筆記之後,再去看一遍Activity的啟動流程,因為了解了Binder的基本原理,這次看印象會更深一點,學習效果也比以前好很多。本來打算直接來寫Activity的啟動流程的,但總覺得Handler也需要寫一下,知道Handler和Binder的原理後,再去看Activity的啟動流程,應該也沒什麼問題了。雖然網上已經有很多Handler相關的文章了,而且Handler機制的上層原理也並不難,還是決定寫一下,因為我想構建自己的知識體系。也希望給看我部落格的朋友們一個無縫銜接的閱讀體驗。
Handler機制涉及到的類主要有Handler、Message、Looper、MessageQueue、ThreadLocal等。雖然我們最熟悉的是Handler和Message這兩個類,但是在我們開始可以使用Handler之前,Looper是為我們做了一些事情的。
本文的原始碼是基於android-28的
Looper
在使用Handler之前,我們必須得初始化Looper,並讓Looper跑起來。
Looper.prepare(); ... Looper.loop();
執行上面兩條語句之後,Looper就可以跑起來了。先來看看對應的原始碼:
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)); } private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
必須保證一個執行緒中有且只有一個Looper物件,所以在初始化Looper的時候,會檢查當前執行緒有沒有Looper物件。Looper的初始化會建立一個MessageQueue。建立完Looper後會放到ThreadLocal中去,關於ThreadLocal,後面會說到。
public static void loop() { // 判斷當前執行緒有沒有初始化Looper 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; ... for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } ... final long traceTag = me.mTraceTag; if (traceTag != 0 && Trace.isTagEnabled(traceTag)) { Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); } try { // target指的是Handler msg.target.dispatchMessage(msg); } finally { if (traceTag != 0) { Trace.traceEnd(traceTag); } } ... msg.recycleUnchecked(); } }
方法比較長,所以只把最核心的程式碼放了出來。省略掉的程式碼中有一個比較有意思的:我們可以指定一個閾值比如說200,當Message的處理超過200ms時,就會輸出Log。這可以在開發中幫助我們發現一些潛在的效能問題。可惜的是,設定閾值的方法是隱藏的,無法直接呼叫,所以這裡就不放出程式碼了,感興趣的朋友自己翻一下原始碼吧。
簡化後的程式碼可以看出邏輯十分簡單,可以說Looper在當中扮演著搬磚工的角色,從MessageQueue中取出Message,然後交給Handler去分發,再去MessageQueue中取出Message...無窮無盡,就像愚公移山一樣。
看到這裡,應該多多少少會覺得有點不對勁,因為這裡是一個死迴圈,按道理來說會一直佔著CPU資源的,並且訊息也總有處理完的時候,難道處理完就從訊息佇列返回Null,然後Looper結束嗎?顯然不是,注意看註釋might block
。
MessageQueue
答案就在MessageQueue裡面,直接來看一下next()
:
Message next() { ... 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; } }
程式碼有點長,這次不打算省略掉一些了,因為這裡面還有一個小彩蛋。
方法中最重要的應該就是這一行了
nativePollOnce(ptr, nextPollTimeoutMillis);
簡單來說,當nextPollTimeoutMillis == -1
時,掛起當前執行緒,釋放CPU資源,當nextPollTimeoutMillis >= 0
時會延時指定的時間啟用一次執行緒,讓程式碼繼續執行下去。這裡涉及到了底層的pipe管道和epoll機制,就不再講下去了(其實是因為講不下去了)。這也就可以回答上面的問題了,當沒有訊息的時候只需要讓執行緒掛起就行了,這樣可以保證不佔用CPU資源的同時保住Looper的死迴圈。
然後我們來看訊息是如何取出來的。MessageQueue中有一個Message,Message類中又有一個Message成員next
,可以看出Message是一個單鏈表結構。訊息的順序是根據時間先後順序排列的。一般來說,我們要取的Message就是第一個(這裡先不考慮非同步訊息,關於非同步訊息以後會講到的,又成功給自己挖了一個坑哈哈),如果當前時間大於等於Message中指定的時間,那麼將訊息取出來,返回給Looper。由於此時nextPollTimeoutMillis
的值為0,所以當前面的訊息處理完之後,Looper就又來取訊息了。
如果當前的時間小於Message中指定的時間,那麼設定nextPollTimeoutMillis
值以便下次喚醒。還有另外一種當前已經沒有訊息了,nextPollTimeoutMillis
會被設定為-1,也就是掛起執行緒。別急,還沒那麼快呢,接著往下看。
緊接著的邏輯是判斷當前有沒有IdleHandler,沒有的話就continue,該掛起就掛起,該延時就延時,有IdleHandler的話會執行它的queueIdle()
方法。這個IdleHandler是幹什麼的呢?從名字應該也能猜出個一二來,這裡就不再展開講了。關於它的一些妙用可以看我之前寫的Android 啟動優化之延時載入
。執行完queueIdle()
方法後,會將nextPollTimeoutMillis
置為0,重新看一下訊息佇列中有沒有新的訊息。
Handler
上面將取訊息的流程都講清楚了,萬事俱備,就差往訊息佇列中新增訊息了,該我們最熟悉的Handler出場了。Handler往佇列中新增訊息,主要有兩種方式:
Handler.sendXXX(); Handler.postXXX();
第一種主要是傳送Message,第二種是Runnable。無論是哪種方式,最終都會進入到MessageQueue的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; }
一般情況下,我們通過Handler傳送訊息的時候,會通過SystemClock.uptimeMillis()
獲取一個開機時間,然後MessageQueue就會根據這個時間來對Message進行排序。所以enqueueMessage()
方法中就分了兩種情況,一種是直接可以在隊頭插入的。一種是排在中間,需要遍歷一下,然後尋一個合適的坑插入。when == 0
對應的是Handler的sendMessageAtFrontOfQueue()
和postAtFrontOfQueue()
方法。needWake
的作用是根據情況喚醒Looper執行緒。
上面有一點還沒有講,就是Looper從MessageQueue中取出Message後,會交由Handler進行訊息的分發。
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自帶的callback,最後才是handleMessage()
這個回撥。
ThreadLocal
還記得Looper中有一個ThreadLocal吧,把它放到最後來講是因為它可以單獨拿出來講,不想在上面干擾到整個流程。ThreadLocal是一個數據儲存類,它最神奇的地方就是明明是同一個ThreadLocal物件,但是在不同執行緒中可以儲存不同的物件,比如說線上程A中儲存了"Hello",而線上程B中儲存了"World"。它們之間互相不干擾。
在Handler機制中,由於一個Looper對應著一個執行緒,所以將Looper存進ThreadLocal最合適不過了。
ThreadLocal比價常用的就set()
和get()
方法。分別來看看怎麼實現的吧。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
首先是去獲取ThreadLocalMap,找得到的話直接設定值,找不到就建立一個。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
看到這裡,大概也能明白了。每個執行緒Thread中有一個ThreadLocalMap物件。通過ThreadLocal.set()
方法,實際上是去獲取當前執行緒中的ThreadLocalMap,執行緒不同,獲取到的ThreadLocalMap自然也不同。
再來看看這個ThreadLocalMap是什麼來頭。看類的註釋中有這麼一句話:
ThreadLocalMap is a customized hash map suitable only for maintaining thread local values.
從註釋中可以知道這就是一個定製的HashMap,並且它的Entry類指定了Key只能為ThreadLocal型別的。所以直接將它看成是一個HashMap就好了。
get()
方法也好理解,就是從Map中取出值而已。大概看一下就好了。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
寫在最後
雖然在開始寫之前,覺得Handler機制比較簡單,好像沒啥必要寫,但真正要寫起來的時候還是得去深入瞭解程式碼的細節,然後才發現有些地方以前理解得也不夠好。能理解和能寫出來讓別人理解,其實是不同的層次了。