Android View的繪製流程分為三大流程:測量、佈局、繪製。三大流程都開始於ViewRootImpl的performTraversals
函式。通過了解三大流程的順序和原理,支撐日常開發工作。繪製是Android進階攔路虎之一。
一、測量流程
三大流程都是始於ViewRootImpl的performTravels
函式,先是從呼叫View的performMeasure
函式開始測量流程,再是呼叫performLayout
函式開始佈局流程,進而是呼叫performDraw
函式開始繪製流程。本節從performMeasure
函式開始,講View的測量流程。
正式開始測量流程了~
performMeasure
函式會呼叫View的measure
函式。
measure
函式第一行會呼叫isLayoutModeOptical
函式,用來判斷當前View是否ViewGroup ,是ViewGroup的話,判斷layoutModel
屬性是否LAYOUT_MODE_OPTICAL_BOUNDS
,即opticalBounds
。該屬性預設為clipBounds
,還可取值opticalBounds
,前者在獲取ViewGroup的四邊(getLeft
,getTop
,getRight
,getBottom
)將返回原始的值,而opticalBounds
表示給ViewGroup加一些特殊的效果,例如陰影或高亮效果,因為返回的四邊也將比clipBounds
小。
measure
函式接下來的這一段主要是為了判斷是否需要進行重新測量,畢竟每次測量也不容易。
//用於儲存上次測量的結果
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
//view是否需要強行重新整理,呼叫froceLayout
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
//判斷此次的widthMeasureSpec與heightMeasureSpec是否與上次相等
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
//判斷此次測量模式是否精確,不是精確的可能需要重新測量
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
//判斷此次測量大小是否與已儲存的大小一致,不是一致可能需要重新測量
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
//如果specChanged為false,即寬高measureSpec與上次都相等,不需要重新測量;true則進一步檢查其他條件
//sAlwaysRemeasureExactly主要用於判斷LinearLayout在舊版本的不同測量模式都會返回不同的測量結果,小於Android 6.0為true,大於為false;所以但小於Android 6.0需要重新測量
//如果isSpecExactly測量模式是非精確模式需要重新測量
//如果matchesSpecSize與已儲存大小不一致需要重新測量
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
複製程式碼
needsLayout
就是根據上面相關變數的值共同判斷是否需要重新測量的最終結果。也可以通過下圖一覽上面的註釋。
接著measure
函式的內容,當呼叫forceLayout
或requestLayout
函式,mPrivalteFlags
就會新增PFLAG_FORCE_LAYOUT
標記,那麼forceLayout
就是true,無論後面其他判斷條件怎麼樣,一定會呼叫onMeasure
函式進行測量。而needsLayout
就在上文剛分析了。
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
語句重置所有的已設定的測量資訊,畢竟要準備重新開始測量了。resolveRtlPropertiesIfNeeded()
主要是處理文字從右到左的情況,因為並不是所有國家文字書寫順序都是從左到右。
LongSparseLongArray
是key與value都為Long,類似HashMap的資料結構。這裡正是通過這種結構用來儲存測量的寬和高,如果mMeasureCache.indexOfKey(key)
返回值小於0,表示不存在對應的寬高,需要測量。
sIgnoreMeasureCache
表示為了效能優化而忽略測量快取,其實是為了相容舊版本,因為在Android4.4前,APP總是希望onMeasure
函式被呼叫,所以該變數總是true,而Android 4.4和後續版本,該標誌總是false。
因此,如果需要測量,則呼叫當前View的onMeasure
函式;不需要重新測量,則從快取mMeasureCache獲取已快取寬高。
measure
函式的最後程式碼就是儲存父View對當前View的寬高要求和往mMeasureCache存值,以供下次測量作為判斷條件使用。
measure函式總結一下:
measure函式主要是為了效能優化,根據快取(已快取)、父類約束是不與上次一致,和行為(重新整理佈局)來判斷是否重新測量大小。
接下來看看View的onMeasure
函式做了什麼事:
看著簡單,其實還是要拆解看看:
getSuggestedZMininumWidth
函式主要判斷當前是否設定背景,如果沒有設定背景,則取最小寬度;設定了背景,則取最小寬度和背景最小寬度的兩者之間的最大值。最小寬度就是我們設定的minWidth
屬性。高度的測量亦是如此。
getDefaultSize
函式主要是根據測量模式,計算出預設的尺寸大小。
到這裡,就應該需要對MeasureSpec的大小和測量模式解釋一下,不然有的同學真一臉懵逼。MeasureSpec是View的靜態內部類,代表一個32位的整型,高2位表示測量模式,低30位表示尺寸大小。
measure
函式的兩個引數widthMeasureSpec,heightMeasureSpec,分別代表著父View對子View的寬高約束。從這裡也可以看出,子View的大小由父View約束和子View自身自身約束共同確定。
通過MeasureSpec提供的一些靜態方法,如int getSize(int measureSpec)
、int getMode(int measureSpec)
,可以獲取到測量模式mode和大小size,分別為:
- EXACTLY:當View的layout_width或者layout_height設定為
match_parent
或具體的值時,該測量模式就是EXACTLY,表示父View對當前View的尺寸要求大小是size; - AT_MOST:當View的
layout_width
或者layout_height
屬性設定為wrap_content
,該測量模式就是AT_MOST,表示父View能給予當前View的最大的可用尺寸是size,具體用多少當前View自己決定; - UNSPECIFIED:表示父View對當前View沒有任何約束,想要多大的尺寸當前View自己決定。
從getDefaultSize
函式對測量模式AT_MOST
和EXACTLY
的處理方式看,自定義View繼承View時,要格外注意layout_width
或layout_height
屬性值為wrap_content
的情況,因為它的表現就跟match_parent
是一樣的,有時需要根據具體情況去更改這種行為。
setMeasureDimension
函式開始跟measure
函式類似,先判讀一下layoutModel
是否optical bound
,進行寬高的調整,並呼叫setMeasureDimensionRaw
函式。
setMeasureDimension
則是簡單的賦值,設定mPrivateFlags
標誌位。這樣就可以通過getMeasuredWidth
與getMeasuredHeight
函式來獲取測量的寬高了。注意: 重寫onMeasure
函式需要呼叫setMeasureDimension
函式進行資料快取。
測量流程也就到此結束了。但仔細一想,發現不對勁,這裡測量指的是View,那麼ViewGroup呢?
ViewGroup是View的子類,而View的measure
函式被被宣告成了final,所以ViewGroup測量自身或者測量子View只能重寫onMeasure
函式。但在ViewGroup類仔細尋找,卻沒有發現重寫onMeasure
函式的痕跡。具體原因是因為具體的ViewGroup,如LinearLayout和RelativeLayout它們各自的測量方式是不一樣的,onMeasure
需要它們具體去實現。但ViewGroup類提供了一些便捷的api,如measureChildren
、measureChildWithMargins
、measureChild
等等。
翻翻LinearLayout的onMeasure
函式,最終也會呼叫View的measure
函式,走View的測量流程。
因此自定義View或者ViewGroup,需要根據自身實現的功能去重寫omMeasure函式,來測量自身或子View的大小
二、佈局流程
上一節分析了測量流程,得知了每個View的寬高大小,這一節緊跟著分析佈局流程,判斷子View如何在父View進行定位。performLayout
函式同樣是在ViewRootImpl類的performTraversals
函式中,performMeasure
函式之後。
可以看到,
performLayout
函式很快就呼叫了View的layout
函式進行佈局流程。這裡先不跟進去,只需要知道已經進行了一次佈局,然後看performLayout
函式的後續內容。
mLayoutRequesters
是一個儲存了在佈局過程中所有請求佈局的View的列表。當列表不為空時候,需要對這些View進行處理。
在佈局的過程中,可能View請求佈局(即設定了PFLAG_FORCE_LAYOUT
),將它們存到列表mLayoutRequesters
中,然後在佈局結束後,第一次通過getValidLayoutRequesters
函式判斷這些View是否需要重新佈局,判斷條件就是當前View是否可見和設定了PFLAG_FORCE_LAYOUT
標誌。
如果返回值
validLayoutRequesters
不為空,重新設定他們的標誌位PFLAG_FORCE_LAYOUT
,並呼叫measureHierarchy
函式,對它們進行View層級的測量,測量流程和整個介面測量流程是一致。然後再跟著重新佈局一次host.layout()
。
進行第二次判斷是否還有在佈局過程中,有View請求佈局,如果有的話,判斷有效的需要重新佈局的View,這次判斷忽略了PFLAG_FORCE_LAYOUT
標誌位,除了不可見的View,其他都列為需要有效的。然後留到下次幀再重新來過。
總結一下
在第一次佈局的過程中,如果有View需要requestLayout
函式(一般發生在ListView等的子View),則需要判斷這些View是否可見或已經處理了requestLaout
。如果有可見的、未處理requestLayout
的View則需要進行View層次級別的測量,然後重新佈局一次。然後進行第二次判斷是否有View需要requestlayout
,這次只判斷是否可見。如果還有,這些View就留到下一幀進行吧,老子不管了。
再回到第一次佈局host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
。
setOpticalFrame
函式最終也會呼叫setFrame,只是追加了點效果邊距長度。setFrame
函式主要是對當前View在父View的位置進行確定,如果此時定位位置有變(四邊有不一致),則changed返回的是true。在setFrame
函式會呼叫sizeChanged
函式,而sizeChanged
函式會呼叫onSizeChanged
函式。
onLayout
函式在View中是一個空實現,而在ViewGroup未重寫該方法。因為子View在父View位置,在不同的ViewGroup表現也是不同的,所以需要具體的ViewGroup根據自己的特性去重寫。但這裡我們注意到一個時機,onSizeChanged
函式的回撥在onMeasure
函式之後,onLayout
函式之前,在尺寸大小發生變化時會回撥該方法。
在呼叫onLaout
函式後的主要進行OnLayoutChangeListener
的回撥和焦點的處理。isLayoutValid
函式表示至少已經經歷過一次佈局了或者不會再進行其他佈局了,就返回true。
到這裡,佈局流程基本也就結束了。
本節小結
佈局流程始於ViewRootImpl的performTraversals
函式,然後呼叫自身的performLayout
函式,對View進行佈局,佈局結束後對佈局過程有請求佈局的View進行View層級測量和佈局。在View的layout
函式中,通過setFrame
對自身進行佈局定位,如果位置發生變化則回撥onSizeChanged
函式。再而是呼叫onLayout
函式。因此自定View無需重寫onLayout
函式,自定義ViewGroup則需要重寫onLayout
函式進行子View的佈局。
三、繪製流程
經過測量、繪製,已經知道了View的大小,在父View的位置,那麼接下來就是如何將View繪製出來,展現在螢幕。
繪製流程始於ViewRootImpl的performTraversals
函式,呼叫自身的performDraw
函式。
//ViewRootImpl.java
performTraversals=>performDraw=>draw=>drawSoftware=>View.draw
複製程式碼
在draw
函式中,主要是繪製區域dirty
的確定,例如是否滾動、全部繪製等。
drawSoftware
函式就是通過軟體去繪製的地方,主要根據dirty區域,生成並鎖定canvas,而canvas就是繪製內容的區域。
而在View的draw
函式,則是View的繪製的開始:
drawBackground=>onDraw=>dispatchDraw=>onDrawForeground=>drawDefaultFocusHighlight
複製程式碼
在View的draw流程中,View一般重寫onDraw
函式,super.onDraw
後繪製自己的內容,表示所繪製內容在系統繪製的內容之後。而在ViewGroup中,如果需要覆蓋在子View之上,應該是重寫dispatchDraw
函式,並呼叫super.dispatchDraw
之後,因為dispatchDraw
函式會去繪製所有子View的內容,在之前繪製的內容都會被覆蓋。當然,也可以以dispatchDraw
作為分界點,根據需要重寫其他函式,繪製內容。
如果重寫ViewGroup的onDraw
函式,繪製的內容一般顯示不出來,因為ViewGroup會優化從而跳過onDraw
函式,可以通過設定背景或setWillNotDraw(false)
來解決這個問題。
四、總結
通過學習Android的繪製流程,需要知道幾點情況:
- 自定View時,需要考慮寬高設定
wrap_content
的情況,因為它的表現在測量階段和match_parent
是一致的。 - 重寫View的
onDraw
函式,要避免在onDraw建立物件,因為onDraw會被呼叫多次,可以考慮在onSizeChanged
函式建立。 - 如果View或ViewGroup需要改變自身大小,應該在
onMeasure
函式實現,並通過setMeasureDimension
儲存下來。 - 重寫ViewGroup的
onDraw
函式時,要注意onDraw函式在整個draw流程的地位,以及它並不是都會被呼叫。
番外篇
1、MeasureSpec是什麼?作用
MeasureSpec
在View中的一個靜態內部類,能將一個32位整型拆分成測量模式和測量大小,代表著父View對子View的約束。32位的整型,高兩位代表著測量模式,低三十位代表測量大小。通過位位運算,可以分別獲取測量模式和大小,而合併成一個32位整型,只需要相加即可。
例如,給寬設定10,此時測量模式是精確模式EXACTLY,即01,用32位的整型表示應該是(暫且用xxx表示中間所有的0)
若求測試模式model,只需要和高兩位都是1,低三十位都是0的MODE_MASK
按位與即可。
程式碼:
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
複製程式碼
若求測試大小,只需要和取反後MODE_MASK
按位與即可。
程式碼:
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
複製程式碼
2、requestLayout和invalidate,postInvalidate區別?
一般來說,需要重新測量佈局,就呼叫requestLayout,然後在呼叫invalidate保證onDraw一定被呼叫。也就是說requestLayout不一定保證onDraw被呼叫,但會呼叫onMeasure和onLayout。而invalidata只會呼叫到onDraw。invalidate在UI執行緒重新整理介面,postInvalidate表示在子執行緒重新整理介面。