從源碼角度理解ConstraintLayout#onMeasure對child的measure調用次數

熟悉繪制流程的都知道,ViewGroup可以決定child的繪制時機以及調用次數。

今天我們簡單看下較為復雜的ConstraintLayout,看一下它對子ViewonMeasure調用次數具體是多少。

簡單起見,我們選擇進入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)、chainconstrainedWidth、biasGuideLineratio等典型用法。

現實效果

寬高兩兩組合,一共有四種情況,具體效果如下表:

自身 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

說明:
MonMeasureLonLayoutDonDraw
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;第二次測量針對的是view2view4view7,跳過了view2view4,只測量了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()方法中,view2view4view7會被加入到BasicMeasure#mVariableDimensionsWidgets中。其中view2view4也參與了第一次測量;第二次測量就是針對BasicMeasure#mVariableDimensionsWidgets中的數據的,所以view2view4view7都參與了第二次測量,但是view2view4因為已經測量過了,所以沒有重復測量,只有view7參與了第二次測量。

view7跳過了第一次測量,參與的是第二次測量,具體代碼如下:

在這里插入圖片描述

view2view4會在ConstraintLayout#Measurer#measure中被攔截住,不會再次參與測量。具體代碼如下:

在這里插入圖片描述

總結下,view1view6都參與了第一次測量,view7參與了第二次測量,所以一個測量流程中,每個child只測量了一次。

當然了,我的demo中寫的都是ConstraintLayout中普通的用法,ConstraintLayout中的optimizerHelperVirtualLayout等其他用法都沒有涉及,這些也會對測量流程有影響。不過可以肯定的是,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?

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容