手把手教你讀懂源碼,View的繪制流程詳細剖析

“勿以善小而不為,勿以惡小而為之。惟賢惟德,能服于人?!边@句出自《三國志·蜀書·先主傳》,是托孤遺詔的話,婦孺皆知,卻被這幾天惡作劇了一番。深處如今的畸形直播文化當(dāng)中,前兩天的女主播琪琪的“黃鱔門”,卻以“鱔”小而“慰”之,用其做出不可描述之事,直奔熱搜榜,甚至蓋過中國隊1-0擊敗韓國隊的國足,在薩德之際不以優(yōu)勢打倒對方,而以弱項讓你屈服!

隨感而發(fā),瞎扯遠了,還是回歸到我們的正題,我們這一批做技術(shù)的,不會被外界任何因素干擾,踏踏實實潛心修煉,爭取早日成佛。

上一篇文章我們分析了View的加載流程,今天我們繼續(xù)來深入學(xué)習(xí)View的繪制流程,接著上次的View繪制開始,同樣使用的是Android 7.1源碼。

1、回顧addView方法

上篇文章從addView方法一路分析到了performTraversals()方法,這個方法非常長,內(nèi)部邏輯也很復(fù)雜,但是主體邏輯很清晰。主要調(diào)用了performMeasure方法、performLayout方法和performDraw方法:

其執(zhí)行的過程可簡單的概括為:是否需要重新計算視圖的大小(measure)、是否需要重新布局視圖的位置(layout),以及是否需要重繪(Draw),也就是我們常說的View的繪制流程。

那么接下來我們一同來詳細分析一下。

2、performMeasure

調(diào)用performMeasure之前會先調(diào)用getRootMeasureSpec方法,通過getRootMeasureSpec方法獲得頂層視圖DecorView的測量規(guī)格。

該方法主要作用是在整個窗口的基礎(chǔ)上計算出root view(頂層視圖DecorView)的測量規(guī)格。傳入的兩個參數(shù)分別指:windowSize是當(dāng)前手機窗口的有效寬和高,一般都是除了通知欄的屏幕寬和高;rootDimension是根布局DecorView請求的寬和高,DecorView根布局寬和高都是MATCH_PARENT。

當(dāng)匹配父容器時,測量模式為MeasureSpec.EXACTLY,測量大小直接為屏幕的大小,也就是充滿真?zhèn)€屏幕;

當(dāng)包裹內(nèi)容時,測量模式為MeasureSpec.AT_MOST,測量大小直接為屏幕大小,也就是充滿真?zhèn)€屏幕;

其他情況時,測量模式為MeasureSpec.EXACTLY,測量大小為DecorView頂層視圖布局設(shè)置的大小。

因此DecorView根布局的測量模式就是MeasureSpec.EXACTLY,測量大小一般都是整個屏幕大小,所以一般我們的Activity窗口都是全屏的。所以上面代碼走第一個分支,然后通過調(diào)用MeasureSpec.makeMeasureSpec方法將DecorView的測量模式和測量大小封裝成DecorView的測量規(guī)格。

該方法只是進行了簡單的封裝。

回到performTraversals方法,直接來看調(diào)用的performMeasure方法:

該方法調(diào)用了mView的measure()方法。其中mView是一個View對象,在ViewRootImpl類中的mView是整個UI的根節(jié)點,實際上也就是PhoneWindow中的mDecor對象,即一個Activity所對應(yīng)的一個屏幕(不包括頂部的系統(tǒng)狀態(tài)條)中的視圖,包括可能存在也可能不存在的ActionBar。

繼續(xù)深入查看View的measure方法:

參數(shù)widthMeasureSpec和heightMeasureSpec用來描述當(dāng)前正在處理的視圖可以獲得的最大寬度和高度。

當(dāng)ViewRoot類的成員變量mPrivateFlags的FORCE_LAYOUT位不等于0時,就表示當(dāng)前視圖正在請求執(zhí)行一次布局操作,這時候方法就需要重新測量當(dāng)前視圖的寬度和高度。此外,當(dāng)參數(shù)widthMeasureSpec和heightMeasureSpec的值不等于ViewRoot類的成員變量mldWidthMeasureSpec和mOldHeightMeasureSpec的值時,就表示當(dāng)前視圖上一次可以獲得的最大寬度和高度已經(jīng)失效了,這時候函數(shù)也需要重新測量當(dāng)前視圖的寬度和高度。

當(dāng)View類的measure方法決定要重新測量當(dāng)前視圖的寬度和高度之后,它就會首先將成員變量mPrivateFlags的MEASURED_DIMENSION_SET位設(shè)置為0,接著再調(diào)用onMeasure方法來真正執(zhí)行測量寬度和高度的操作。View類的onMeasure方法執(zhí)行完成之后,需要再調(diào)用setMeasuredDimension方法來將測量好的寬度和高度設(shè)置到View類的成員變量mMeasuredWidth和mMeasuredHeight中,并且將成員變量mPrivateFlags的EASURED_DIMENSION_SET位設(shè)置為1。這個操作是強制的,因為當(dāng)前視圖最終就是通過View類的成員變量mMeasuredWidth和mMeasuredHeight來獲得它的寬度和高度的。

繼續(xù)查看View類的onMeasure()方法:

其實View類的onMeasure方法一般是由其子類來重寫的。如對于用來應(yīng)用程序窗口的頂層視圖的DecorView類來說,它是通過父類FrameLayout來重寫祖父類View的onMeasure方法的,接下來我們就分析FrameLayout類的onMeasure方法的實現(xiàn)。

分析onMeasure方法,我們先從子類DecorView的onMeasure方法入手,這個方法主要是調(diào)整了兩個入?yún)⒏叨群蛯挾?,然后調(diào)用其父類的onMeasure方法。

再看FrameLayout的onMeasure方法,主要是遍歷所有的子View進行測量,然后設(shè)置高度、寬度。

首先是調(diào)用measureChildWithMargins方法來測量每一個子視圖的寬度和高度,并且找到這些子視圖的最大寬度和高度值,保存在變量maxWidth和maxHeight 中。

接著再將前面得到的寬度maxWidth和高度maxHeight分別加上當(dāng)前視圖所設(shè)置的Padding值,得到的寬度maxWidth和高度maxHeight還不是最終的寬度和高度,還需要考慮以下兩個因素:

1. 當(dāng)前視圖是否設(shè)置有最小寬度和高度。如果設(shè)置有的話,并且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大,那么就將它們作為當(dāng)前視圖的寬度和高度值。

2. 當(dāng)前視圖是否設(shè)置有前景圖。如果設(shè)置有的話,并且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大,那么就將它們作為當(dāng)前視圖的寬度和高度值。

經(jīng)過上述兩步檢查之后,F(xiàn)rameLayout類的成員函數(shù)onMeasure就得到了當(dāng)前視圖的寬度maxWidth和高度maxHeight。由于得到的寬度和高度又必須要限制在參數(shù)widthMeasureSpec和heightMeasureSpec所描述的寬度和高度規(guī)范之內(nèi),因此會調(diào)用從View類繼承下來的resolveSizeAndState方法來獲得正確的大小。得到了當(dāng)前視圖的正確大小之后,F(xiàn)rameLayout類的onMeasure方法就可以調(diào)用從父類View繼承下來的setMeasuredDimension方法來將它們?yōu)楫?dāng)前視圖的大小了。

我們首先來看一下resolveSizeAndState方法:

該方法把measureSpec入?yún)⒌膍ode和size解析出來,mode封裝在高位中,然后根據(jù)mode來決定最后返回的size。

回到FrameLayout的onMeasure方法,繼續(xù)分析從父類View繼承下來的setMeasuredDimension方法:

該方法中最關(guān)鍵的步驟是對View的兩個成員變量進行一次賦值,設(shè)置自己所需要的大小。計算的根據(jù)是在xml文件或者代碼中設(shè)置的寬度和高度的參數(shù),參數(shù)指明了要求你是填充父控件(match_parent)還是包裹內(nèi)容(wrap_content)還是精確的一個大小,但最終你的大小不應(yīng)該超過父控件給你提供的空間。

onMeasure()方法結(jié)束之前必須調(diào)用setMeasuredDimensionRaw()來設(shè)置View.mMeasuredWidth和View.mMeasuredHeight兩個參數(shù)。

而當(dāng)這兩個成員變量設(shè)置完成,也就是當(dāng)前的View測量結(jié)束了。

簡單總結(jié)概括一下,measure的時序圖如下:

3、performLayout

繼續(xù)分析ViewRootImpl的performLayout方法:

調(diào)用了根視圖的layout()方法,從傳遞的4個參數(shù)知道DecorView布局的位置是從屏幕最左最頂端開始布局,到屏幕最低最右結(jié)束。因此DecorView根布局是充滿整個屏幕的。

繼續(xù)分析View類的layout方法:

layout()方法有四個參數(shù),分別是left, top, right, bottom,它們是相對于父控件的位移距離。方法里面先調(diào)用了setFrame()方法,該方法非常重要:

該方法先判斷當(dāng)前視圖的大小或者位置是否發(fā)生變化,將參數(shù)保存起來。當(dāng)前視圖距離父視圖的邊距一旦設(shè)置好之后,它就是一個具有邊界的視圖了。接下來又會計算當(dāng)前視圖新的寬度newWidth和高度newHeight,如果它們與上一次的寬度oldWidth和oldHeight的值不相等,那么就說明當(dāng)前視圖的大小發(fā)生了變化,這時候就會調(diào)用onSizeChanged方法來讓子類有機會處理這個變化事件。

繼續(xù)回到layout()方法,后面調(diào)用了onLayout()方法,實際上是給自己的子控件布局。從以上可以知道m(xù)easure出來的寬度與高度,是該控件期望得到的尺寸,但是真正顯示到屏幕上的位置與大小是由layout()方法來決定的。left, top決定位置,right,bottom決定frame渲染尺寸。

發(fā)現(xiàn)onLayout方法是空的,直接看DecorView的onLayout方法:

這里先是調(diào)用了FrameLayout的onLayout方法,然后是調(diào)整個別參數(shù)。繼續(xù)看父類FrameLayout的onLayout方法:

直接調(diào)用了調(diào)用了layoutChildren方法,繼續(xù)分析:

該方法遍歷各個子View,然后調(diào)用子View的layout方法。

需要注意的是FrameLayout布局其實在View類中的layout方法中已經(jīng)實現(xiàn),布局的邏輯實現(xiàn)是在父視圖中實現(xiàn)的,不像View視圖的measure測量,通過子類實現(xiàn)onMeasure方法來實現(xiàn)測量邏輯。

自定義View一般都無需重寫onMeasure方法,但是如果自定義一個ViewGroup容器的話,就必須實現(xiàn)onLayout方法,因為該方法在ViewGroup是抽象的,所有ViewGroup的所有子類必須實現(xiàn)onLayout方法。

簡單總結(jié)概括一下,layout的時序圖如下:

4、performDraw

繼續(xù)分析ViewRootImpl的performDraw方法:

這里面主要看draw方法:

方法結(jié)束前執(zhí)行了drawSoftware方法:

該方法首先獲取需要重繪的位置,鎖定并獲取對應(yīng)的canvas,最后調(diào)用了DecorView的draw方法。

這里的代碼非常簡單,調(diào)用了父類的draw方法,以此查找最終定位到了View類的draw方法:

該類非常重要,也是最后比較關(guān)鍵的繪制操作。代碼比較多,但是注釋解釋的非常清楚,流程具體如下:

1.繪制當(dāng)前視圖的背景。

2.保存當(dāng)前畫布的堆棧狀態(tài),并且在當(dāng)前畫布上創(chuàng)建額外的圖層,以便接下來可以用來繪制當(dāng)前視圖在滑動時的邊框漸變效果。

3.繪制當(dāng)前視圖的內(nèi)容。

4.繪制當(dāng)前視圖的子視圖的內(nèi)容。

5.繪制當(dāng)前視圖在滑動時的邊框漸變效果。

6.繪制當(dāng)前視圖的滾動條。

接下來分別分析這個流程,首先來看背景的繪制,非常簡單:

接著是保存畫布canvas的邊框參數(shù)。獲取當(dāng)前視圖View水平或者垂直方向是否需要繪制邊框漸變效果,如果不需要繪制邊框的漸變效果,就無需執(zhí)行上面的2、5了,那么就直接執(zhí)行上面的3、4、6步驟。

假如我們需要繪制視圖View的邊框漸變效果,那么我們繼續(xù)分析步驟2,3,4,5,6。

這段代碼用來檢查是否需要保存參數(shù)canvas所描述的一塊畫布的堆棧狀態(tài),并且創(chuàng)建額外的圖層來繪制當(dāng)前視圖在滑動時的邊框漸變效果。首先需要計算出當(dāng)前視圖的左、右、上以及下內(nèi)邊距的大小,以便得到邊框所要繪制的區(qū)域。

然后接著繪制當(dāng)前視圖的內(nèi)容,調(diào)用了onDraw方法:

發(fā)現(xiàn)該方法為空,主要在子類中實現(xiàn),繼續(xù)看DecorView的onDraw方法:

當(dāng)前視圖的內(nèi)容繪制完成后,接著繪制子視圖的內(nèi)容,調(diào)用了dispatchDraw方法。

發(fā)現(xiàn)該方法為空,真正的實現(xiàn)在ViewGroup中:

首先判斷當(dāng)前ViewGroup容器是否設(shè)置的布局動畫,然后遍歷給每個子視圖View設(shè)置動畫效果,接著獲得布局動畫的控制器,最后開始布局動畫。

接下來循環(huán)遍歷每一個子View,并調(diào)用drawChild方法繪制當(dāng)前視圖的子視圖View:

這個draw方法也是View里面的方法,被drawChild()方法調(diào)用:

該方法主要判斷是否有繪制緩存,如果有直接使用緩存,如果沒有重復(fù)調(diào)用上面的draw()方法。

然后是第五步,繪制滑動時的漸變效果:

最后在繪制滾動條:

至此,所有的View對象都繪制出來了。

需要注意的是:View繪制的畫布參數(shù)canvas是由surface對象獲得,意味著View視圖繪制最終會繪制到Surface對象去。父類View繪制主要是繪制背景、邊框漸變效果、進度條,View具體的內(nèi)容繪制調(diào)用了onDraw方法,通過該方法把View內(nèi)容的繪制邏輯留給子類去實現(xiàn)。因此在自定義View的時候都一般都需要重寫父類的onDraw方法來實現(xiàn)View內(nèi)容繪制。

簡單總結(jié)概括一下,draw的時序圖如下:

總結(jié)

View的繪制流程是從 ViewRoot 的 performTraversals 方法開始的,它經(jīng)過 measure、layout、draw三個過程才最終將一個View繪制出來,performTraversals會依次調(diào)用 performMeasure,performLayout和 performDraw 三個方法,這三個方法分別會完成 View 的 measure、layout、draw的流程。

在measure方法中,會調(diào)用onMeasure方法,在onMeasure方法中會對所有的子元素進行measure過程,這個時候measure流程就從父容器傳遞給子容器,這樣就完成了一次測量,接著子元素會重復(fù)父容器的measure的測量過程,如此反復(fù)的完成整個View樹的過程。同理performLayout的執(zhí)行原理和performDraw的執(zhí)行原理與performMeasure的原理類似。

關(guān)于View的繪制流程,經(jīng)常出現(xiàn)在Android面試過程中,同時會嚴重影響到界面開發(fā)。這一塊理清了,無論是掌握系統(tǒng)View,還是自定義View,也或者是解決一些bug,都有不小的幫助。

如果還有疑問的童鞋,歡迎留言繼續(xù)討論。


今天就先分享到這里,后續(xù)將推出更多精彩內(nèi)容,歡迎一起探討學(xué)習(xí)進步。

此文章版權(quán)為微信公眾號分享達人秀(ShareExpert)——鑫鱻所有,若轉(zhuǎn)載請備注出處,特此聲明!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容