Android中View的量算、佈局及繪圖機制

孫群發表於2015-10-17

為了研究Android中View的佈局及繪圖機制,我建立了一個非常簡單的App,該App只有一個Activity,該Activity對應的layout如下所示:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView android:text="@string/hello_world" android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

該佈局檔案很簡單,RelativeLayout下面就一個TextView。

我們啟動App後,通過Hierarchy Viewer檢視App中的佈局層級,如下所示:
這裡寫圖片描述

從上圖我們可以看出,App的根結點是PhoneWindow$DecorView,此處的$表示DecorView是PhoneWindow下面的內部類例項。PhoneWindow$DecorView下面有三個child,分別是LinearLayout例項、View@49da043和View@44ff410。View@49da043表示的是navigationBarBackground,View@44ff410表示的是statusBarBackground。LinearLayout下面有兩個child,分別是ViewStub例項和FrameLayout例項,其中ViewStub不需要繪製,所以我們在下面的討論中可以直接對其忽略。FrameLayout下有一個child,RelativeLayout例項,該RelativeLayout例項對應的就是佈局檔案activity_main.xml中的RelativeLayout,RelativeLayout下有一個child,即TextView。

以上提到的控制元件都是View的例項,有的則是ViewGroup的例項,ViewGroup繼承自View,PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都直接或間接繼承自ViewGroup,只有ViewGroup例項才能有子節點。

當我們在onCreate()方法中呼叫setContentView(R.layout.activity_main)方法後,Android會從layout的樹形結構中自上而下開始對所有的View進行量算、佈局、繪圖,具體來說經過以下過程:

  1. Android自上而下對所有View進行量算,這樣Android就知道了每個View想要的尺寸大小,即寬高資訊

  2. 在完成了對所有View的量算工作後,Android會自上而下對所有View進行佈局,Android就知道了每個View在其父控制元件中的位置,即View到其父控制元件四邊的left、right、top、bottom

  3. 在完成了對所有View的佈局工作後,Android會自上而下對所有View進行繪圖,這樣Android就將所有的View渲染到螢幕上了

以下是涉及到的相關類的原始碼:
View原始碼
ViewGroup原始碼
ViewRootImpl原始碼
PhoneWindow$DecorView原始碼
LinearLayout原始碼
FrameLayout原始碼
RelativeLayout原始碼
TextView原始碼


量算

關於Measure:

  1. View用measure()方法進行量算,量算的目的是View讓其父節點知道它想要多大的尺寸,所以說量算是後面對View進行佈局以及繪圖的基礎。

  2. View的measure()方法中會執行onMeasure()方法,View類本身的onMeasure()方法不是空方法,其將量算完的結果儲存到View中。View的子類不應該重寫measure()方法,如果需要的話應該重寫onMeasure()方法,ViewGroup的子類都應該重寫onMeasure()方法,比如PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都重寫了onMeasure()方法,這些類都在onMeasure()方法中遍歷child,並呼叫child的measure()方法,對child進行量算,縱向遞迴進行,從而實現自上而下對View樹進行量算,直至完成對葉子節點View的量算。

  3. 量算的起點是ViewRootImpl類,ViewRootImpl是根View,即View樹上面的根結點,嚴格來說ViewRootImpl不屬於View,其實現了ViewParent介面, 其下才是PhoneWindow$DecorView

  4. Android在對View樹進行自上而下的量算時,採用的是深度優先演算法,而非廣度優先演算法,即遍歷到某個View時,Android會首先沿著該View一直縱向遍歷並量算到處於葉子節點的View,只有對該View及其所有子孫View(如果存在子孫View的話)完成量算後,才會量算該View的兄弟節點View。

以下是Android對所有View自上而下量算的呼叫過程:

這裡寫圖片描述

  • 由上我們可以看出,首先ViewRootImpl執行了doTraversal()和performTraversals() 方法,然後執行ViewRootImpl的performMeasure()方法,該方法是Android對所有View進行量算的起點。在該方法中會從ViewRootImpl開始自上而上對View樹進行遍歷,首先ViewRootImpl對PhoneWindow$DecorView進行量算,在執行到PhoneWindow$DecorView的onMeasure()方法時,其遍歷所有的child,對依次它們進行量算,首先對呼叫LinearLayout的measure()方法,對第一個子節點LinearLayout進行量算。

  • LinearLayout在measure()方法中會呼叫onMeasure()方法,在該方法中LinearLayout呼叫了measureVertical()方法,該方法會遍歷其child並對其進行量算,由於其子節點ViewStub不用於渲染,所以此處不對其量算,對其忽略,對另一個child FrameLayout進行量算,呼叫FrameLayout的measure()方法。

  • FrameLayout在執行measure()方法時會執行onMeasure()方法,在該方法中會遍歷所有的child,並對它們進行量算。其下只有一個child,即RelativeLayout,呼叫RelativeLayout的measure()方法,對其進行量算。

  • RelativeLayout在measure()方法中會執行onMeasure()方法,在該方法中會遍歷所有的child,並對它們進行量算。其下只有一個child,即TextView,呼叫TextView的measure()方法對其進行量算,在其中會執行onMeasure()方法。

  • 以上完成了對View樹中LinearLayout及其所有子算View的量算工作,之後會對PhoneWindow$DecorView中的另外兩個View進行量算,這也體現了Android採用深度優先演算法對View樹進行遍歷量算的過程。View@49da0d3和View@44ff410會依次執行measure()方法和onMeasure()方法。

這樣整個View樹自上而下的量算過程就結束了,經過量算Android知道了各個View想要渲染的尺寸大小,即寬度和高度資訊。

關於量算中measure()和onMeasure()方法的一些細節可參見博文《 原始碼解析Android中View的measure量算過程》


佈局

關於Layout:

  1. 佈局的前提是已經對View進行了量算,View通過呼叫layout()方法進行佈局,佈局的目的是讓Android知道View在其父控制元件中的位置,即距父控制元件四邊的距離left、right、top、bottom。佈局是繪圖的基礎,只有完成了佈局,才能對View進行繪圖。

  2. View的layout()方法中會執行onLayout()方法,View類本身的onLayout()是空方法。View的子類不應該重寫layout()方法,如果需要的話應該重寫其onLayout()方法,ViewGroup的子類都應該重寫onLayout()方法,比如PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都重寫了onLayout()方法,這些類都在onLayout()方法中遍歷child,並呼叫child的layout()方法,對child進行佈局,縱向遞迴進行,從而實現自上而下對View樹進行佈局,直至完成對葉子節點View的佈局。

  3. 佈局的起點也是ViewRootImpl類,ViewRootImpl是根View,即View樹上面的根結點,嚴格來說ViewRootImpl不屬於View,其實現了ViewParent介面, 其下才是PhoneWindow$DecorView

  4. Android在對View樹進行自上而下的佈局時,採用的是深度優先演算法,而非廣度優先演算法,即遍歷到某個View時,Android會首先沿著該View一直縱向遍歷並佈局到處於葉子節點的View,只有對該View及其所有子孫View(如果存在子孫View的話)完成佈局後,才會佈局該View的兄弟節點View。

Android中的佈局過程與之前上面提到的量算過程很類似,以下是Android對所有View自上而下佈局的呼叫過程:

這裡寫圖片描述

  • 由上我們可以看出,首先ViewRootImpl執行了doTraversal()和performTraversals() 方法,然後執行ViewRootImpl的performLayout()方法,該方法是Android對所有View進行佈局的起點。在該方法中會從ViewRootImpl開始自上而下對View樹進行遍歷,首先ViewRootImpl執行PhoneWindow$DecorView的layout()方法,對其進行佈局。

  • PhoneWindow$DecorView在其layout()方法中會執行onLayout()方法,PhoneWindow$DecorView會在onLayout()方法中遍歷其所有的child,並依次呼叫child的layout()方法,實現對child的佈局。首先呼叫其第一個child LinearLayout的layout()方法。

  • LinearLayout在layout()方法中會執行onLayout()方法,在該方法中會呼叫layoutVertical()方法,該方法會遍歷其所有的child並依次呼叫child的layout()方法進行佈局。由於其子節點ViewStub不用於渲染,所以此處不對其進行佈局,對其忽略,對另一個child FrameLayout進行佈局,呼叫FrameLayout的layout()方法。

  • FrameLayout在layout()方法中會執行onLayout()方法,在該方法中會呼叫layoutChildren()方法,該方法會遍歷其所有的child並依次呼叫child的layout()方法進行佈局。其下只有一個child,即RelativeLayout,執行RelativeLayout的layout()方法,對其進行佈局。

  • RelativeLayout在layout()方法中會執行onLayout()方法,在該方法中會遍歷所有的child並依次呼叫child的layout()方法進行佈局。其下只有一個child,即TextView,呼叫TextView的layout()方法對其進行佈局,在其中會執行onLayout()方法。

  • 以上完成了對View樹中LinearLayout及其所有子孫View的佈局工作,之後會對PhoneWindow$DecorView中的另外兩個View進行佈局,這也體現了Android採用深度優先演算法對View樹進行遍歷佈局的過程。View@49da043和View@44ff410會依次執行layout()方法和onLayout()方法。

這樣整個View樹自上而下的佈局過程就結束了,經過佈局Android知道了各個View在其父控制元件中的位置。

關於佈局layout的細節可參見博文《原始碼解析Android中View的layout佈局過程》


繪圖

關於Draw:

  1. 繪圖的前提是已經對View進行了量算和佈局,View通過呼叫draw()方法進行繪圖,繪圖的目的就是讓View在UI介面上呈現出來。

  2. View的draw()方法中會依次onDraw()和dispatchDraw()方法,View類本身的onDraw()和dispatchDraw()方法都是空方法。View的子類不應該重寫draw()方法,如果需要的話應該按具體情況選擇重寫onDraw()方法或dispatchDraw()方法,具體來說:

    • 當我們需要自定義一個View(而非ViewGroup)時,我們需要重寫View的onDraw()方法以實現對自定義View的繪製,即onDraw()用於繪製View自身UI。
    • Android中的ViewGroup類重寫了View中的dispatchDraw()方法,ViewGroup.dispatchDraw()方法會遍歷其所有的child,並依次呼叫child的draw()方法,即dispatchDraw()用於繪製ViewGroup的所有子孫View的UI,這是與onDraw()不同的。由於ViewGroup已經具體實現了dispatchDraw()方法,所以大部分情況下ViewGroup的子類無需再對其進行重寫,例如PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都沒有重寫dispatchDraw()方法。只有在極少數情況下,為了實現某些特殊需求,我們才有可能重寫ViewGroup的dispatchDraw()方法,但是即便重寫該方法我們也應該在我們的實現中呼叫super.dispatchDraw()方法以便實現對子孫View進行繪製。
  3. 繪圖的起點也是ViewRootImpl類,ViewRootImpl是根View,即View樹上面的根結點,嚴格來說ViewRootImpl不屬於View,其實現了ViewParent介面, 其下才是PhoneWindow$DecorView。

  4. Android在對View樹進行自上而下的繪圖時,採用的也是深度優先演算法,而非廣度優先演算法,即遍歷到某個View時,Android會首先沿著該View一直縱向遍歷並繪圖到處於葉子節點的View,只有對該View及其所有子孫View(如果存在子孫View的話)完成繪圖後,才會渲染該View的兄弟節點View。

Android中的繪圖過程與之前上面提到的量算、佈局過程類似,以下是Android對所有View進行自上而下繪圖的呼叫過程:

這裡寫圖片描述

  • 由上我們可以看出,首先ViewRootImpl執行了doTraversal()和performTraversals() 方法,然後執行ViewRootImpl的performDraw()方法,該方法是Android對所有View進行繪圖的起點。在該方法中會從ViewRootImpl開始自上而下對View樹進行遍歷,首先ViewRootImpl執行PhoneWindow$DecorView的draw()方法,對其繪圖。

  • PhoneWindow$DecorView在其draw()方法中會依次執行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中會遍歷所有的child,呼叫child的draw()方法,對child進行繪圖。首先呼叫其第一個child LinearLayout的draw()方法。

  • LinearLayout在darw()方法中也會依次執行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中會遍歷所有的child,呼叫child的draw()方法,對child進行繪圖。由於其子節點ViewStub不用於渲染,所以此處不對其進行繪圖,對其忽略,對另一個child FrameLayout進行繪圖,呼叫FrameLayout的draw()方法。

  • FrameLayout在draw()方法中也會依次執行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中會遍歷所有的child,呼叫child的draw()方法,對child進行繪圖。其下只有一個child,即RelativeLayout,執行RelativeLayout的draw()方法,對其進行繪圖。

  • RelativeLayout在draw()方法中也會依次執行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中會遍歷所有的child,呼叫child的draw()方法,對child進行繪圖。其下只有一個child,即TextView,執行TextView的draw()方法,對其進行繪圖,並在其中執行TextView的onDraw()方法,對TextView進行實際的渲染。

  • 以上完成了對View樹中LinearLayout及其所有子孫View的繪圖工作,之後會對PhoneWindow$DecorView中的另外兩個View進行繪圖,這也體現了Android採用深度優先演算法對View樹進行遍歷繪圖的過程。View@49da043和View@44ff410會依次執行draw()方法和onDraw()方法。


總結

當我們在onCreate()方法中呼叫setContentView(R.layout.activity_main)方法後,Android會從layout的樹形結構中自上而下開始對所有的View進行量算、佈局、繪圖:

  1. 量算、佈局、繪圖的起點都是ViewRootImpl

  2. 通過呼叫ViewRootImpl的performMeasure() 方法,開始驅動Android自上而下對所有View進行量算,這樣Android就知道了每個View想要的尺寸大小,即寬高資訊

  3. 在完成了對所有View的量算工作後,通過呼叫ViewRootImpl的performLayout()方法,開始驅動Android會自上而下對所有View進行佈局,Android就知道了每個View在其父控制元件中的位置,即View到其父控制元件四邊的left、right、top、bottom

  4. 在完成了對所有View的佈局工作後,通過呼叫ViewRootImpl的performDraw()方法,開始驅動Android會自上而下對所有View進行繪圖,這樣Android就將所有的View渲染到螢幕上了

希望本文對大家理解Android中View的佈局和繪圖機制有所幫助。

相關閱讀:
《Android相關博文整理彙總》
《原始碼解析Android中View的measure量算過程》
《原始碼解析Android中View的layout佈局過程》

相關文章