View 绘制流程分析
掌握 View 繪制流程能對視圖的各個繪制時機有更深刻的認識,并且能寫出更好的自定義 View, 反正看源碼(SDK28)就完了。
一、介紹
二、源碼分析
三、總結(jié)
一、介紹
Activity 是通過 Window 與 View系統(tǒng)進行交互,而 Window 則是通過 ViewRootImpl 與 根View(DecorView)交互,View 最關(guān)鍵的三個步驟就是測量(measure)、布局(layout)、繪制(draw), 最開始繪制的入口是 ViewRootImpl 類的 performTravesals 方法,下圖對整體流程做了個概述:
二、源碼分析
1. measure
MeasureSpec: 這個關(guān)鍵對象貫穿在測量流程中,我們可以把它理解成一個 View 自身的「測量規(guī)格」, 它包含兩個變量一個是 mode(測量模式),另一個是 size(測量尺寸)。
我覺得源碼有一點設(shè)計的特別巧妙,但也很難理解,那就是用位操作來表示某個狀態(tài)值。這么做的原因是能節(jié)省更多的內(nèi)存以及計算更快。MeasureSpec 是一個數(shù)據(jù)結(jié)構(gòu),但是它主要是用來制作一個 int 整型的變量,這個變量高 2 位表示測量模式,低 30 位表示測量尺寸,這是根據(jù)模式的數(shù)量決定的,總共就三種模式,因此用兩位就很夠了,如 01000000000000000000001111010101 粗體即表示模式。兩個變量合并成一個變量了,看到這種方式簡直就像發(fā)現(xiàn)新大陸一般。。但不推薦自己寫代碼的時候用這種方式,因為別人不一定看得懂,可讀性差。。
三種模式:
- UNSPECIFIED: 父視圖不強加任何約束給子視圖,子視圖想多大就多大,此模式一般不會用到,以下討論就略過這個模式了。
- EXACTLY: 精確模式,父視圖已經(jīng)知道子視圖確切的尺寸,一般對應(yīng) match_parent 和 具體數(shù)值。
- AT_MOST: 最大模式,在父視圖允許的范圍內(nèi),子視圖盡量的大,一般對應(yīng) wrap_content。
LayoutParams: 布局參數(shù)。每個 View 都有自身的布局參數(shù),最最基礎(chǔ)的就是寬高,我們平時最常見的就是設(shè)置width 和 height 為 match_parent 或 wrap_content。然后不同的 LayoutParams 有不同的屬性,如 LinearLayout.LayoutParams 就增加了 margin 相關(guān)的屬性。
View 自身的 MeasureSpec 是由父視圖的 MeasureSpec 和 自身的 LayoutParams 一起決定的,接著 View 根據(jù)自身的 MeasureSpec 來確定自身測量后的寬/高。
從入口 ViewRootImpl.java 的 performTraversals 方法開始看,它調(diào)用 performMeasure 之前做了如下操作:
// ViewRootImpl.java ... int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... 復制代碼mWidth, mHeight 表示屏幕的寬高,lp.width, lp.height 表示 DecorView 的寬高屬性,對于 DecorView 來說其 width 和 height 都是 match_parent,因此它的尺寸就是屏幕的尺寸,看下 getRootMeasureSpec 方法做了啥:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {int measureSpec;switch (rootDimension) {case ViewGroup.LayoutParams.MATCH_PARENT:// Window can't resize. Force root view to be windowSize.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);break;case ViewGroup.LayoutParams.WRAP_CONTENT:// Window can resize. Set max size for root view.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);break;default:// Window wants to be an exact size. Force root view to be that size.measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);break;}return measureSpec; } 復制代碼若布局參數(shù)中的寬/高是 MATCH_PARENT, 那么它最終得到的「測量規(guī)格」的 mode 是 EXACTLY, size 是屏幕寬/高,MeasureSpec.makeMeasureSpec 方法就是合并了 mode 和 size, 制作了一個 measureSpec 變量;若布局參數(shù)中的寬或高是 WRAP_CONTENT, 那么它最終得到的「測量規(guī)格」的 mode 是 AT_MOST, size 是屏幕寬/高,乍一看其實尺寸和 MATCH_PARENT 是一樣的,所以一般系統(tǒng)定義的控件或者我們自定義 View 都會對 WRAP_CONTENT 進行處理,否則其實它的效果在大部分情況下和 MATCH_PARENT 并無一致;若是其他值(一般用戶提供了精確的大小),那么它最終得到的「測量規(guī)格」的 mode 是 EXACTLY, size 是用戶給定的值。
在求出 DecorView 的「測量規(guī)格」后,調(diào)用 performMeasure 方法,內(nèi)部主要是調(diào)用了 DecorView 的 measure 方法。由于 measure 方法用 final 修飾了,因此子類無法重寫此方法,所有的視圖都統(tǒng)一經(jīng)過 View 中的 measure 這個方法。
// View.java public final void measure(int widthMeasureSpec, int heightMeasureSpec) {// 前半部分代碼主要做了優(yōu)化,若寬高都不變的情況下// 或沒有強制重新布局的標志位,那就不重新 measure 了...onMeasure(widthMeasureSpec, heightMeasureSpec);... } 復制代碼可以把 measure 方法看做是一個統(tǒng)一的測量入口,做了一些通用的事情,真正的測量是在 onMeasure 方法,這個方法是 View 提供給各個子類去實現(xiàn)的,這里大家能自定義很多測量邏輯,如 LinearLayout 布局容器就是通過此方法獲取垂直、水平線性布局時自身的寬/高,反正總之就是一句話, measure 流程就是為了求出自身測量后的寬/高,并保存下來。現(xiàn)在看下 View 默認的 onMeasure 實現(xiàn):
// View.java protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } 復制代碼getSuggestedMinimumWidth 方法就是看下是否有背景,如果有就獲取背景的寬度,否則看下是否設(shè)置了 minWidth 屬性,getSuggestedMinimumHeight同理。在這里直接就無視這兩個情況吧,正常來說這個方法返回值是 0, 看下 getDefaultSize :
public static int getDefaultSize(int size, int measureSpec) {int result = size;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED:result = size;break;case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:result = specSize;break;}return result; } 復制代碼根據(jù)「測量規(guī)格」獲取測量模式和測量尺寸, 跳過 UNSPECIFIED 模式,當模式為 AT_MOST 和 EXACTLY 時,最原始的 View 視圖無論是指定 match_parent 還是 wrap_content 模式,最后的 size 都是「測量規(guī)格」的 size, 所以對于不重寫 onMeasure 方法的 View 來說,這兩個模式?jīng)]差別。setMeasuredDimension 也是一個 final 修飾的方法,任何視圖都統(tǒng)一將寬/高保存成全局變量以便之后使用。以上就是 View 默認的測量流程,下面看下 ViewGroup 自定義實現(xiàn)的 onMeasure 方法。
由于 DecorView 繼承自 FrameLayout,因此接下來的流程其實會調(diào)用到 FrameLayout 中的 onMeasure, 不過本文不分析 FrameLayout ,而是分析比較常用的 LinearLayout 重寫的 onMeasure 方法,我們只分析垂直方向的:
// LinearLayout.java void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {for (int i = 0; i < count; ++i) {final View child = getVirtualChildAt(i);......measureChildBeforeLayout(child, i, widthMeasureSpec, 0,heightMeasureSpec, usedHeight);final int childHeight = child.getMeasuredHeight();......final int totalLength = mTotalLength;mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));......}maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); } 復制代碼這里不分析 weight 屬性,加上這個屬性就有點復雜了。首先遍歷子視圖,讓每個子視圖都執(zhí)行自身的 onMeasure 方法,這個過程在 measureChildBeforeLayout 方法內(nèi),一會兒在分析。測量子 View 之后,child.getMeasuredHeight() 就能獲得這一波測量后的高度了,mTotalLength 可以看做是目前 child 在豎直方向累加的高度(包括padding, margin)。最后調(diào)用 setMeasuredDimension 表示這次測量結(jié)束,會記錄測量后的寬和高。measureChildBeforeLayout 內(nèi)部會直接調(diào)用 measureChildWithMargins, 此方法是父容器測量子視圖的統(tǒng)一入口:
// ViewGroup.java protected void measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed) {final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();final int = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed, lp.width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed, lp.height);child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } 復制代碼是否還記得之前說的 View 的「測量規(guī)格」是由父視圖的「測量規(guī)格」和自身的布局參數(shù)決定的,這里 childWidthMeasureSpec 就是通過 父視圖的「測量規(guī)格」+ 自身的布局參數(shù) + padding + margin + 已使用的寬/高 決定的。
// ViewGroup.java public static int getChildMeasureSpec(int spec, int padding, int childDimension) {// 這是父容器的測量模式int specMode = MeasureSpec.getMode(spec);// 這是父容器的測量尺寸(寬/高)int specSize = MeasureSpec.getSize(spec);int size = Math.max(0, specSize - padding);int resultSize = 0;int resultMode = 0;switch (specMode) {// Parent has imposed an exact size on us// 父容器是精確模式 EXACTLY case MeasureSpec.EXACTLY:// 子視圖有一個精確的尺寸,那么它的測量尺寸也就是這個大小,// 并且指定它的模式為 EXACTLYif (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;}// 子視圖布局的寬/高是 MATCH_PARENT,那么它的大小就是父容器的大小,// 并且指定它的模式為 EXACTLY,這里就能看出,一般精確值和 MATCH_PARENT 對應(yīng) EXACTLYelse if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size. So be it.resultSize = size;resultMode = MeasureSpec.EXACTLY;} // 子視圖布局的寬度是 WRAP_CONTENT,那么它的大小就是父容器的大小,// 并且指定它的模式為 AT_MOST,所以一般來說自定義View要重寫onMeasure。else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// Parent has imposed a maximum size on us// 父容器是最大模式 AT_MOST case MeasureSpec.AT_MOST:// 這里的邏輯和父容器為精確模式時完全一樣,// 看起來子視圖指定了精確值就不受父容器的約束了if (childDimension >= 0) {// Child wants a specific size... so be itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} // 和父容器精確模式相比,大小都是父容器的大小,// 測量模式跟隨父容器的模式。else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size, but our size is not fixed.// Constrain child to not be bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;} // 依然和父容器精確模式一樣else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;......// 最后制作一個子View自身的「測量規(guī)格」return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } 復制代碼上面的注釋寫的比較清晰了,總結(jié)下獲取子視圖 MeasureSpec 的過程:如果子 View 布局參數(shù)的尺寸是精確值,那么父容器的 mode 不會影響到子視圖,子視圖都是 EXACTLY 模式 + 精確值尺寸;如果子 View 的寬/高是 MATCH_PARENT, 那么子視圖跟隨父容器模式 + 父容器尺寸;如果子 View 的寬/高是 WRAP_CONTENT,那么子視圖是 AT_MOST 模式 + 父容器尺寸。
在獲得子視圖的「測量規(guī)格」后直接調(diào)用子視圖的 measure 方法讓子視圖根據(jù)自身的 MeasureSpec 得到測量后的寬高,這個流程和之前講解的又是一樣的。
到此為止 LinearLayout 的 onMeasure 垂直方向大致的流程已經(jīng)分析完畢。總結(jié)下流程:它會先遍歷所有子視圖,通過 LinearLayout 的 MeasureSpec 和子視圖的 LayoutParams 得出子視圖的 MeasureSpec,接著讓子視圖執(zhí)行 measure 方法 ,計算子視圖測量后的寬/高。通過累加子視圖的高度,如果 LinearLayout 是 EXACTLY 模式那么高度還是自身的尺寸,如果 LinearLayout 是 AT_MOST 模式那么對比子視圖高度總和取較小一方作為 LinearLayout 的高度。同理,寬度也有這么一個比較過程。關(guān)于 weight 屬性,最關(guān)鍵的其實是它會讓子視圖 measure 兩次,稍微有點耗時。
舉個栗子,現(xiàn)在有一個布局,LinearLayout 中嵌套一個 TextView 和 View 視圖,以下是圖解:
2. layout
layout 和 measure 的流程是類似的,直接上源碼:
// ViewRootImpl.java private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {......host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());// 以下主要是對 requestLayout 處理,暫不深究。...... } 復制代碼host 就是 DecorView, 直接可以看到 View.layout 方法,雖說此方法沒被 final 修飾,但可以看做統(tǒng)一入口,其他子類貌似并沒有重寫此方法:
public void layout(int l, int t, int r, int b) {.....boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);......onLayout(changed, l, t, r, b);...... } 復制代碼先解釋下前半部分的代碼,這里的 l, t, r, b 分別表示 自身左邊緣與父容器左邊緣的距離、自身上邊緣與父容器上邊緣的距離、自身右邊緣與父容器左邊緣的距離、自身下邊緣與父容器上邊緣的距離,根據(jù)這些值就能得出自身的寬度為 r - l, 高度為 b - t, 以及自身的四個頂點。 這里比較重要的是 setFrame 方法,里面用全局變量 mLeft, mTop, mRight, mBottom 分別記錄了 l, t, r, b, 這個時候它的寬/高算是真正的定下來了(注意 measure 階段的測量寬高不一定是最終寬高),并且 setFrame 內(nèi)部調(diào)用了, onSizeChanged 方法,于是恍然大悟,怪不得寫自定義 View 的時候要在 onSizeChanged 內(nèi)拿最終寬高。
接下來解釋下 layout 方法中的 onLayout 方法。View 類并沒有實現(xiàn) onLayout,也就是說它完全去讓子類去實現(xiàn)了,并且 ViewGroup 將此方法設(shè)為抽象方法強制去實現(xiàn),因此只要是父容器都得實現(xiàn) onLayout 來控制子視圖的位置,而子視圖沒有特殊需求基本不需要去實現(xiàn)此方法。下面看下 LinearLayout 重寫的 onLayout 方法,同樣只看垂直方向:
void layoutVertical(int left, int top, int right, int bottom) {......for (int i = 0; i < count; i++) {......setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);} } 復制代碼依然還是省略了一堆代碼,只需要解釋關(guān)鍵的幾個變量。 childLeft 表示子視圖的左邊緣與父容器的左邊緣的距離,這個變量會被padding, margin, gravity 所影響。childTop 表示子視圖的上邊緣與父容器的上邊緣的距離,受到 padding, 已累加的高度影響(因為是垂直布局)。childWidth 和 childHeight 分別是子視圖的測量后的寬/高。在 setChildFrame 方法中直接調(diào)用了 child.layout, 那么 layout 事件繼續(xù)往子容器傳遞,過程和之前解釋的一樣。
對 layout 做個總結(jié):layout 方法的四個參數(shù)決定了自身在父容器內(nèi)的位置保存為 mLeft, mTop, mRight, mBottom,此方法真正確定了自身的最終寬高。然后如果是繼承 ViewGroup 的父容器,那么會重寫 onLayout 方法對子視圖進行布局確定它們的位置,最后會調(diào)用到子視圖的 layout 方法,按這種步驟一直傳遞。
依然舉個栗子,,LinearLayout 中嵌套一個 TextView 和 View 視圖,以下是圖解:
3. draw
performDraw 方法會調(diào)到 View 的 draw 方法,重點在于 onDraw 自身的繪制,這也是自定義 View 實現(xiàn)的最關(guān)鍵方法,其次是 dispatchDraw, 此方法在 ViewGroup 被重寫主要用來遍歷子視圖并調(diào)用它們的 draw 方法傳遞繪制事件:
public void draw(Canvas canvas) {// 繪制背景drawBackground(canvas);// 繪制自身內(nèi)容onDraw(canvas);// 遍歷子視圖讓它們繪制 drawdispatchDraw(canvas);// 畫裝飾(前景,滾動條)onDrawForeground(canvas);// 繪制默認焦點高亮drawDefaultFocusHighlight(canvas); } 復制代碼draw 調(diào)用流程是比較清晰簡單的,但它真正的實現(xiàn)是很復雜的,這一塊是自定義 View 的關(guān)鍵部分,需要學很多東西呀。。不過從這里能看出自定義 View 主要是重寫 onDraw 以及 onMeasure 方法,而自定義 ViewGroup 主要是重寫 onMeasure 以及 onLayout 方法。
三、總結(jié)
用文字的形式表達下整個繪制流程:
整個繪制流程的入口是 ViewRootImpl.performTravesals 方法,繪制的先后順序是 measure, layout, draw.
performMeasure 通過計算得出 DecorView 的 MeasureSpec 然后調(diào)用其 measure 方法,此方法是 View 類的統(tǒng)一入口,主要是做了判斷是否要測量和布局,如果需要則直接調(diào)用重寫的 onMeasure 方法(因繼承 ViewGroup 容器的布局特性所決定的)根據(jù) MeasureSpec 對自身進行測量得出寬/高。父容器會遍歷所有子視圖,根據(jù)自身的 MeasureSpec 和 子視圖的 LayoutParams 決定子視圖的 MeasureSpec, 并調(diào)用子視圖的 measure 方法傳遞測量事件,直到傳遞到整個 View 樹的葉子為止。
performLayout 從 View 樹的頂端開始,依次向下調(diào)用 layout 方法來確認自身在父容器內(nèi)的位置,這時最終的寬高被確認,然后調(diào)用重寫過的 onLayout 方法(根據(jù)布局特性重寫)來確認所有子視圖的位置。
performDraw 也是按照前面測量和布局的思路傳遞在整個 View 樹中,onDraw 繪制自身的內(nèi)容是實現(xiàn)自定義View的最關(guān)鍵方法。
View 相關(guān)的常見問題:
- requestLayout 為什么耗時?View 調(diào)用 requestLayout 方法后,會自下而上傳遞事件,將設(shè)置每層 View 的測量和布局的標志位,最后會調(diào)用 performTravesals 方法基本會重新走一遍整棵 View 樹的繪制流程 measure, layout, draw。
- invalidate 和 postInvalidate?這兩個重繪方法也會調(diào)用到 performTravesals, 但不會設(shè)置測量和布局的標志位,所以只會執(zhí)行 draw 過程。invalidate 在主線程中執(zhí)行,postInvalidate 是異步繪制,通過 handler 回調(diào)到主線程。
- onMeasure 多次調(diào)用的情況?繪制過程中可能會出現(xiàn)多次 measure 的情況,如父容器 LinearLayout 使用了 weight 屬性。
- onSizeChanged 調(diào)用時機?此方法在 layout 中調(diào)用,這時已經(jīng)確認了最終的寬/高,因此這個方法取寬高的時機比 onMeasure 取寬高的時機靠譜。
- RelativeLayout 和 LinearLayout 性能對比?一般層級比較多的情況下推薦使用 RelativeLayout,因為它可以有效減少 LinearLayout 的層級問題,但只有一層的情況下推薦用 LinearLayout,因為 RelativeLayout 總是會 measure 兩次,而 LinearLayout 不設(shè)置 weight 的話只會 measure 一次。RelativeLayout 中優(yōu)先用 padding 而不是 margin,對margin 的處理比較耗時。
- 還有啥問題呢。。
最后推薦 ConstraintLayout,還沒有真正去研究這個約束布局,但它基本一層就能搞定一個布局,還管你什么層級的性能問題嗎?應(yīng)該是完爆其他布局的。
轉(zhuǎn)載于:https://juejin.im/post/5c4c1ac55188252e46622d96
總結(jié)
以上是生活随笔為你收集整理的View 绘制流程分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 设计模式-享元模式
- 下一篇: 你真的会搜索?低效的你简直在浪费生命(三