Android多執行緒之(一)View.post()原始碼分析——在子執行緒中更新UI
提起View.post(),相信不少童鞋一點都不陌生,它用得最多的有兩個功能,使用簡便而且實用:
1)在子執行緒中更新UI。從子執行緒中切換到主執行緒更新UI,不需要額外new一個Handler例項來實現。
2)獲取View的寬高等屬性值。在Activity的onCreate()、onStart()、onResume()等方法中呼叫View.getWidth()等方法時會返回0,而通過post方法卻可以解決這個問題。
本文將由從原始碼角度分析其原理,由於篇幅原因會分(上)、(下)兩篇來進行講解,本篇將分析第1)點。在閱讀文字之前,希望讀者是對Handler的Looper問題有一定了解的,如果不瞭解請先閱讀【朝花夕拾】Handler篇。
本文的主要內容如下:
1、在子執行緒中使用View.post更新UI功能使用示例
一般我們通過使用View.post()實現在子執行緒中更新UI的示例大致如下:
1 private Button mStartBtn; 2 @Override 3 protected void onCreate(Bundle savedInstanceState) { 4 super.onCreate(savedInstanceState); 5 setContentView(R.layout.activity_intent_service); 6 mStartBtn = findViewById(R.id.start); 7 new Thread(new Runnable() { 8 @Override 9 public void run() { 10 mStartBtn.post(new Runnable() { 11 @Override 12 public void run() { 13 //處理一些耗時操作 14 mStartBtn.setText("end"); 15 } 16 }); 17 } 18 }).start(); 19 }
第7行開啟了一個執行緒,第10行通過呼叫post方法,使得在第14行實現了修改自身UI介面的顯示(當然,平時使用中不一定只能在onCreate中,這裡僅舉例而已)。
2、post原始碼分析
在上述例子中,mStartBtn是如何實現在子執行緒中通過post來更新UI的呢?我們進入post原始碼看看。
//====================View.java=================
1 /** 2 * <p>Causes the Runnable to be added to the message queue. 3 * The runnable will be run on the user interface thread.</p> 4 * ...... 5 */ 6 public boolean post(Runnable action) { 7 final AttachInfo attachInfo = mAttachInfo; 8 if (attachInfo != null) { 9 return attachInfo.mHandler.post(action); //① 10 } 11 // Postpone the runnable until we know on which thread it needs to run. 12 // Assume that the runnable will be successfully placed after attach. 13 getRunQueue().post(action); //② 14 return true; 15 }
第1~5行的註釋說,該方法將Runnable新增到訊息佇列中,該Runnable將在UI執行緒執行。這就是該方法的作用,新增成功了就會返回true。
上述原始碼的執行邏輯,關鍵點在mAttachInfo是否為null,這會導致兩種邏輯:
1)mAttachInfo != null,走程式碼①的邏輯。
2)mAttachInfo == null,走程式碼②的邏輯。
當前View尚未attach到Window時,整個View體系還沒有載入完,mAttachInfo就會為null,表現在Activity中,就是onResume()方法還沒有執行完。反之,mAttachInfo就不會為null。這部分內容會在下一篇文章中詳細講解,這裡先知道這個結論。
(1)mAttachInfo != null的情況
對於第一種情況,當看到程式碼①時,應該會竊喜一下,因為看到了老熟人Handler,這就是Handler.post(Runnable)方法,我們再熟悉不過了。這裡的Runnable會在哪個執行緒執行,取決於該Handler例項化時使用的哪個執行緒的Looper。我們繼續跟蹤mHandler是在哪裡例項化的。
1 //=============View.AttachInfo=============== 2 /** 3 * A Handler supplied by a view's {@link android.view.ViewRootImpl}. This 4 * handler can be used to pump events in the UI events queue. 5 */ 6 final Handler mHandler; 7 AttachInfo(IWindowSession session, IWindow window, Display display, 8 ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer, 9 Context context) { 10 ...... 11 mViewRootImpl = viewRootImpl; 12 mHandler = handler; 13 ...... 14 }
我們發現mHandler是在例項化AttachInfo時傳入的,該例項就是前面post方法第7行的mAttachInfo。在View類中只有一處給它賦值的地方:
//===============View.java=============
1 void dispatchAttachedToWindow(AttachInfo info, int visibility) { 2 mAttachInfo = info; 3 ...... 4 }
現在的問題就變成了要追蹤dispatchAttachedToWindow方法在哪裡呼叫的,即從哪裡把AttachInfo傳進來的。這裡我們先停住,看看第二種情況。
(2)mAttachInfo == null的情況
post原始碼中第11、12行,對程式碼②有說明:推遲Runnable,直到我們知道需要它在哪個執行緒中執行。程式碼②處,看看getRunQueue()的原始碼:
1 //=============View.java============ 2 /** 3 * Queue of pending runnables. Used to postpone calls to post() until this 4 * view is attached and has a handler. 5 */ 6 private HandlerActionQueue mRunQueue; 7 /** 8 * Returns the queue of runnable for this view. 9 * ...... 10 */ 11 private HandlerActionQueue getRunQueue() { 12 if (mRunQueue == null) { 13 mRunQueue = new HandlerActionQueue(); 14 } 15 return mRunQueue; 16 }
getRunQueue()是一個單例模式,返回HandlerActionQueue例項mRunQueue。mRunQueue,顧名思義,表示該view的HandlerAction佇列,下面會講到,HandlerAction就是對Runnable的封裝,所以實際就是一個Runnable的佇列。註釋中也提到,它用於推遲post的呼叫,直到該view被附著到Window並且擁有了一個handler。
HandlerActionQueue的關鍵程式碼如下:
1 //============HandlerActionQueue ======== 2 /** 3 * Class used to enqueue pending work from Views when no Handler is attached. 4 * ...... 5 */ 6 public class HandlerActionQueue { 7 private HandlerAction[] mActions; 8 private int mCount; 9 10 public void post(Runnable action) { 11 postDelayed(action, 0); 12 } 13 14 public void postDelayed(Runnable action, long delayMillis) { 15 final HandlerAction handlerAction = new HandlerAction(action, delayMillis); 16 17 synchronized (this) { 18 if (mActions == null) { 19 mActions = new HandlerAction[4]; 20 } 21 mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction); 22 mCount++; 23 } 24 } 25 ...... 26 public void executeActions(Handler handler) { 27 synchronized (this) { 28 final HandlerAction[] actions = mActions; 29 for (int i = 0, count = mCount; i < count; i++) { 30 final HandlerAction handlerAction = actions[i]; 31 handler.postDelayed(handlerAction.action, handlerAction.delay); 32 } 33 34 mActions = null; 35 mCount = 0; 36 } 37 } 38 ...... 39 private static class HandlerAction { 40 final Runnable action; 41 final long delay; 42 43 public HandlerAction(Runnable action, long delay) { 44 this.action = action; 45 this.delay = delay; 46 } 47 ...... 48 } 49 }
正如註釋中所說,該類用於在當前view沒有handler附屬時,將來自View的掛起的作業(就是Runnable)加入到佇列中。
當開始執行post()時,實際進入到了第14行的postDelay()中了。第15行中,將Runnable封裝成了HandlerAction,在39行可以看到HandlerAction實際上就是對Runnable的封裝。第21行作用就是將封裝後的Runnable加入到了陣列中,具體實現我們不深究了,知道其作用就行,而這個陣列就是我們所說的佇列。這個類中post呼叫邏輯還是比較簡單的,就不囉嗦了。
程式碼②處執行的結果就是將post的引數Runnable action新增到View的全域性變數mRunQueue中了,這樣就將Runnable任務儲存下來了。那麼這些Runnable在什麼時候開始執行呢?我們在View類中搜索一下會發現,mRunQueue的真正使用只有一處:
1 //===========View.java============ 2 void dispatchAttachedToWindow(AttachInfo info, int visibility) { 3 ...... 4 // Transfer all pending runnables. 5 if (mRunQueue != null) { 6 mRunQueue.executeActions(info.mHandler); 7 mRunQueue = null; 8 } 9 ...... 10 onAttachedToWindow(); 11 ...... 12 }
這裡我們又到dispatchAttachedToWindow()方法了,第一種情況也是到了這個方法就停下來了。我們看看第6行,傳遞的引數也是形參AttachInfo info的mHandler。進入到HandlerActionQueue類的executeActions可以看到,這個方法的作用就是通過傳進來的Handler,來post掉mRunQueue中儲存的所有Runnable,該方法中的邏輯就不多說了,比較簡單。這些Runnable最終在哪個執行緒執行,就看這個Handler了。
到這裡為止,兩種情況就殊途同歸了,最後落腳點都集中到了dispatchAttachedToWindow方法的AttachInfo引數的mHandler屬性了。所以現在的任務就是找到哪裡呼叫了這個方法,mHandler到底是使用的哪個執行緒的Looper。
3、dispatchAttachedToWindow方法的呼叫
要搞清這個方法的呼叫問題,對於部分童鞋來說可能會稍微有點複雜,所以這裡單獨用一小節來分析。當然,不想深入研究的童鞋,直接記住本節最後的結論也是可以的,不影響對post機制的理解。
這裡需要對框架部分的程式碼進行全域性搜尋,所以需要準備一套系統框架部分的原始碼,以及原始碼閱讀工具。筆者這裡用的是Source Insight來查詢的(不會使用童鞋可以學習一下,使用非常廣的原始碼閱讀工具,推薦閱讀:【工利其器】必會工具之(一)Source Insight篇)。沒有原始碼的童鞋,也可以直接線上查詢,直接通過網站的形式來閱讀原始碼(不知道如何操作的,推薦閱讀:安卓本卓】Android系統原始碼篇之(一)原始碼獲取、原始碼目錄結構及原始碼閱讀工具簡介第四點,AndroidXRef,使用非常廣)。
全域性搜尋後的結果如下:
對於這個結果,我們可以首先排除“Boot-image-profile.txt”和“RecyclerView.java”兩個檔案(原因不需多說吧...如果真的不知道,那就說明還完全沒有到閱讀這篇文章的時候),跟這個方法呼叫相關的類就縮小到View,ViewGroup和ViewRootImpl類中。在View.java中與該方法相關的只有如下兩處,顯然可以排除掉View.java。
ViewRootImpl類中的呼叫如下:
1 //=============ViewRootImpl.java=========== 2 final View.AttachInfo mAttachInfo; 3 ...... 4 public ViewRootImpl(Context context, Display display) { 5 ...... 6 mAttachInfo = new View.AttachInfo(mWindowSession, 7 mWindow, display, this, mHandler, this, context); 8 ...... 9 } 10 11 private void performTraversals() { 12 ...... 13 host.dispatchAttachedToWindow(mAttachInfo, 0); 14 ...... 15 } 16 ...... 17 final ViewRootHandler mHandler = new ViewRootHandler(); 18 ......
追蹤dispatchAttachedToWindow方法的呼叫,目的是為了找到AttachInfo的例項化,從而找到mHandler的例項化,這段程式碼中正好就實現了AttachInfo的例項化,看起來有戲,我們先放這裡,繼續下看ViewGroup類中的呼叫。
在ViewGroup類中,這個方法出現稍微多一點,但是稍微觀察可以發現,根本沒有找到AttachInfo例項化的地方,要麼直接使用的View類中的mAttachInfo(因為ViewGroup是View的子類),要麼就是圖一中通過傳參得到。而圖一的方法,也是重寫的View的方法,所以這個AttachInfo info實際也是來自View。這樣一來我們也就排除了ViewGroup類中的呼叫了,原始的呼叫不在這裡面。
通過排除法,最後可以斷定,最原始的呼叫其實就在ViewRootImpl類中。如果研究過View的繪製流程,那麼就會清楚View體系的繪製流程measure,layout,draw就是從ViewRootImpl類的performTraversals開始的,然後就是對DecorView下面的View樹遞迴繪製的(如果對View的繪製流程不明白的,推薦閱讀我的文章:【朝花夕拾】Android自定義View篇之(一)View繪製流程)。這裡的dispatchAttachedToWindow方法也正好從這裡開始,遞迴遍歷實現各個子View的attach,中途在層層傳遞AttachInfo這個物件。當然,我們在前面介紹View.post原始碼時,就看到過如下的註釋:
1 /** 2 * A Handler supplied by a view's {@link android.view.ViewRootImpl}. This 3 * handler can be used to pump events in the UI events queue. 4 */ 5 final Handler mHandler;
這裡已經很明確說到了這個mHandler是ViewRootImpl提供的,我們也可以根據這個線索,來確定我們的推斷是正確的。有的人可能會吐槽了,原始碼都直接給出了這個說明,那為什麼還要花這麼多精力追蹤dispatchAttachedToWindow的呼叫呢,不是浪費時間嗎?答案是:我們是在研究原始碼及原理,僅僅限於別人的結論是不夠的,這是一個成長過程。對於不想研究本節過程的童鞋,記住結論即可。
結論:View中dispatchAttachedToWindow的最初呼叫,在ViewRootImpl類中;重要引數AttachInfo的例項化,也是在ViewRootImpl類中;所有問題的核心mHandler,也來自ViewRootImpl類中。
4、mHandler所線上程問題分析
通過上一節的分析,現在的核心問題就轉化為mHandler的Looper在哪個執行緒的問題了。在第三節中已經看到mHandler例項化是在ViewRootImpl類例項的時候完成的,且ViewRootHandler類中也沒有指定其Looper。所以,我們現在需要搞清楚,ViewRootImpl是在哪裡例項化的,那麼就清楚了mHandler所線上程問題。
現在追蹤ViewRootImpl時會發現,只有如下一個地方直接例項化了。
1 //==========WindowManagerGlobal========= 2 public void addView(View view, ViewGroup.LayoutParams params, 3 Display display, Window parentWindow) { 4 ...... 5 ViewRootImpl root; 6 ...... 7 root = new ViewRootImpl(view.getContext(), display); 8 ...... 9 }
到這裡,我們就很難繼續追蹤了,因為呼叫addView的地方太多了,很難全域性搜尋,我們先在這裡停一會。其實到這個addView方法時,我們會看到裡面有很多對View view引數的操作,而addView顧名思義,也是在修改UI。而對UI的修改,只能發生主執行緒中,否則會報錯,這是一個常識問題,所以我們完全可以明確,addView這個方法,就是執行在主執行緒的。我想,這樣去理解,應該是完全沒有問題的。但是筆者總感覺還差點什麼,總覺得這裡有點猜測的味道,所以還想一探究竟,看看這個addView方法是否真的就執行在主執行緒。當然,如果不願意繼續深入探究的童鞋,記住本節最後的結論也沒有問題。
既然現在倒著推導比較困難,那就正著來推,這就需要我們有一定的知識儲備了,需要知道Android的主執行緒,Activity的啟動流程,以及Window新增view的相關知識。
我們平時所說的主執行緒,實際上指的就是ActivityThread這個類,它裡面有一個main()函式:
1 public static void main(String[] args) { 2 ...... 3 ActivityThread thread = new ActivityThread(); 4 ...... 5 }
看到這裡,想必非常親切了,Java中程式啟動的入口函式,到這裡就已經進入到Android的主執行緒了(對於Android的主執行緒是否就是UI執行緒這個問題,業內總有些爭議,但官方文件很多地方的表述為主執行緒也就是UI執行緒,既然如此,我們也沒有必要糾結了,把這兩者等同,完全沒有問題)。在main中,例項了一個ActivityThread(),該類中有如下的程式碼:
1 //========ActivityThread.java========= 2 ...... 3 final H mH = new H(); 4 ...... 5 private class H extends Handler { 6 public static final int LAUNCH_ACTIVITY = 100; 7 public static final int RESUME_ACTIVITY = 107; 8 public static final int RELAUNCH_ACTIVITY = 126; 9 ...... 10 public void handleMessage(Message msg) { 11 switch (msg.what) { 12 case LAUNCH_ACTIVITY: 13 ...... 14 case RESUME_ACTIVITY: 15 handleResumeActivity(...) 16 ...... 17 case RELAUNCH_ACTIVITY: 18 ...... 19 } 20 ...... 21 final void handleResumeActivity(...) { 22 ...... 23 ViewManager wm = a.getWindowManager(); 24 ...... 25 wm.addView(decor, l); 26 ...... 27 }
其中定義了一個Handler H,現在毫無疑問,mH使用的是主執行緒的Looper了。如果清楚Activity的啟動流程,就會知道不同場景啟動一個Acitivty時,都會進入到ActivityThread,通過mH來sendMessage,從而直接或間接地在handleMessage回撥方法中呼叫handleResumeActivity(...),顯然,這個方法就執行在主執行緒中了。
handleResumeActivity(...)的第25行會新增DecorView,即開始新增整個View體系了,我們平時所說的View的繪製流程,就是從這裡開始的。這裡我們就需要了解ViewManager、WindowManager、WindowManagerImpl和WindowManagerGlobal類之間的關係了,如下所示:
這裡用到了系統原始碼中常用的一種設計模式——橋接模式,呼叫WindowManagerImpl中的方法時,實際上是由WindowManagerGlobal對應方法來實現的。所以第25行實際執行的就是WindowManagerGlobal的addView方法,我們需要追蹤的ViewRootImpl例項化就是在這個方法中完成的,前面的原始碼顯示了這一點。
結論:這裡的關鍵mHandler使用的Looper確實是來自於主執行緒。
5、mHandler所用Looper執行緒問題分析狀態圖
上一節分析mHandler所用Looper所線上程問題,其實就是伴隨著啟動Activity並繪製整個View的過程,可以得到如下簡略流程圖:
通過這裡的dispatchAttachedToWindow方法,就將mHandler傳遞到了View.post()這個流程中,從而實現了從子執行緒中切換到主執行緒更新UI的功能。
6、總結
到這裡,使用View.post方法實現在子執行緒中更新UI的原始碼分析就結束了。我們可以看到,實際上底層還是通過Handler從子執行緒切換到主執行緒,來實現UI的更新,由此可見Handler在子執行緒與主執行緒切換上的重要地位。而整個分析流程其實主要是在做一件事,確定核心Handler使用的是主執行緒的Looper。這其中還穿插了ActivityThread、Activity啟動、WMS新增view、View的繪製流程等相關知識點,讀者可以根據自己掌握的情況選擇性地閱讀。當然,原始碼中有很多知識點是環環相扣的,各種知識點都需要平時多積累,希望讀者們遇到問題不要輕易放過,這就是一個打怪升級的過程。
由於筆者經驗和水平有限,如有描述不當或不準確的地方,請多多指教,謝