熟悉繪制流程的都知道,ViewGroup
可以決定child的繪制時機以及調用次數。
今天我們簡單看下較為復雜的ConstraintLayout,看一下它對子View
的onMeasure
調用次數具體是多少。
簡單起見,我們選擇進入Activity
的時機,在前面的blog進入Activity時,為何頁面布局內View#onMeasure會被調用兩次?提到過,進入頁面時最少會走兩遍繪制流程,我們需要觀測下每次繪制流程中,child的onMeasure
執行次數。
系列文章:
從源碼角度理解FrameLayout#onMeasure對child的measure調用次數
從源碼角度理解LinearLayout#onMeasure對child的measure調用次數
從源碼角度理解RelativeLayout#onMeasure對child的measure調用次數
從源碼角度理解ConstraintLayout#onMeasure對child的measure調用次數
ViewGroup在調用onMeasure時,會先測量父View,還是會先測量子View?
通過log觀測現象
時機:進入頁面;
環境:Android sdk版本30
由于ConstraintLayout的功能強大,導致其繪制流程相對復雜,我無法在很短的篇幅中講到所有的繪制關鍵節點,那就退而求其次,以一些比較典型的用法來觀測ConstraintLayout的繪制調用棧,讓大家直觀的感受下ConstraintLayout中繪制相關的核心類。
xml布局:里面的自定義View都只是添加了log。
demo:ConstraintLayoutTest1Activity
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".measure.ConstraintLayoutTest1Activity">
<com.tinytongtong.androidstudy.measure.view.CustomConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.tinytongtong.androidstudy.measure.view.CustomSingleView
android:id="@+id/view1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.tinytongtong.androidstudy.measure.view.CustomTextView
android:id="@+id/view2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:text="我是文本我是文本我是文本"
android:textColor="#FDA413"
android:textSize="12sp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/view3"
app:layout_constraintTop_toTopOf="parent" />
<com.tinytongtong.androidstudy.measure.view.CustomImageView
android:id="@+id/view3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_birthday_cake"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/view2"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="44dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="44dp" />
<com.tinytongtong.androidstudy.measure.view.CustomButton
android:id="@+id/view4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:gravity="center_vertical"
android:orientation="vertical"
android:text="貓了個咪啊"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="@id/guideline1"
app:layout_constraintRight_toRightOf="@id/guideline2"
app:layout_constraintTop_toTopOf="parent" />
<com.tinytongtong.androidstudy.measure.view.CustomLinearLayout
android:id="@+id/view5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_debug"
app:layout_constraintBottom_toBottomOf="parent" />
<com.tinytongtong.androidstudy.measure.view.CustomRelativeLayout
android:id="@+id/view6"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@color/background_info"
app:layout_constraintRight_toRightOf="parent" />
<com.tinytongtong.androidstudy.measure.view.CustomFrameLayout
android:id="@+id/view7"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/background_wtf"
android:minWidth="50dp"
android:minHeight="50dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintLeft_toLeftOf="@id/view1"
app:layout_constraintRight_toRightOf="@id/view1"
app:layout_constraintTop_toBottomOf="@id/view1" />
</com.tinytongtong.androidstudy.measure.view.CustomConstraintLayout>
</LinearLayout>
這里的不居中,使用了ConstraintLayout的一些典型用法,包括不同寬高表示、相對定位(eg:layout_constraintBottom_toBottomOf
)、chain
、constrainedWidth、bias
、GuideLine
、ratio
等典型用法。
現實效果
寬高兩兩組合,一共有四種情況,具體效果如下表:
寬 | 高 | 自身 | view1(固定寬高,ttt、rtr、ltl、btb均為parent) | view2(w、h:wrap_content), app:layout_constrainedWidth="true", app:layout_constraintBottom_toBottomOf="parent", app:layout_constraintHorizontal_bias="0", app:layout_constraintHorizontal_chainStyle="packed", app:layout_constraintLeft_toLeftOf="parent", app:layout_constraintRight_toLeftOf="@id/view3", app:layout_constraintTop_toTopOf="parent" | view3(w、h:wrap_content), app:layout_constraintBottom_toBottomOf="parent", app:layout_constraintLeft_toRightOf="@id/view2", app:layout_constraintRight_toRightOf="parent", app:layout_constraintTop_toTopOf="parent" | view4(w、h:wrap_content), app:layout_constrainedWidth="true", app:layout_constraintBottom_toBottomOf="parent", app:layout_constraintLeft_toLeftOf="@id/guideline1", app:layout_constraintRight_toRightOf="@id/guideline2", app:layout_constraintTop_toTopOf="parent" | view5(w:match,h:wrap), app:layout_constraintBottom_toBottomOf="parent" | view6(w:wrap,h:match), app:layout_constraintRight_toRightOf="parent" | view7(w:0dp,h:0dp), android:minWidth="50dp", android:minHeight="50dp", app:layout_constraintDimensionRatio="1:1", app:layout_constraintLeft_toLeftOf="@id/view1", app:layout_constraintRight_toRightOf="@id/view1", app:layout_constraintTop_toBottomOf="@id/view1" | 備注: parent執行一次measure,CostraintLayout的child只執行一次onMeasure。 |
---|---|---|---|---|---|---|---|---|---|---|
match_parent | match_parent | M:2,L:1,D:0(默認不參與onDraw) | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:2(參與兩次onDraw) | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:1 | |
match_parent | wrap_content | M:2,L:1,D:0 | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:2(參與兩次onDraw) | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:1 | |
wrap_content | match_parent | M:2,L:1,D:0 | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:2(參與兩次onDraw) | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:1 | |
wrap_content | wrap_content | M:2,L:1,D:0 | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:2(參與兩次onDraw) | M:2,L:1,D:1 | M:2,L:1,D:1 | M:2,L:1,D:1 |
說明:
M
:onMeasure
;L
:onLayout
;D
:onDraw
。
M:2,L:1,D:1
表示onMeasure
調用了2次,onLayout
調用了1次,onDraw
調用了一次。
我們知道,進入Activity
時,最少會走兩次onMeasure
方法,具體請看進入Activity時,為何頁面布局內View#onMeasure會被調用兩次?。
觀察表格中的內容我們發現,在我們demo的布局中,不論處于何種約束,一次測量流程中,child的onMeasure都是調用一次
的。
源碼分析-debug分析
ConstraintLayout的繪制相關的核心類
先明確一下,ConstraintLayout的繪制相關的核心類:
1、ConstraintLayout中:
①持有ConstraintWidgetContainer對象mLayoutWidget,繪制是交給ConstraintWidgetContainer來處理的。
②持有Measurer對象mMeasurer,ConstraintLayout中實現了Measurer接口,所有的繪制最終都會調用到ConstraintLayout#Measurer#measure中。
2、ConstraintWidgetContainer中:
①持有多個ConstraintWidget對象,ConstraintLayout的一個child對應一個ConstraintWidget對象。ConstraintWidget對象中也會持有child對象。
②持有BasicMeasure對象mBasicMeasureSolver,BasicMeasure是測量流程的核心類。
③持有DependencyGraph對象mDependencyGraph
3、ConstraintWidget:
①ConstraintLayout的每個child的LayoutParams中的數據,都會轉換到對應的ConstraintWidget中,后續的測量是基于ConstraintWidget的。
②ConstraintWidgetContainer也繼承了ConstraintWidgetContainer。
③通過getCompanionWidget()方法,可以獲取到ConstraintWidget綁定的child。
④持有ConstraintAnchor列表,表示控件相互間的依賴關系。
4、BasicMeasure:測量流程的核心類。
①持有ArrayList<ConstraintWidget>類型變量mVariableDimensionsWidgets,mVariableDimensionsWidgets參與第二次繪制。
5、BasicMeasure#Measurer接口:
①所有的測量操作,最終都會交由BasicMeasure#Measurer#measure方法處理。
②唯一實現類是ConstraintLayout
6、BasicMeasure#Measure類:
①定義了measureStrategy的三種取值。
②定義了horizontalBehavior和verticalBehavior等數據
記住一句話就好:最終的測量邏輯,都在ConstraintLayout#Measurer#measure(ConstraintWidget widget, BasicMeasure.Measure measure)
方法中。
Debug觀測調用棧
由于ConstraintLayout的功能強大,導致其繪制流程相對復雜,我無法在很短的篇幅中講到所有的繪制關鍵節點,那就退而求其次,以一些比較典型的用法來觀測ConstraintLayout的繪制調用棧,讓大家直觀的感受下ConstraintLayout中繪制相關的核心類。
我們在ConstraintLayout#Measurer#measure
方法中打斷點,這樣就可以觀察到所有child
的調用時機
和調用棧
了。
我們的布局中一共有7個child
(不算GuideLine),經過觀測,在一次測量流程中,他們經過了兩輪測量
。第一次測量包含所有的child,但是跳過了view7
;第二次測量針對的是view2
、view4
、view7
,跳過了view2
和view4
,只測量了view7
。這樣下來一次測量流程中,所有的child就只測量了一遍,沒有重復測量
。
第一次測量的調用棧
具體細節我們看下:
第一次測量的調用棧:不會測量所有的widget,會跳過部分widget。本例中跳過了view7
。
調用child#measure
方法的地方:
第一次測量調用鏈:方法后面的行數,跟debug截圖是一致的。
--> ConstraintLayout#onMeasure(int widthMeasureSpec, int heightMeasureSpec):1708行
--> ConstraintLayout#resolveSystem(ConstraintWidgetContainer layout, int optimizationLevel, ...):1594行
--> ConstraintWidgetContainer#measure(int optimizationLevel, int widthMode, int widthSize, ...):120行
--> BasicMeasure#solverMeasure(ConstraintWidgetContainer layout, int optimizationLevel, ...):278行
--> BasicMeasure#measureChildren(ConstraintWidgetContainer layout):134行
--> BasicMeasure#measure(Measurer measurer, ConstraintWidget widget, int measureStrategy):466行
--> ConstraintLayout#Measurer#measure(ConstraintWidget widget, BasicMeasure.Measure measure):811行
--> View#measure(int widthMeasureSpec, int heightMeasureSpec):25466行
--> View#onMeasure(int widthMeasureSpec, int heightMeasureSpec)。
最終的測量邏輯,都在ConstraintLayout#Measurer#measure(ConstraintWidget widget, BasicMeasure.Measure measure)方法中。
第二次測量的調用棧
第二次測量的調用棧:是從BasicMeasure#solverMeasure
方法中,maxIterations
中觸發的,但是這一次不會執行View#onMeasure
方法。
從BasicMeasure#solverMeasure
方法中,maxIterations
中觸發的。
第二次測量調用鏈:跟第一次調用的區別是,BasicMeasure#solverMeasure
方法中直接調用了BasicMeasure#measure
方法。
--> ConstraintLayout#onMeasure(int widthMeasureSpec, int heightMeasureSpec):1708行
--> ConstraintLayout#resolveSystem(ConstraintWidgetContainer layout, int optimizationLevel, ...):1594行
--> ConstraintWidgetContainer#measure(int optimizationLevel, int widthMode, int widthSize, ...):120行
--> BasicMeasure#solverMeasure(ConstraintWidgetContainer layout, int optimizationLevel, ...):372行
--> BasicMeasure#measure(Measurer measurer, ConstraintWidget widget, int measureStrategy):466行
--> ConstraintLayout#Measurer#measure(ConstraintWidget widget, BasicMeasure.Measure measure):811行
--> View#measure(int widthMeasureSpec, int heightMeasureSpec):25466行
--> View#onMeasure(int widthMeasureSpec, int heightMeasureSpec)。
最終的測量邏輯,都在ConstraintLayout#Measurer#measure(ConstraintWidget widget, BasicMeasure.Measure measure)方法中。
ConstraintLayout#onMeasure核心邏輯
我們看下ConstraintLayout#onMeasure方法的核心邏輯:
①ConstraintLayout#updateHierarchy():建立child間的約束關系。
②mLayoutWidget.updateHierarchy():向BasicMeasure#mVariableDimensionsWidgets中添加符合條件的數據。
③resolveSystem:執行測量流程
在第二步mLayoutWidget.updateHierarchy()
方法中,view2
、view4
、view7
會被加入到BasicMeasure#mVariableDimensionsWidgets
中。其中view2
、view4
也參與了第一次測量;第二次測量就是針對BasicMeasure#mVariableDimensionsWidgets
中的數據的,所以view2
、view4
和view7
都參與了第二次測量,但是view2
和view4
因為已經測量過了,所以沒有重復測量,只有view7
參與了第二次測量。
view7
跳過了第一次測量,參與的是第二次測量,具體代碼如下:
view2
和view4
會在ConstraintLayout#Measurer#measure
中被攔截
住,不會再次參與測量。具體代碼如下:
總結下,view1
到view6
都參與了第一次測量,view7
參與了第二次測量,所以一個測量流程中,每個child只測量了一次。
當然了,我的demo中寫的都是ConstraintLayout
中普通的用法,ConstraintLayout
中的optimizer
、Helper
、VirtualLayout
等其他用法都沒有涉及,這些也會對測量流程有影響。不過可以肯定的是,ConstraintLayout
針對測量次數做了大量優化,盡可能的降低了繪制次數,這個從我們的demo中也可以看出來。
總結
通過本次淺顯的實驗,可以大致得出一個結論,一次測量流程中,ConstraintLayout中child#onMeasure的調用次數,大部分情況下是一次
(本次的demo沒有觀測到多次的場景),即使它支持的特性很多,這應該也是我們喜歡使用它的一個原因。
相關資料
ConstraintLayout
進入Activity時,為何頁面布局內View#onMeasure會被調用兩次?
demo:ConstraintLayoutTest1Activity
系列文章:
從源碼角度理解FrameLayout#onMeasure對child的measure調用次數
從源碼角度理解LinearLayout#onMeasure對child的measure調用次數
從源碼角度理解RelativeLayout#onMeasure對child的measure調用次數
從源碼角度理解ConstraintLayout#onMeasure對child的measure調用次數
ViewGroup在調用onMeasure時,會先測量父View,還是會先測量子View?