1. 程式人生 > >Android多執行緒之(一)View.post()原始碼分析——在子執行緒中更新UI

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的繪製流程等相關知識點,讀者可以根據自己掌握的情況選擇性地閱讀。當然,原始碼中有很多知識點是環環相扣的,各種知識點都需要平時多積累,希望讀者們遇到問題不要輕易放過,這就是一個打怪升級的過程。

       由於筆者經驗和水平有限,如有描述不當或不準確的地方,請多多指教,謝