提起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上這部分代碼邏輯有些變化,還沒來得及詳細研究,但基本機制應該不會變)。
本文的主要内容如下:
一、在子線程中更新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,使用非常廣)。
全局搜尋後的結果如下:
對于這個結果,我們可以首先排除“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()函數:
//==========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類之間的關系了,如下所示:
這裡用到了系統源碼中常用的一種設計模式——橋接模式,調用WindowManagerImpl中的方法時,實際上是由WindowManagerGlobal對應方法來實作的。是以第25行實際執行的就是WindowManagerGlobal的addView方法,我們需要追蹤的ViewRootImpl執行個體化就是在這個方法中完成的,前面的源碼顯示了這一點。
結論:這裡的關鍵mHandler使用的Looper确實是來自于主線程。
5、mHandler所用Looper線程問題分析狀态圖
上一節分析mHandler所用Looper所線上程問題,其實就是伴随着啟動Activity并繪制整個View的過程,可以得到如下簡略流程圖:
圖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方法隻會執行一個,這裡沒有具體分析什麼條件下執行哪一個,這不是本文的重點)都是重寫的這些方法。整個流程大緻可以表示為如下的狀态圖:
現在就很明白了,雖然我們平時是在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()之後的。
此時,疑惑就解開了,這裡我們總結一下本小結的結論:performTraversals()方法中,dispatchAttachedToWindow()所産生的Runnable①,是在view繪制流程結束後才執行的。
4、小結
其實本節主要就是要搞明白一個難點,dispatchAttachedToWindow執行Runnable與View的繪制執行的時間順序問題,最後落腳點還是到了Handler上面。
結語
本文大部分的篇幅都是在分析Handler問題,由此可見Handler在整個流程中的重要地位。整個分析過程還穿插了ActivityThread、Activity啟動、WMS添加view、View的繪制流程等相關知識點,讀者可以根據自己掌握的情況選擇性地閱讀。當然,源碼中有很多知識點是環環相扣的,各種知識點都需要平時多積累,希望讀者們遇到問題不要輕易放過,這就是一個打怪更新的過程。
【朝花夕拾】Handler篇
【工利其器】必會工具之(一)Source Insight篇
【朝花夕拾】Android自定義View篇之(一)View繪制流程
【安卓本卓】Android系統源碼篇之(一)源碼擷取、源碼目錄結構及源碼閱讀工具簡介
由于筆者經驗和水準有限,如有描述不當或不準确的地方,請多多指教,謝謝!