本文以源碼分析+實際應用的形式,詳細講解了 Handler 機制的原理,以及在開發中的使用場景和要注意的地方。java
在 Android 開發中,Handler及相關衍生類的應用常常用到,Android的運行也是創建在這套機制上的,因此瞭解其中的原理細節,以及其中的坑對於每位開發者來講都是很是有必要的。Handler機制的五個組成部分:Handler、Thread(ThreadLocal)、Looper、MessageQueue、Message。android
Handler機制用到的跟Thread相關的,而根本緣由是Handler必須和對應的Looper綁定,而Looper的建立和保存是跟Thread一一對應的,也就是說每一個線程均可以建立惟一一個且互不相關的Looper,這是經過ThreadLocal來實現的,也就是說是用ThreadLocal對象來存儲Looper對象的,從而達到線程隔離的目的。數組
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)); }
Handler() Handler(Callback callback) Handler(Looper looper) Handler(Looper looper, Callback callback) Handler(boolean async) Handler(Callback callback, boolean async) Handler(Looper looper, Callback callback, boolean async)
一種是不傳Looper安全
這種就須要在建立Handler前,預先調用Looper.prepare來建立當前線程的默認Looper,不然會報錯。
一種是傳入指定的Looper網絡
這種就是Handler和指定的Looper進行綁定,也就是說Handler實際上是能夠跟任意線程進行綁定的,不侷限於在建立Handler所在的線程裏。
這裏Handler有個async參數,經過這個參數代表經過這個Handler發送的消息全都是異步消息,由於在把消息壓入隊列的時候,會把這個標誌設置到message裏.這個標誌是全局的,也就是說經過構造Handler函數傳入的async參數,就肯定了經過這個Handler發送的消息都是異步消息,默認是false,即都是同步消息。至於這個異步消息有什麼特殊的用途,咱們在後面講了屏障消息後,再聯繫起來說。異步
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
這個回調參數是消息被分發以後的一種回調,最終是在msg調用Handler的dispatchMessage時,根據實際狀況進行回調:async
public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } }
用於爲線程運行消息循環的類。默認線程沒有與它們相關聯的Looper;因此要在運行循環的線程中調用prepare(),而後調用loop()讓它循環處理消息,直到循環中止。ide
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)); } public static void loop() { ... for (;;) { ... } ... } class LooperThread extends Thread { public Handler mHandler; public void run() { Looper.prepare(); mHandler = new Handler() { public void handleMessage(Message msg) { Message msg=Message.obtain(); } }; Looper.loop(); } }
既然在使用Looper前,必須調用prepare建立Looper,爲何咱們日常在主線程裏沒有看到調用prepare呢?這是由於Android主線程建立的時候,在ActivityThread的入口main方法裏就已經默認建立了Looper。函數
public static void main(String[] args) { ... Looper.prepareMainLooper(); ... Looper.loop(); ... }
咱們再來回顧一下Looper相關類的之間的聯繫:oop
MessageQueue是一個消息隊列,Handler將Message發送到消息隊列中,消息隊列會按照必定的規則取出要執行的Message。Message並非直接加到MessageQueue的,而是經過Handler對象和Looper關聯到一塊兒。
MessageQueue裏的message是按時間排序的,越早加入隊列的消息放在隊列頭部,優先執行,這個時間就是sendMessage的時候傳過來的,默認是用的當前系統從啓動到如今的非休眠的時間SystemClock.uptimeMillis()。
sendMessageAtFrontOfQueue 這個方法傳入的時間是0,也就是說調用這個方法的message確定會放到對消息隊列頭部,可是這個方法不要輕易用,容易引起問題。
存到MessageQueue裏的消息可能有三種:同步消息,異步消息,屏障消息。
咱們默認用的都是同步消息,即前面講Handler裏的構造函數參數的async參數默認是false,同步消息在MessageQueue裏的存和取徹底就是按照時間排的,也就是經過msg.when來排的。
異步消息就是在建立Handler若是傳入的async是true或者發送來的Message經過msg.setAsynchronous(true);後的消息就是異步消息,異步消息的功能要配合下面要講的屏障消息纔有效,不然和同步消息是同樣的處理。
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { msg.target = this; // 這個mAsynchronous就是在建立Handler的時候傳入async參數 if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
屏障(Barrier) 是一種特殊的Message,它最大的特徵就是target爲null(只有屏障的target能夠爲null,若是咱們本身設置Message的target爲null的話會報異常),而且arg1屬性被用做屏障的標識符來區別不一樣的屏障。屏障的做用是用於攔截隊列中同步消息,放行異步消息。
那麼屏障消息是怎麼被添加和刪除的呢?咱們能夠看到在MessageQueue裏有添加和刪除屏障消息的方法:
private int postSyncBarrier(long when) { // Enqueue a new sync barrier token. // We don't need to wake the queue because the purpose of a barrier is to stall it. synchronized (this) { final int token = mNextBarrierToken++; final Message msg = Message.obtain(); msg.markInUse(); msg.when = when; msg.arg1 = token; Message prev = null; Message p = mMessages; if (when != 0) { // 這裏是說若是p指向的消息時間戳比屏障消息小,說明這個消息比屏障消息先進入隊列, // 那麼這個消息不該該受到屏障消息的影響(屏障消息隻影響比它後加入消息隊列的消息),找到第一個比屏障消息晚進入的消息指針 while (p != null && p.when <= when) { prev = p; p = p.next; } } // 上面找到第一個比屏障消息晚進入的消息指針以後,把屏障消息插入到消息隊列中,也就是屏障消息指向第一個比它晚進入的消息p, // 上一個比它早進入消息隊列的prev指向屏障消息,這樣就完成了插入。 if (prev != null) { // invariant: p == prev.next msg.next = p; prev.next = msg; } else { // 若是prev是null,說明上面沒有通過移動,也就是屏障消息就是在消息隊列的頭部了。 msg.next = p; mMessages = msg; } return token; } } public void removeSyncBarrier(int token) { // Remove a sync barrier token from the queue. // If the queue is no longer stalled by a barrier then wake it. synchronized (this) { Message prev = null; Message p = mMessages; // 前面在插入屏障消息後會生成一個token,這個token就是用來刪除該屏障消息用的。 // 因此這裏經過判斷target和token來找到該屏障消息,從而進行刪除操做 // 找到屏障消息的指針p while (p != null && (p.target != null || p.arg1 != token)) { prev = p; p = p.next; } if (p == null) { throw new IllegalStateException("The specified message queue synchronization " + " barrier token has not been posted or has already been removed."); } final boolean needWake; // 上面找到屏障消息的指針p後,把前一個消息指向屏障消息的後一個消息,這樣就把屏障消息移除了 if (prev != null) { prev.next = p.next; needWake = false; } else { mMessages = p.next; needWake = mMessages == null || mMessages.target != null; } p.recycleUnchecked(); // If the loop is quitting then it is already awake. // We can assume mPtr != 0 when mQuitting is false. if (needWake && !mQuitting) { nativeWake(mPtr); } } }
說完了屏障消息的插入和刪除,那麼屏障消息在哪裏起做用的?它跟前面提到的異步消息又有什麼關聯呢?咱們能夠看到MessageQueue的next方法裏有這麼一段:
// 這裏就是判斷當前消息是不是屏障消息,判斷依據就是msg.target==null, 若是存在屏障消息,那麼在它以後進來的消息中, // 只把異步消息放行繼續執行,同步消息阻塞,直到屏障消息被remove掉。 if (msg != null && msg.target == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; // 這裏的isAsynchronous方法就是前面設置進msg的async參數,經過它判斷若是是異步消息,則跳出循環,把該異步消息返回 // 不然是同步消息,把同步消息阻塞。 } while (msg != null && !msg.isAsynchronous()); }
屏障消息的做用是把在它以後入隊的同步消息阻塞,可是異步消息仍是正常按順序取出執行,那麼它的實際用途是什麼呢?咱們看到ViewRootImpl.scheduleTraversals()用到了屏障消息和異步消息。
TraversalRunnable的run(),在這個run()中會執行doTraversal(),最終會觸發View的繪製流程:measure(),layout(),draw()。爲了讓繪製流程儘快被執行,用到了同步屏障技術。
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; // 這裏先將主線程的MessageQueue設置了個消息屏障 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 這裏發送了個異步消息mTraversalRunnable,這個mTraversalRunnable最終會執行doTraversal(),也就是會觸發View的繪製流程 // 也就是說經過設置屏障消息,會把主線程的同步消息先阻塞,優先執行View繪製這個異步消息進行界面繪製。 // 這很好理解,界面繪製的任務確定要優先,不然就會出現界面卡頓。 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } } private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { if (DEBUG_FRAMES) { Log.d(TAG, "PostCallback: type=" + callbackType + ", action=" + action + ", token=" + token + ", delayMillis=" + delayMillis); } synchronized (mLock) { final long now = SystemClock.uptimeMillis(); final long dueTime = now + delayMillis; mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); if (dueTime <= now) { scheduleFrameLocked(now); } else { Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); msg.arg1 = callbackType; // 設置該消息是異步消息 msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); } } }
那麼除了系統中使用到了屏障消息,咱們在開發中有什麼場景能派上用場嗎? 運用屏障消息能夠阻塞同步消息的特性,咱們能夠用來實現UI界面初始化和數據加載同時進行。
咱們通常在Activity建立的時候,爲了減小空指針異常的發生,都會在onCreate先setContent,而後findView初始化控件,而後再執行網絡數據加載的異步請求,待網絡數據加載完成後,再刷新各個控件的界面。
試想一下,怎麼利用屏障消息的特性來達到界面初始化和異步網絡數據的加載同時進行,而不影響界面渲染?先來看一個時序圖:
咱們經過下面僞代碼進一步加深理解:
// 在上一個頁面裏異步加載下一個頁面的數據 // 網絡請求返回的數據 Data netWorkData; // 建立屏障消息會生成一個token,這個token用來刪除屏障消息,很重要。 int barrierToken; // 建立異步線程加載網絡數據 HandlerThread thread = new HandlerThread("preLoad"){ @Override protected void onLooperPrepared() { Handler mThreadHandler = new Handler(thread.getLooper()); // 一、把請求網絡耗時消息推入消息隊列 mHandler.post(new Runnable() { @Override public void run() { // 異步耗時操做:網絡請求數據,賦值給netWorkData netWorkData = xxx; } }); // 二、而後給異步線程的隊列發一個屏障消息推入消息隊列 barrierToken = thread.getLooper().getQueue().postSyncBarrier(); // 三、而後給異步線程的消息隊列發一個刷新UI界面的同步消息 // 這個消息在屏障消息被remove前得不到執行的。 mHandler.post(new Runnable() { @Override public void run() { // 回調主線程, 把netWorkData賦給監聽方法,刷新界面 } }); } }; thread.start(); // 當前界面初始化界面 protected void onCreate(Bundle savedInstanceState) { setContentView(view); // 各類findview操做完成 Button btn = findViewById(R.id.xxx); ... // 四、待控件初始化完成,把異步線程設置的屏障消息remove掉,這樣異步線程請求數據完成後,三、處的刷新UI界面的同步消息就有機會執行,就能夠安全得刷新界面了。 thread.getLooper().getQueue().removeSyncBarrier(barrierToken); }
可是,MessageQueue源碼裏咱們咱們看到,屏障消息的建立和刪除都是隱藏方法(@hide),咱們無法直接調用,只能用反射來調用,因此在實際使用中還得綜合驗證。
IdleHandler,字面意思就是空閒的處理器(就是說我是在消息隊列空閒的時候纔會執行的,若是消息隊列裏有其餘非IdleHandler消息在執行,則我先不執行),它其實就是一個接口,咱們就認爲它是空閒消息吧,只不過它不是存在MessageQueue裏,而是以數組的形式保存的。
public static interface IdleHandler { /** * Called when the message queue has run out of messages and will now * wait for more. Return true to keep your idle handler active, false * to have it removed. This may be called if there are still messages * pending in the queue, but they are all scheduled to be dispatched * after the current time. */ boolean queueIdle(); }
咱們看到MessageQueue有添加和刪除IdleHandler的方法,IdleHandler被保存在一個ArrayList裏:
private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>(); ... public void addIdleHandler(@NonNull IdleHandler handler) { if (handler == null) { throw new NullPointerException("Can't add a null IdleHandler"); } synchronized (this) { mIdleHandlers.add(handler); } } public void removeIdleHandler(@NonNull IdleHandler handler) { synchronized (this) { mIdleHandlers.remove(handler); } }
那麼,它是怎麼實如今消息隊列空閒的間隙獲得執行的呢?仍是看next()方法。
// 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. // pendingIdleHandlerCount < 0是說for循環只執行第一次 // mMessages == null || now < mMessages.when) 是說當前消息隊列沒有消息或者要執行的消息晚於當前時間 // 說明如今消息隊列處於空閒。 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; }
在上面這段代碼斷定當前消息隊列處於空閒後,就會拿到空閒消息的大小,下面這段代碼就是把把空閒消息執行一遍。
// 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 { // 若是queueIdle返回true,則該空閒消息不會被自動刪除,在下次執行next的時候,若是還出現隊列空閒,會再次執行。 // 若是返回false,則該空閒消息會在執行完後,被自動刪除掉。 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. // 這裏把空閒消息標誌置爲0,而不置爲-1,就是說本次已經處理完,防止for循環反覆執行,影響其餘消息的執行 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;
總結一下:
- 若是本次循環拿到的消息爲空,或者這個消息是一個延時的消息並且還沒到指定的觸發時間,那麼,就認定當前的隊列爲空閒狀態。
- 接着就會遍歷mPendingIdleHandlers數組(這個數組裏面的元素每次都會到mIdleHandlers中去拿)來調用每個IdleHandler實例的queueIdle方法, 若是這個方法返回false的話,那麼這個實例就會從mIdleHandlers中移除,也就是當下次隊列空閒的時候,不會繼續回調它的queueIdle方法了。
- 處理完IdleHandler後會將nextPollTimeoutMillis設置爲0,也就是不阻塞消息隊列,固然要注意這裏執行的代碼一樣不能太耗時,由於它是同步執行的,若是太耗時確定會影響後面的message執行。
IdleHandler的原理大概就是上面講的那樣,那麼能力決定用處,從本質上講就是趁着消息隊列空閒的時候乾點事情,具體作什麼,是在IdleHandler的queueIdle()方法裏。那麼IdleHandler在系統源碼裏使用場景是怎樣的?咱們能夠看到它在主線程生命週期處理中使用比較多,好比在ActivityThread裏有個 就有一個名叫GcIdler的內部類,實現的就是IdleHandler接口,它的做用就是在主線程空閒的時候對內存進行強制GC。
final class GcIdler implements MessageQueue.IdleHandler { @Override public final boolean queueIdle() { doGcIfNeeded(); return false; } } // 這裏的意思就是說判斷距離上次GC的時間是否超過5秒,超過則執行後臺強制GC void doGcIfNeeded() { mGcIdlerScheduled = false; final long now = SystemClock.uptimeMillis(); //Slog.i(TAG, "**** WE MIGHT WANT TO GC: then=" + Binder.getLastGcTime() // + "m now=" + now); if ((BinderInternal.getLastGcTime()+MIN_TIME_BETWEEN_GCS) < now) { //Slog.i(TAG, "**** WE DO, WE DO WANT TO GC!"); BinderInternal.forceGc("bg"); } }
咱們看看它是在哪裏添加到消息隊列的:
// 這個方法是在mH的handleMessage方法裏調的,也就是說也是經過AMS(ActivityManagerService)把消息發送到主線程消息隊列 void scheduleGcIdler() { if (!mGcIdlerScheduled) { mGcIdlerScheduled = true; Looper.myQueue().addIdleHandler(mGcIdler); } mH.removeMessages(H.GC_WHEN_IDLE); }
還有就是在ActivityThread的performLaunchActivity方法執行時,最終會執行到Instrumentation.callActivityOnCreate方法,在這個方法裏,也有用到IdleHandler作一些額外的事情。
public void callActivityOnCreate(Activity activity, Bundle icicle) { prePerformCreate(activity); activity.performCreate(icicle); postPerformCreate(activity); } private void prePerformCreate(Activity activity) { if (mWaitingActivities != null) { synchronized (mSync) { final int N = mWaitingActivities.size(); for (int i=0; i<N; i++) { final ActivityWaiter aw = mWaitingActivities.get(i); final Intent intent = aw.intent; if (intent.filterEquals(activity.getIntent())) { aw.activity = activity; mMessageQueue.addIdleHandler(new ActivityGoing(aw)); } } } } }
除此以外,在一些第三方庫中都有使用IdleHandler,好比LeakCanary,Glide中有使用到。
那麼對於咱們來講,IdleHandler能夠有些什麼使用場景呢?根據它最核心的原理,在消息隊列空閒的時候作點事情,那麼對於主線程來說,咱們有不少的一些代碼不是必需要跟隨生命週期方法同步執行的,就能夠用IdleHandler,減小主線程的耗時,也就減小應用或者Activity的啓動時間。例如:一些第三方庫的初始化,埋點尤爲是延時埋點上報等,均可以用IdleHandler添加到消息隊列裏。
==好了,提個問題:前面咱們說了在主線程建立的main函數裏建立了Handler和Looper,回顧了上面的Handler機制的原理,咱們都知道通常線程執行完就會退出,由系統回收資源,那Android UI線程也是基於Handler Looper機制的,那麼爲何UI線程能夠一直常駐?不會被阻塞呢?==
由於Looper在執行loop方法裏,是一個for循環,也就是說線程永遠不會執行完退出,因此打開APP能夠一直顯示,Activity的生命週期就是經過消息隊列把消息一個一個取出來執行的,而後由於MessageQueue的休眠喚醒機制,當消息隊列裏沒有消息時,消息隊列會進入休眠,並釋放CPU資源,當又有新消息進入隊列時,會喚醒隊列,把消息取出來執行。
HandlerThread本質上是一個Thread,所不一樣的是,它充分利用了Handler機制,經過在內部建立Looper循環,外部經過Handler把異步任務推送給消息隊列,從而達到不用重複建立多個Thread,即能將多個異步任務排隊進行異步執行,它的原理很簡單:
@Override public void run() { mTid = Process.myTid(); Looper.prepare(); synchronized (this) { mLooper = Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid = -1; }
在線程的run方法裏建立了looper循環,這樣這個線程不主動quit的話,不會銷燬,有消息則執行消息,沒有消息根據MessageQueue休眠機制,會釋放CPU資源,進入休眠。
使用HandlerThread時,咱們注意到,在建立Handler時,是要傳入線程的Looper進行綁定的,因此必須先執行HandlerThread的start方法,由於執行start方法,纔會執行HandlerThread的run方法,纔會建立線程的Looper,建立Handler傳入的Looper纔不會是null。
因此咱們通常使用是這樣的:
那麼怎麼回收一個HandlerThread呢?咱們看到HandlerThread裏有個quit方法,這個方法最終會調用到MessageQueue的quit方法,從而結束消息分發,最終終止一個HandlerThread線程。
public boolean quit() { Looper looper = getLooper(); if (looper != null) { looper.quit(); return true; } return false; }
IntentService實際上是Service和HandlerThread的結合體,咱們能夠看到在onCreate裏建立了個HandlerThread並建立了個Handler和該HandlerThread綁定,而後在onStat方法裏以消息的形式發送給HandlerThread執行
@Override public void onCreate() { // TODO: It would be nice to have an option to hold a partial wakelock // during processing, and to have a static startService(Context, Intent) // method that would launch the service & hand off a wakelock. super.onCreate(); // 建立HandlerThread HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); thread.start(); // 建立Handler和HandlerThread綁定 mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); } @Override public void onStart(@Nullable Intent intent, int startId) { Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent; // 想HandlerThread的消息隊列發送消息 mServiceHandler.sendMessage(msg); }
最終在handleMessage裏執行
private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { onHandleIntent((Intent)msg.obj); stopSelf(msg.arg1); } }
因此咱們使用IntentService都必須實現onHandleIntent這個抽象方法,在這個抽象方法裏作具體的業務操做。
咱們都知道IntentService在執行完異步任務後,會自動銷燬,這是怎麼實現的?
public ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { onHandleIntent((Intent)msg.obj); // 答案在這裏:在這裏會中止Service stopSelf(msg.arg1); } } // 而後在onDestory裏會終止掉消息循環,從而達到銷燬異步線程的目的: @Override public void onDestroy() { mServiceLooper.quit(); }
咱們先來看個你們日常常用的案例:獲取View的寬高。
@Override protected void onCreate(Bundle savedInstanceState) { // 位置1 Log.i("view_w_&_h", "onCreate " + mView.getWidth() + " " + mView.getHeight()); mView.post(new Runnable() { @Override public void run() { // 位置2 Log.i("view_w_&_h", "onCreate postRun " + mView.getWidth() + " " + mView.getHeight()); } }); new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { // 位置3 Log.i("view_w_&_h", "onCreate Handler " + mView.getWidth() + " " + mView.getHeight()); } }); } @Override protected void onResume() { super.onResume(); // 位置4 Log.i("view_w_&_h", "onResume " + mView.getWidth() + " " + mView.getHeight()); new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { // 位置5 Log.i("view_w_&_h", "onResume Handler " + mView.getWidth() + " " + mView.getHeight()); } }); }
這幾個位置,哪些能獲取到mView的寬高?
咱們都知道在View被attach到window以前,是獲取不到View的寬高的,由於這個時候View尚未被Measure、layout、draw,因此在onCreate或者onResume直接調用View的寬高方法,都是0,Handler.post在onCreate裏也是獲取不到,可是在onResume裏能獲取到,而View.post不管放在onCreate或者onResume裏,都能獲取到View的寬高,爲何?
咱們先看個簡版的View的繪製流程:
咱們都知道View的最終繪製是在performTraversals()方法裏,包括measure、layout、draw,從上面的圖往上追溯,咱們知道,View的繪製是在ActivityThread的handleResumeActivity方法裏,這個方法相信你們不會陌生,這個方法就是會回調Activity的onResume方法的頂級方法。
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { ... // 這裏追溯進去,最終會調用Activity的onStart方法和onResume方法 r = performResumeActivity(token, clearHide, reason); ... // 調用WindowManager的addView方法,這裏就是最終執行View繪製的地方 wm.addView(decor, l); ... }
從上面的代碼片斷執行順序來看,Activity的onStart和onResume被執行的時候,其實界面尚未開始進行繪製(wm.addView(decor, l)還沒執行到),這裏就能夠解釋爲何用Handler.post在onCreate裏拿不到寬高。由於Handler機制,它是把消息推送到主線程的消息隊列裏去,在onCreate裏把消息推到消息隊列時,onResume的消息都還沒入隊,也就沒有執行,因此拿不到。那爲何onResume裏能拿到呢?由於消息隊列的機制,Handler.post推送的消息,必須得等上一個消息執行完才能獲得執行,因此它必須得等handleResumeActivity執行完,而handleResumeActivity執行完成後,View已經繪製完成了,固然就能拿到寬高了。
好了,如今解釋第二個疑問,爲何View.post在onCreate裏能拿到View的寬高呢?咱們先看下View.post方法:
public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; // attachInfo不爲null,說明View已經被attach到window,也就是完成了繪製,因此直接把消息推送到主線程的消息隊列執行。 if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Postpone the runnable until we know on which thread it needs to run. // Assume that the runnable will be successfully placed after attach. // 關鍵就在這裏,走到這裏,說明attachInfo爲null,也就是如今View還沒attach到window,因此把消息臨時保存到RunQueue裏 getRunQueue().post(action); return true; }
上面咱們能夠看到,若是attachInfo爲null,則Runnable會臨時存儲起來,保存到RunQueue裏,並無當即執行,那麼保存到RunQueue是何時被執行的呢?
咱們看到HandlerActionQueue有個executeActions方法,這個方法就是用來執行保存其中的Runnable的:
public void executeActions(Handler handler) { synchronized (this) { final HandlerAction[] actions = mActions; for (int i = 0, count = mCount; i < count; i++) { final HandlerAction handlerAction = actions[i]; handler.postDelayed(handlerAction.action, handlerAction.delay); } mActions = null; mCount = 0; } }
那麼這個方法是在什麼時機調用的呢?接着往下看:在View的dispatchAttachedToWindow方法裏,咱們看到調用了RunQueue的executeActions,執行保存在RunQueue裏的runnable。
void dispatchAttachedToWindow(AttachInfo info, int visibility) { ... // Transfer all pending runnables. if (mRunQueue != null) { mRunQueue.executeActions(info.mHandler); mRunQueue = null; } onAttachedToWindow(); ... }
那麼dispatchAttachedToWindow又是在何時被調用呢?在ViewRootImpl的performTraversals方法裏,咱們看到dispatchAttachedToWindow被執行。host就是DecorView。
private void performTraversals() { ... host.dispatchAttachedToWindow(mAttachInfo, 0); ... performMeasure(); ... performLayout(); ... performDraw(); }
從前面的View繪製的UML時序圖,咱們知道,performTraversals是在ActivityThread的handleResumeActivity被調用的。
總結一下:
系統在執行ActivityThread的handleResumeActivity的方法裏,最終會調到ViewRootImpl的performTraversals()方法,performTraversals()方法調用host的dispatchAttachedToWindow()方法,host就是DecorView也就是View,接着在View的dispatchAttachedToWindow()方法中調用mRunQueue.executeActions()方法,這個方法內部會遍歷HandlerAction數組,利用Handler來post以前存放的Runnable。
這裏就能夠解釋爲何View.post在onCreate裏一樣能夠獲得View的寬高,是由於View.post發出的消息,它被執行的時機是在View被繪製以後。
==可能有同窗要問了:dispatchAttachedToWindow 方法是在 performMeasure 方法以前調用的,既然在調用的時候尚未執行performMeasure來進行測量,那麼爲何在執行完dispatchAttachedToWindow方法後就能夠獲取到寬高呢?==
仍是回到Handler機制最基本的原理,消息是以隊列的形式存在消息隊列裏,而後依次等待Loop執行的,而performTraversals的執行它自己就是在一個Runnable消息裏,因此performTraversals在執行的時候,其餘消息得等performTraversals執行完了才能獲得執行,也就是說mRunQueue.executeActions()的消息必須得等performTraversals完全執行完才能獲得執行,因此View.post(runnable)中的runnable執行是要在performTraversals方法執行完以後的,並不是一調用dispatchAttachedToWindow就會執行。
前面還遺留了一個問題:View.post方法裏的mAttachInfo是在何時賦值的呢?
public ViewRootImpl(Context context, Display display) { ... mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context); ... }
咱們看到它是在ViewRootImpl的構造函數裏被賦值的,那麼ViewRootImpl是何時被建立的呢?順着往上找,咱們看到,它是在WindowManagerGlobal的add方法裏被建立的。
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ... ViewRootImpl root; ... root = new ViewRootImpl(view.getContext(), display); ... }
前面也講了WindowManagerGlobal的addView方法是在ActivityThread的handleResumeActivity()方法裏被執行的,因此問題就解開了,爲何View.post方法裏會先判斷mAttachInfo是否爲空,不爲空,說明View.post的調用時機是在onResume以後,也就是View繪製完成以後,這個時候直接推入主線程消息隊列執行就能夠。而若是mAttachInfo爲空,說明View還沒繪製完,先暫存起來,待繪製完後再依次推入主線程執行。
要注意的是View.post方法是有坑的,android版本 < 24,也就是7.0如下的系統。
// 7.0如下系統 public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Assume that post will succeed later // 注意此處,不一樣於我7.0及以上系統, ViewRootImpl.getRunQueue().post(action); return true; }
而咱們看一下 ViewRootImpl 的RunQueue是怎麼實現的:
static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>(); static RunQueue getRunQueue() { RunQueue rq = sRunQueues.get(); if (rq != null) { return rq; } rq = new RunQueue(); sRunQueues.set(rq); return rq; }
結合前面講的ThreadLocal特性,它是跟線程相關的,也就是說保存其中的變量只在本線程內可見,其餘線程獲取不到。
好了,假設有這種場景,咱們子線程裏用View.post一個消息,從上面的代碼看,它會保存子線程的ThreadLocal裏,可是在執行RunQueue的時候,又是在主線程裏去找runnable調用,由於ThreadLocal線程隔離,主線程永遠也找不到這個消息,這個消息也就無法獲得執行了。
而7.0及以上沒有這個問題,是由於在post方法裏把runnable保存在主線程裏:getRunQueue().post(action)。
總結一下:
上面這個問題的前提有兩個:View被繪製前,且在子線程裏調用View.post。若是View.post是在View被繪製以後,也就是mAttachInfo非空,那麼會當即推入主線程調用,也就不存在因線程隔離找不到runnable的問題。
做者:He Ying