天天看點

【朝花夕拾】Android多線程之(一)View.post()篇

提起View.post(),相信不少童鞋一點都不陌生,它用得最多的有兩個功能,使用簡便而且實用:1)在子線程中更新UI。從子線程中切換到主線程更新UI,不需要額外new一個Handler執行個體來實作。2)擷取View的寬高等屬性值。在Activity的onCreate()、onStart()、onResume()等方法中調用View.getWidth()等方法時會傳回0,而通過post方法卻可以解決這個問題。本文将由從源碼角度來分析其中的原理。

前言

       轉載請聲明,轉自【https://www.cnblogs.com/andy-songwei/p/12021867.html】,謝謝!

       提起View.post(),相信不少童鞋一點都不陌生,它用得最多的有兩個功能,使用簡便而且實用:

       1)在子線程中更新UI。從子線程中切換到主線程更新UI,不需要額外new一個Handler執行個體來實作。

       2)擷取View的寬高等屬性值。在Activity的onCreate()、onStart()、onResume()等方法中調用View.getWidth()等方法時會傳回0,而通過post方法卻可以解決這個問題。

       本文将由從源碼角度分析其原理,在閱讀文本之前,希望讀者是對Handler的Looper問題有一定了解的,如果不了解請先閱讀【朝花夕拾】Handler篇。源碼基本基于API-28(即Android 9),隻有ActivityThread中Activity啟動到調用handleResumeActivity()這一小部分是參考的API-26(即Android 8.0,API-28上這部分代碼邏輯有些變化,還沒來得及詳細研究,但基本機制應該不會變)。

       本文的主要内容如下:

【朝花夕拾】Android多線程之(一)View.post()篇

一、在子線程中更新UI

  1、在子線程中更新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,使用非常廣)。

       全局搜尋後的結果如下:

【朝花夕拾】Android多線程之(一)View.post()篇

對于這個結果,我們可以首先排除“Boot-image-profile.txt”和“RecyclerView.java”兩個檔案(原因不需多說吧...如果真的不知道,那就說明還完全沒有到閱讀這篇文章的時候),跟這個方法調用相關的類就縮小到View,ViewGroup和ViewRootImpl類中。在View.java中與該方法相關的隻有如下兩處,顯然可以排除掉View.java。

【朝花夕拾】Android多線程之(一)View.post()篇

 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類中的調用。

【朝花夕拾】Android多線程之(一)View.post()篇
【朝花夕拾】Android多線程之(一)View.post()篇
【朝花夕拾】Android多線程之(一)View.post()篇

 在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()函數:

//==========ActivityThread===========
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類之間的關系了,如下所示:

【朝花夕拾】Android多線程之(一)View.post()篇

這裡用到了系統源碼中常用的一種設計模式——橋接模式,調用WindowManagerImpl中的方法時,實際上是由WindowManagerGlobal對應方法來實作的。是以第25行實際執行的就是WindowManagerGlobal的addView方法,我們需要追蹤的ViewRootImpl執行個體化就是在這個方法中完成的,前面的源碼顯示了這一點。

       結論:這裡的關鍵mHandler使用的Looper确實是來自于主線程。

  5、mHandler所用Looper線程問題分析狀态圖

       上一節分析mHandler所用Looper所線上程問題,其實就是伴随着啟動Activity并繪制整個View的過程,可以得到如下簡略流程圖:

【朝花夕拾】Android多線程之(一)View.post()篇

圖1.5.1 View繪制狀态圖

通過這裡的dispatchAttachedToWindow方法,就将mHandler傳遞到了View.post()這個流程中,進而實作了從子線程中切換到主線程更新UI的功能。

  6、小結

       到這裡,使用View.post方法實作在子線程中更新UI的源碼分析就結束了。我們可以看到,實際上底層還是通過Handler從子線程切換到主線程,來實作UI的更新,而整個分析流程其實主要是在做一件事,确定核心Handler使用的是主線程的Looper。

二、使用View.post()擷取View的寬高

       看到這個标題的時候,您可能會很納悶,平時工作中不是可以直接通過view.getWidth()(getHeight也一樣,後面不贅述)就能擷取view的寬高嗎,通過View.post()來實作豈不是多此一舉?

  1、通過post擷取寬高的示例示範

       對于上述疑惑,咱們先來一個例子:

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     Log.d(TAG, "width-1=" + mStartBtn.getWidth());
 8     mStartBtn.post(new Runnable() {  //Runnable ①
 9         @Override
10         public void run() {
11             Log.d(TAG, "width-3=" + mStartBtn.getWidth());
12         }
13     });
14     mStartBtn.setOnClickListener(new View.OnClickListener() {
15         @Override
16         public void onClick(View v) {
17             Log.d(TAG, "width-4=" + mStartBtn.getWidth());
18         }
19     });
20 }
21 @Override
22 protected void onResume() {
23     super.onResume();
24     Log.d(TAG, "width-2=" + mStartBtn.getWidth());
25 }      

運作後得到如下log:

1 12-10 16:16:49.059 18918-18918/com.example.demos D/postDemo: width-1=0
2 12-10 16:16:49.065 18918-18918/com.example.demos D/postDemo: width-2=0
3 12-10 16:16:49.104 18918-18918/com.example.demos D/postDemo: width-3=264
4 12-10 16:16:53.074 18918-18918/com.example.demos D/postDemo: width-4=264      

 看到這份log,對于部分童鞋來說,是不是無法淡定了?第7行和第24行為什麼得到的值是0呢?後面我們會詳細分析原因,這裡咱們先知道會有這個現象,這就是為什麼要使用view.post()來擷取view寬高的原因了。第8~13行示範了該方法的使用示例,使用起來也是很簡單的。

  2、view繪制的時機問題

       通過上面的示例,我們可以看到在onCreate()和onResume()中直接調用view.getWidth()傳回值都是0,但是到了view.post()中以及按鈕的onClick()事件中卻可以得到正确值。通過這兩組值可以推斷,view是還沒有繪制的,也還沒有attach到Window中。前面也分析過post源碼,這個post()裡面的Runnable是推遲到View attach到Window後才會執行,是以就能得到準确值了。onClick的時機就更晚了,自然也能夠得到正常的值。

       是以,出現上述log中的現象,是和view的繪制時機有着密切聯系的。下面重點分析Activity啟動以及View的繪制時機源碼。

1 //=======ActivityThread.java=========
 2 final void handleResumeActivity(...) {
 3     ......
 4     //這裡會執行onStart/onRestart、onResume方法
 5     r = performResumeActivity(token, clearHide, reason);
 6     ......
 7      //開始添加并繪制view
 8      ViewManager wm = a.getWindowManager();
 9      wm.addView(decor, l);
10      ......
11 }      

       第5行看字面意思大概也能猜出是和Activity的onResume生命周期方法有關系,實際上這裡面會執行onStart/onRestart、onResume這些生命周期方法,下面會詳細分析。第8、9行前面我們講過它的作用,添加DecorView并開始整個View體系的繪制。我們繼續看看第5行具體做了什麼:

1 //========ActivityThread.java=========
 2 public final ActivityClientRecord performResumeActivity(...){
 3      ......
 4      r.activity.performResume();
 5      ......
 6 }
 7 
 8 //=========Activity.java========
 9 final void performResume() {
10     performRestart();
11     ......
12     mInstrumentation.callActivityOnResume(this);
13     ......
14 }
15 
16 final void performRestart() {
17     ......
18     mInstrumentation.callActivityOnRestart(this);
19     ......
20     performStart();
21     ......
22 }
23 
24 final void performStart() {
25     ......
26     mInstrumentation.callActivityOnStart(this);
27     ......
28 }      

r.activity是一個Activity執行個體,在Launch Activity時建立。這一塊代碼比較容易了解,實際上就是依次執行了Instrumentation執行個體的callActivityOnStart \ callActivityOnRestart 、callActivityOnResume方法。

1 //==========Instrumentation.java========
 2 public void callActivityOnStart(Activity activity) {
 3     activity.onStart();
 4 }
 5 ......
 6 public void callActivityOnRestart(Activity activity) {
 7     activity.onRestart();
 8 }
 9 ......
10 public void callActivityOnResume(Activity activity) {
11     activity.onResume();
12     ......
13 }      

這三個方法最終調用的就是對應的如下方法:

1 //===========Activity.java========
 2 protected void onStart() {
 3     ......
 4 }
 5 
 6 protected void onReStart() {
 7     ......
 8 }
 9 
10 protected void onResume() {
11     ......
12 }      

這就很熟悉了,我們自定義Activity時需要重寫的生命周期方法(我們知道onStart 和onRestart方法隻會執行一個,這裡沒有具體分析什麼條件下執行哪一個,這不是本文的重點)都是重寫的這些方法。整個流程大緻可以表示為如下的狀态圖:

【朝花夕拾】Android多線程之(一)View.post()篇

 現在就很明白了,雖然我們平時是在onCreate()方法中調用setView()來加載xml中的布局,但真正将整個view樹添加到Window中是在onResume之後的,添加整個view的過程包含了view的繪制流程。這樣我們就明白了第一點中,為什麼在onCreate()和onResume()方法中直接調用getWidth()得到的值為0了。

  3、dispatchAttachedToWindow()發生在view繪制前,如何能擷取真實的寬高?

       前面在研究dispatchAttachedToWindow()方法執行時機的時候,我們已經明确了它是在ViewRootImpl類中的performTraversals()方法中調用的。但是細心的童鞋會發現如下的情況:

1 //=========ViewRootImpl========代碼2.3.1
 2 private void performTraversals() {
 3        ......
 4        host.dispatchAttachedToWindow(mAttachInfo, 0);
 5        ......    
 6        performMeasure(...);
 7        ......
 8        performLayout(...);
 9        ......
10        performDraw();
11        ......
12 }      

dispatchAttachedToWindow()方法居然發生在View的繪制流程執行之前!不知道這有沒有驚到您的下巴呢?我們前面在講post源碼時就講過,如果view還沒有attach到Window,post裡面的Runnable會推遲到dispatchAttachedToWindow()方法執行時才會再執行。既然view都還沒有繪制,那Runnable中又如何能夠擷取到真實的寬高呢?

       這裡先看一下performTraversals()方法的調用流程:

1 //========ViewRootImpl=========代碼2.3.2
 2 Choreographer mChoreographer;
 3 
 4 public ViewRootImpl(...){
 5     ......
 6     mChoreographer = Choreographer.getInstance();
 7     ......
 8 }
 9 
10 void scheduleTraversals() {
11     ......
12     mChoreographer.postCallback(
13                 Choreographer.CALLBACK_TRAVERSAL, 
14                 mTraversalRunnable, null);
15     ...... 
16 }
17 
18 final class TraversalRunnable implements Runnable {  //Runnable ②
19     @Override
20     public void run() {
21         doTraversal();
22     }
23 }
24 
25 final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
26 
27 void doTraversal() {
28     ...... 
29     performTraversals();
30     ......
31 }      

在前文圖1.5.1 View繪制狀态圖中顯示了從activity啟動到performTraversals()方法執行的大緻流程,部分流程的對應代碼如上。進入第12行看看其執行邏輯:

1 //=========Choreographer=======代碼2.3.3
 2 private final FrameHandler mHandler;
 3 
 4 private Choreographer(Looper looper, int vsyncSource) {
 5     mLooper = looper;
 6     mHandler = new FrameHandler(looper);
 7     ......
 8 }
 9 
10 private final class FrameHandler extends Handler {
11     public FrameHandler(Looper looper) {
12         super(looper);
13     }
14 
15     @Override
16     public void handleMessage(Message msg) {
17         ......
18     }
19 }
20 
21 public void postCallback(int callbackType, Runnable action, Object token) {
22     postCallbackDelayed(callbackType, action, token, 0);
23 }
24 
25 public void postCallbackDelayed(int callbackType,
26         Runnable action, Object token, long delayMillis) {
27    ......
28     postCallbackDelayedInternal(callbackType, action, token, delayMillis);
29 }
30 
31 private void postCallbackDelayedInternal(int callbackType,
32         Object action, Object token, long delayMillis) {
33     ......
34     Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
35     ......
36     mHandler.sendMessageAtTime(msg, dueTime);
37 }      

可見代碼2.3.2處實際就是封裝的一個Handler來執行一個Runnable任務的,而且從代碼2.3.3來看,這個封裝的FrameHandler mHandler所使用的looper來自于第4行的構造函數。那麼這裡就需要分析本次使用的Choreographer執行個體傳入的looper了。

       代碼2.3.2中第6行,繼續追蹤:

1 //==========Choreographer=========代碼2.3.4
 2 
 3 // Thread local storage for the choreographer.
 4 private static final ThreadLocal<Choreographer> sThreadInstance =
 5         new ThreadLocal<Choreographer>() {
 6     @Override
 7     protected Choreographer initialValue() {
 8         Looper looper = Looper.myLooper();
 9         if (looper == null) {
10             throw new IllegalStateException("The current thread must have a looper!")
11         }
12         Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
13         if (looper == Looper.getMainLooper()) {
14             mMainInstance = choreographer;
15         }
16         return choreographer;
17     }
18 };
19 
20 /**
21  * Gets the choreographer for the calling thread.  Must be called from
22  * a thread that already has a {@link android.os.Looper} associated with it.
23  *
24  * @return The choreographer for this thread.
25  * @throws IllegalStateException if the thread does not have a looper.
26  */
27 public static Choreographer getInstance() {
28     return sThreadInstance.get();
29 }      

這裡用到了ThreadLocal,本文不展開講它的實作機制,隻需要知道本段代碼中,它會為每一個線程存儲一個對應線程下的Choreographer執行個體。本次調用Choreographer.getInstance()是在主線程中(前面第一節中已經分析過ViewRootImpl是在主線程中執行個體化的)完成的,是以此次擷取的是主線程下的Choreographer執行個體,在代碼2.3.4中傳入的looper就是主線程的looper,是以代碼2.3.3中的Handler使用的就是主線程的looper。

       分析到這裡,就明白了,整個performTraversals()方法,是作為Runnable②的一部分被封裝成Message,被加入主線程的MessageQueue中的。當執行到代碼2.3.1第3行dispatchAttachedToWindow()時,它會再向主線程的MessageQueue中添加一個封裝了本節開頭執行個體中Runnable①的Message。由于Looper.loop()取MessageQueue中的Message執行時是有順序的,是以Runnable②會先執行完畢,然後才會執行Runnable①,也就是說實際執行中,Runnable①中的view.getWidth()是發生在performMeasure() — performLayout() — performDraw()之後的。

【朝花夕拾】Android多線程之(一)View.post()篇

       此時,疑惑就解開了,這裡我們總結一下本小結的結論:performTraversals()方法中,dispatchAttachedToWindow()所産生的Runnable①,是在view繪制流程結束後才執行的。

  4、小結

       其實本節主要就是要搞明白一個難點,dispatchAttachedToWindow執行Runnable與View的繪制執行的時間順序問題,最後落腳點還是到了Handler上面。

結語

       本文大部分的篇幅都是在分析Handler問題,由此可見Handler在整個流程中的重要地位。整個分析過程還穿插了ActivityThread、Activity啟動、WMS添加view、View的繪制流程等相關知識點,讀者可以根據自己掌握的情況選擇性地閱讀。當然,源碼中有很多知識點是環環相扣的,各種知識點都需要平時多積累,希望讀者們遇到問題不要輕易放過,這就是一個打怪更新的過程。

       【朝花夕拾】Handler篇

       【工利其器】必會工具之(一)Source Insight篇

       【朝花夕拾】Android自定義View篇之(一)View繪制流程

        【安卓本卓】Android系統源碼篇之(一)源碼擷取、源碼目錄結構及源碼閱讀工具簡介

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

繼續閱讀