天天看點

深入了解View知識系列二- View底層工作原理以及View的繪制流程

一般我們都知道一個View到展示出來會經過onMeasure、onLayout、onDraw三個方法,但是在分析完了setContentView後發現這幾個方法都還沒有執行,這篇将會上一篇的基礎上繼續分析View的工作原理

深入了解View知識系列一- setContentView和LayoutInflater源碼原理分析

深入了解View知識系列二- View底層工作原理以及View的繪制流程

深入了解View知識系列三-Window機制、Canvas的由來、Android事件的由來

深入了解View知識系列四-View的測量規則以及三大方法流程

本篇你會學到什麼?

  • Activitiy是在哪裡開始準備顯示View的
  • View的三大方法是在什麼時候開始執行的,又是再哪裡被調用的
  • 我們經常使用的View.post後就可以擷取到View的寬高,為什麼呢?
  • View的requestLayout,invalidate請求重繪的原理
  • 子線程真的不能更新ui嗎?為什麼

上一篇回顧

我們帶着問題在上一篇的基礎上繼續分析,View的繪制流程及工作原理,在正式分析之前我們先回顧一下上一篇的内容.

setContentView

  • 我們知道了Activity的三個setContentView方法内部全部調用了getWindow.setContentView()
  • Activity的getWindow類型是PhoneWindow,而且也是Window的唯一實作類,PhoneWindow在Activity的attach方法中被初始化,并且設定了一些回調接口指向自己,例如Window的Callback接口,這個接口中有不少我們熟悉的方法,例如dispatchTouchEvent等。
  • 在PhoneWindow的setContentView方法中主要執行三個邏輯

1.判斷裝在我們設定布局的FrameLayout是否為空,如果為空調用installDecor方法,方法中首先會先執行generateDecor()建立DecorView,接着執行generateLayout()方法設定一些Window的樣式,根據樣式來選擇需要加載的布局,然後将布局添加到DecorView中,最後找到id為content的FrameLayout,也就是來裝我們設定布局的父View。

2.通過LayoutInflater将我們設定的布局添加到id為content的FrameLayout中

3.回調Callback接口的onContentChanged()方法,這個方法在Activity中是個空實作。

LayoutInflater

  • PhoneWindow中的LayoutInflater是在構造函數中被建立,建立的方式和我們平時使用一樣調用的是LayoutInflater.from(content);
  • LayoutInflater.from方法中就是封裝了context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  • LayoutInflater中一共有4個inflater方法,其中分為兩類,一類是傳入資源id的,一類是傳入XmlPullParser的,但是最終都會執行帶xml解析器的三參方法中,這裡注意第三個參數源碼中傳入的是root!=null,該參數代表了是否将加載後的View添加到父View中,如果使用了兩個參數的inflater,并且傳入的父view不為空,則預設會将加載後的布局添加到父view,并最後傳回的是父view。
  • inflater方法中首先會擷取到xml的跟節點,接着判斷該節點是否為merger标簽,如果是merger标簽則繼續判斷傳入的父view是否為空,或者第三個參數是否為flase,如果滿足一項則直接抛出異常,因為merger标簽隻是為了減少布局的嵌套才存在,他并不是View的子類,是以不能單獨存在,反之則會執行rInflate方法進行遞歸加載所有子View。
  • 如果不是merger标簽則會走到else方法中,先活着節點的名稱,根據名稱建立View,在建立View的邏輯中最終全部會調用createView方法,在這之前首先會判斷是否存在自定義的加載工廠,如果存在則調用加載工廠的建立View方法,接着通過名稱中是否包含 . 來判斷是否為Android自帶的控件,如果是Android自帶的控件第二個參數中傳入的值為android.view.,反之傳入null
  • 在createView方法中建立View的時候會執行如下邏輯

1.首先會判斷這個名稱的構造函數是否存在,如果存在則還需要驗證這個函數所屬的ClassLoader是否合法,如果不合法則會置空這個構造函數并清除緩存。

2.接着繼續判斷構造函數是否為空,如果為空則會通過一個三元運算來加載這個名稱的Class對象,這個三元運算主要是用來判斷是否需要拼接要加載Class的名字。

3.然後會判斷是否存在Filter過濾器,這個過濾器是用來判斷是否可以建立這個Class的View對象,通過檢視設定過濾器的RemoteViews和AppWeightHostView都是通過clazz.isAnnotationPresent(RemoteView.class)來判斷的,如果傳回為flase則執行抛出異常。

4.或者該Class的構造函數并存入HashMap中

5.在else邏輯中,主要是用來優化存在過濾器的情況,為了優化性能,系統隻會在第一次的時候去驗證是否允許建立這個View,并将結果存入HashMap中,下次隻會取map中結果進行判斷。

6.通過構造函數執行個體化這個View并傳回

  • 在建立了跟節點的View後,接着同樣會遞歸加載所有的子view,在加載的過程中會根據不同的标簽進行不同的操作,例如merger、include。如果是merger則直接抛出異常,因為merger隻能作為根節點。如果是include則會拿到layout屬性設定的布局檔案,如果未設定這個屬性執行抛出異常,接着也是根據标簽進行遞歸加載邏輯類似。
  • inflater最後根據傳入的父view和第三個參數的值進行判斷是傳回父view還是傳回剛加載的View

好了,回顧完了,在上一次講的setContentView過程中隻是初始化了PhoneWindow、DecorView并将我們設定的View入到了id為content的FrameLayout中,這時Activity的界面還是不可見的,因為View還沒有開始繪制的流程呢,那麼Activity中DecorView繪制的流程到底在哪裡開始的呢?其實是在ActivityThread中的handleResumeActivity()方法中,下面正式開始分析。

本篇源碼基于Android 7.1.1

1.邏輯開始點:ActivityThread中的handleResumeActivity()

這裡又提到了ActivityThread這個類,簡單說一下這個類,要知道所有的程式都是需要一個入口的,Android也不例外,ActivityThread就是Android應用的入口類,Activity、Service、ContentProvider幾乎都直接或者間接在這裡排程。現在暫時知道這麼多就好了,先不要糾結這個類,後續的會專門在四大元件系列去講。

源碼位置:/frameworks/base/core/java/android/app/ActivityThread.java

2. 上面的代碼中主要執行如下了三個邏輯。

  1. 首先調用執行了performResumeActivity方法,在這裡方法中會調用Activity的onResume方法
  2. 擷取Activity的PhoneWindow,接着拿到PhoneWindow中的DecorView并設定為隐藏,擷取Activity的中WindowManger并執行addView方法将DecorView添加到Window中。
  3. 執行Activity的makeVisible方法展示DecorView,方法中就是調用了View.VISIBLE

而且我們通過上面的可以總結兩個問題。

  1. 其實在Activity的onResume方法執行了以後才開始将DecorView添加到Window中,換句話說,也就是在onResume方法中是不能直接擷取View的寬度等參數的,因為這個時候連DecorView都沒有添加到Window中呢,是以這時也還沒有執行View的三大流程呢,又如何産生這是東西。
  2. DecorView最終也是通過WindowManager來添加的,或者說Activity可以顯示内容其實也是通過WindowManager添加View來實作的,其實Android中所有設計View展示的最終全部都是通過WindowManager的addView來添加的,例如PopupWindow、Dialog、Toast等,下一篇會詳細分析

3.在上面的代碼中,使用了ViewManager執行了addView方法,在上一篇我們簡單的說過了,WindowManger是繼承自ViewManger的,而且WindowManger中的addView、updateViewLayout、removeView全部都是ViewManger中的方法,下面先簡單看一下這兩個類的聲明

4.我們可以看到WindowManager也是一個接口,它的具體實作是WindowMangerImpl,那麼我們繼續上面的邏輯去看一下它的addView方法,順便看一下ViewManger中的其餘兩個方法

源碼位置:/frameworks/base/core/java/android/view/WindowManagerImpl.java

5.可以看到WindowManagerImpl中三個方法的實作全部都橋接到了WindowManagerGlobal中,并且這個類是單例的。我們隻分析addView方法,其他方法原理相通

源碼位置:/frameworks/base/core/java/android/view/WindowManagerGlobal.java

6. 在WindowManagerGlobal中的addView方法中主要有三個邏輯如下,并且我們通過上面的代碼可以看出來WindowManager每次在添加View的時候都需要建立一個ViewRootImpl,或者說每個Window中都會對應一個ViewRootImple。

  1. 檢查參數的合法性
  2. 建立ViewRootImpl,并将要添加的view、ViewRootImpl和LayoutParams緩存起來
  3. 最後調用了ViewRootImpl的setView方法後邏輯跳轉到ViewRootImpl中。

源碼位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

7. ViewRootImpl的setView方法相等的長,最重要的是執行了上面的兩個方法,一個是requestLayout方法,這個方法中最終開始了繪制的流程,但是并不是直接開始的,一會我們再分析。還有一個就是通過mWindowSession的addToDisplay最終在WindowManagerService中完成Window的添加過程,至于到首先我們先來看一下requestLayout這個方法,WindowSession的addToDisplay邏輯和原理我們會在下次講解Window時在去講,還有就是要記住這裡調用了view.assignParent(this),下面分析View的requestLayout原理時會說

源碼位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

8.上面的代碼中繪制的入口跳轉到了scheduleTraversals中,在看這個方法前我們先來看一下checkThread這個方法

源碼位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

9.這裡看到了在開始繪制前首先會檢查線程,而且上面抛出的異常再熟悉不過了,子線程更新ui了,這裡就說到了最開始說的子線程真的不能更新ui嗎?答案是可以的,但是有一個前提條件就是必須是在ViewRootImpl沒有建立之前,簡單說可以在Activitiy的onCreate裡邊開一個線程去更新ui是不會報錯的,但是這個線程不能太耗時,因為Activity的繪制入口是在onResume以後開始的,但是也不能太耗時,畢竟我們的Activitiy隻是一個回調方法而已,當ViewRootImpl被建立後就會抛出異常了。下面我們就去看一下scheduleTraversals的方法吧

源碼位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

10. scheduleTraversals中開啟個消息屏障,目的是為了使View更快速的布局和繪制完成。然後執行mChoreographer的postCallback,Choreographer這個類是用來異步更新ui的,裡面也是使用了Handler,也就是在這裡真正的開始了繪制的流程,這個類還涉及了Android中的VSYNC機制,感興趣可以自己查閱。接着我們來看一下這個Runnable

源碼位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

11. 終于到了真正的繪制繪制起點了,這個方法相等的長,有800行代碼,我們隻截取部分重要的代碼

源碼位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

13.好了,到了這裡就開始了View真正的繪制流程了,至于具體的每個流程會在下幾次分别的詳情的去分析,這一次我們隻是為了弄清View的工作原理和整體的流程,我們來看一下getRunQueue().executeActions這個方法有什麼用?,在說這個方法之前我們要先說一下平時我們經常使用View.post來擷取View的寬高資訊,但是為什麼這樣就可以擷取到呢,我們去看一下View.post中的源碼。

源碼位置:/frameworks/base/core/java/android/view/View.java

14.這個AttachInfo是在哪裡被指派呢?我們再回去看一下ViewRootImpl中performTraversals的一行代碼host.dispatchAttachedToWindow(mAttachInfo, 0),host就是一個頂層的View,但是這個View,而這個AttachInfo則是在ViewRootImpl中傳過去的,我們去View中看一下

源碼位置:/frameworks/base/core/java/android/view/View.java

15.好了接着上邊的View.post說,這個方法是在View被添加到視窗後,馬上繪制前被回調的方法,那麼也就是說我們的View在被添加到視窗之前這個mAttachInfo就是null,那麼就會走到 ViewRootImpl.getRunQueue().post(action)這個代碼,我們去VIewRootImpl中去看一下

源碼位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

16.看上面的代碼可以看出最終是調用了ViewRootImpl類中的RunQueue.post方法,接着會将傳過來的Runnable封裝成一個HandlerAction,然後存入ArrayList中,那麼我們現在再來看一下ViewRootImpl中performTraversals方法裡的一句代碼, getRunQueue().executeActions(mAttachInfo.mHandler),上面我們已經知道了getRunQueue就是傳回了RunQueue,我們看看RunQueue的executeActions方法

源碼位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

17.到這裡是不是豁然開朗了呢?還不明白?好吧,其實就是在View還沒有View完成的時候是不會執行View.post過來的Runnable的,隻是将他添加到了一個集合中,然後當繪制的時候會執行executeActions這句話周遊所有的消息然後将這些消息添加到隊列裡依次的執行,真正執行被post過來的Runnable時View已經執行完了繪制流程,是以我們可以通過View.post擷取到View的寬高屬性。

18.完了我們再想一個問題,我們經常會使用View.requestLayout來重繪界面,那麼它到底是怎麼回事呢,我們來看一下View中的這個方法

源碼位置:/frameworks/base/core/java/android/view/View.java

18.mParent是ViewParent類型,而且我們知道了被指派的方法是assignParent,那麼這個方法是怎麼調用的呢?首先我們來回顧一下之前分析的東西,在上面的ViewRootImpl中的setView方法中有這個方法的調用view.assignParent(this),那麼這個時候這個View是視圖的最頂層View,也就是DecorView,它的Parent是ViewRootImpl,那麼DecorView是一個FrameLayout的ViewGroup,它又會在添加View的時候給所屬子View指派,接着子View如果還是ViewGroup則是一樣的邏輯,直到所有的View添加完畢,那麼這個時候所有的View也就存在了Parent的值,還記得我們分析的LayoutInflater的過程嗎,最終也是通過root.addView來完成添加的,是以說所有的子View都會存在它的父Parent。

源碼位置:/frameworks/base/core/java/android/view/ViewGroup.java

19.那麼到這裡我們就知道了所有的View都會存在它自己的Parent,而最頂層的View的Parent是ViewRootImpl,那麼也就是說View在調用requestLayout的會層層向上調用,直到最頂層的ViewRootImpl,也就是我們之前分析完了的ViewRootImpl,在這裡會開始View的繪制流程,我們上面已經分析過了,至于invalidate的原理其實和requestLayout的原理是一樣的,最終都會執行到ViewRootImpl中,這裡由于篇幅的緣故就不再貼代碼了,還有就是這一篇我們還有一個分支沒有分析就是WMS添加window的過程,會在下一篇去講。

總結:

繼續閱讀