原文鏈接 Android View的渲染過(guò)程
對(duì)于安卓開發(fā)猿來(lái)說(shuō),每天都會(huì)跟布局打交道,那么從我們寫的一個(gè)布局文件,到運(yùn)行后可視化的視圖頁(yè)面,這么長(zhǎng)的時(shí)間內(nèi)到底 發(fā)生了啥呢?今天我們就一起來(lái)探詢這一旅程。
[圖片上傳失敗...(image-951964-1687781701075)]
View tree的創(chuàng)建過(guò)程
布局文件的生成過(guò)程
一般情況下,一個(gè)布局寫好了,如果不是特別復(fù)雜的布局,那么當(dāng)把布局文件塞給Activity#setContentView或者一個(gè)Dialog或者一個(gè)Fragment,之后這個(gè)View tree就創(chuàng)建好了。那么setContentView,其實(shí)是通過(guò)LayoutInflater這個(gè)對(duì)象來(lái)具體的把一個(gè)布局文件轉(zhuǎn)化為一個(gè)內(nèi)存中的View tree的。這個(gè)對(duì)象不算太復(fù)雜,主要的邏輯就是解析XML文件,把每個(gè)TAG,用反射的方式來(lái)生成一個(gè)View對(duì)象,當(dāng)XML文件解析完成后,一顆View tree就生成完了。
但是需要注意,inflate之后雖然View tree是創(chuàng)建好了,但是這僅僅是以單純對(duì)象數(shù)據(jù)的形式存在,這時(shí)去獲取View的一些GUI的相關(guān)屬性,如大小,位置和渲染狀態(tài),是不存在的,或者是不對(duì)的。
手動(dòng)創(chuàng)建
除了用布局文件來(lái)生成布局,當(dāng)然也可以直接用代碼來(lái)擼,這個(gè)就比較直觀了,view tree就是你創(chuàng)建的,然后再把根節(jié)點(diǎn)塞給某個(gè)窗口,如Activity或者Dialog,那么view tree就創(chuàng)建完事了。
渲染前的準(zhǔn)備工作
View tree生成的最后一步就是把根結(jié)點(diǎn)送到ViewRootImpl#setView里面,這里會(huì)把view添加到wms之中,并著手開始渲染,接下來(lái)就主要看ViewRootImpl這個(gè)類了,主要入口方法就是ViewRootImpl#requestLayout,然后是scheduleTraversals(),這里會(huì)把請(qǐng)求放入到隊(duì)列之中,最終執(zhí)行渲染的是doTraversal,它里面調(diào)用的是performTraversals(),所以,我們需要重點(diǎn)查看ViewRootImpl#performTraversals這個(gè)方法,view tree渲染的流程全在這里面。這個(gè)方法相當(dāng)之長(zhǎng),接近1000行,主要就是三個(gè)方法performMeasure,performLayout和performDraw,就是常說(shuō)的三大步:measure,layout和draw。
渲染之measure
就看performMeasure方法,這個(gè)方法很簡(jiǎn)單,就是調(diào)用了根view的measure方法,然后傳入widthSpec和heightSpec。measure的目的就是測(cè)量view tree的大小,就是說(shuō)view tree在用戶可視化角度所占屏幕大小。要想理解透徹measure,需要理解三個(gè)事情,MeasureSpec,View#measure方法和View#onMeasure方法:
理解MeasureSpec
從文檔中可以了解到,MeasureSpec是從父布局傳給子布局,用以代表父布局對(duì)子布局在寬度和高度上的約束,它有兩部分一個(gè)是mode,一個(gè)是對(duì)應(yīng)的size,打包成一個(gè)integer。
-
UNSPECIFIED
父布局對(duì)子布局沒(méi)有要求,子布局可以設(shè)置任意大小,這個(gè) 基本上 不常見。
-
EXACTLY
父布局已經(jīng)計(jì)算好了一個(gè)精確的大小,子布局要嚴(yán)格按照 這個(gè)來(lái)。
-
AT_MOST
子布局最大可以達(dá)到傳過(guò)來(lái)的這個(gè)尺寸。
光看這幾個(gè)mode,還是不太好理解。因?yàn)槲覀兤饺绽飳懖季?,在大小(或者說(shuō)寬和高)這塊就三種寫法:一個(gè)是MATCH_PARENT,也就是要跟父布局一樣大;要么是WRAP_CONTENT,也就是說(shuō)子布局想要?jiǎng)偤煤线m夠顯示自己就行了;再者就是寫死的如100dp等。需要把measure時(shí)的mode與LayoutParams結(jié)合聯(lián)系起來(lái),才能更好的理解measure的過(guò)程。
還是得從performMeasure這時(shí)入手,這個(gè)MeasureSpec是由父節(jié)點(diǎn)傳給子節(jié)點(diǎn),追根溯源,最原始的肯定是傳給整個(gè)view tree根節(jié)點(diǎn)的,也就是調(diào)用performMeasure時(shí)傳入的參數(shù)值。
根節(jié)點(diǎn)的MeasureSpec
根節(jié)點(diǎn)的MeasureSpec是由getRootMeasureSpec得來(lái)的,這個(gè)方法傳入的是窗口的大小,這是由窗口來(lái)給出的,當(dāng)前的窗口肯定 是知道自己的大小的,以及根節(jié)點(diǎn)布局中寫的大小。從這個(gè)方法就能看出前面說(shuō)的布局中的三種寫法對(duì)MeasureSpec的影響了:
- 如果 根節(jié)點(diǎn)布局是MATCH_PARENT的,那么 mode就是EXACTLY,大小就是父布局的尺寸,因?yàn)楦?jié)點(diǎn)的父親就是窗口,所以就是窗口的大小
- 如果 根節(jié)點(diǎn)布局是WRAP_CONTENT的,那么 mode是AT_MOST,大小依然會(huì)是父布局的尺寸。這個(gè)要這樣理解,WRAP_CONTENT是想讓子布局自己決定自己多大,但是,你的極限 就是父布局的大小了。
- 其他,其實(shí)就是根節(jié)點(diǎn)寫死了大小的(寫布局時(shí)是必須 要指定layout_width和layout_height的,即使某些view可以省略一個(gè),也是因?yàn)槿笔≈?,而并非不用指定),那么mode會(huì)是EXACTLY,大小用根節(jié)點(diǎn)指定的值。
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;
}
子View的MeasureSpec
MeasureSpec這個(gè)東西是自上而下的,從根節(jié)點(diǎn)向子View傳遞。前面看過(guò)了根節(jié)點(diǎn)的spec生成方式,還有必要再看一下子View在measure過(guò)程中是如何生成spec的,以更好的理解整體過(guò)程。主要看ViewGroup#getChildMeasureSpec方法就可以了:
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
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} 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
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = 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;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let them have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
單純從spec角度來(lái)理解,與上面的是一樣的,基本上WRAP_CONTENT會(huì)是AT_MOST,而其他都是EXACTLY。
后面會(huì)再詳細(xì)討論一下,父布局與子View的相互影響。
View#measure和View#onMeasure
performMeasure比較簡(jiǎn)單,只是調(diào)用根節(jié)點(diǎn)的measure方法,然后把計(jì)算出來(lái)的根節(jié)點(diǎn)的MeasureSpec傳進(jìn)去,就完事了,所以 重點(diǎn)要View#measure方法。這里需要注意的是整個(gè)View的設(shè)計(jì)體系里面一些主要的邏輯流程是不允許子類override的,可定制的部分作被動(dòng)式的方法嵌入在主要邏輯流程中,如measure是不能被override的,它會(huì)調(diào)用可以被子類override的onMeasure。onMeasure是每個(gè)View必須實(shí)現(xiàn)的方法,用傳入的父布局的約束來(lái)計(jì)算出自已的大小。
為了優(yōu)化measure流程,還有一個(gè)cache機(jī)制,用從父布局傳入的MeasureSpec作為key,從onMeasure得出的結(jié)果 作為value,保存在cache中,當(dāng)后面再次調(diào)用measure時(shí),如果MeasureSpec未發(fā)生變化,那么就直接從cache中取出結(jié)果,如果 有變化 那么再調(diào)用onMeasure去計(jì)算一次。光看View#measure和onMeasure這兩個(gè)方法也沒(méi)啥啊,或者說(shuō)常見的view或者我們自己定義的view的onMeasure方法也沒(méi)啥啊,都不算太復(fù)雜,有同學(xué)就會(huì)問(wèn),這里為啥這么費(fèi)勁 非要搞出一個(gè)cache呢?這個(gè)也好理解,要明白任何一個(gè)view不光是你自己,還涉及到所有你的子view啊,如果你只是一個(gè)未端的view(葉子),那當(dāng)然 無(wú)所謂了,但如果是一個(gè)ViewGroup,下面有很多個(gè)子view,那么 如果能少調(diào)用一次onMeasure,還是能節(jié)省不少CPU資源的。
ViewGroup的onMeasure
每個(gè)View的本身的onMeasure并不復(fù)雜,只需要關(guān)注好本身的尺寸就好了。
復(fù)雜的在于ViewGroup的onMeasure,簡(jiǎn)單來(lái)理解也并不復(fù)雜,它除了需要測(cè)量自己的寬與高之外,還需要逐個(gè)遍歷子view以measure子view。如果ViewGroup自身是EACTLY的,那么onMeasure過(guò)程就會(huì)簡(jiǎn)單不少,因?yàn)樗陨淼膶捙c高是確定的,只需要挨個(gè)measure子View就可了,而且子View并不影響它本身。當(dāng)然,要把padding和margin考慮進(jìn)來(lái)。
最為復(fù)雜的就是AT_MOST,ViewGroup自身的寬與高是由其所有子View決定的,這才是最復(fù)雜的,也是各個(gè)ViewGroup子類布局器需要重點(diǎn)解決的,而且過(guò)程各不相同,因?yàn)槊總€(gè)布局器的特點(diǎn)不一樣,所以過(guò)程并不相同,下面來(lái)各自討論一下。
幾種常見的ViewGroup的measure邏輯
下來(lái)來(lái)看一下一些非常常見的ViewGroup是如何measure的:
LinearLayout
它的方向只有兩個(gè),可以只分析一個(gè)方向,另外一個(gè)方向是差不多的,我們就看看measureVertical。
第1種情況,也就是height mode是EXACTLY的時(shí)候,這個(gè)時(shí)候LinearLayout布局本身的高度是已知的,挨個(gè)遍歷子view然后measure一下就可以。
第2種情況,比較復(fù)雜的情況,是AT_MOST時(shí),這其實(shí)也還好,理論上高度就是所有子view的高度之和。
對(duì)于LinearLayout,最為復(fù)雜的情況是處理weight,這需要很多復(fù)雜處理,要把剩余所有的空間按weight來(lái)分配,具體比較復(fù)雜,有興趣的可以具體去看源碼。這也說(shuō)明了,為何在線性布局中使用weight會(huì)影響性能,代碼中就可以看出當(dāng)有weight要處理的時(shí)候,至少多遍歷一遍子view以進(jìn)行相關(guān)的計(jì)算。
雖然方向是VERTICAL時(shí),重點(diǎn)只處理垂直方向,但是width也是需要計(jì)算的,但width的處理就要簡(jiǎn)單得多,如果其是EXACTLY的,那么就已知了;如果是AT_MOST的,就要找子view中width的最大值。
FrameLayout
FrameLayout其實(shí)是最簡(jiǎn)單的一個(gè)布局管理器,因?yàn)樗鼘?duì)子view是沒(méi)有約束的,無(wú)論水平方向還是垂直方向,對(duì)子view都是沒(méi)有約束,所以它的measure過(guò)程最簡(jiǎn)單。
如果是EXACTLY的,它本身的高度與寬度是確定的,那么就遍歷子view,measure一下就可以了,最后再把margin和padding加一下就完事了。
如果是AT_MOST的,那么也不難,遍歷子View并measure,然后取子view中最大寬為它的寬度,取最大的高為其高度,再加上margin和padding,基本上就做完了。
因?yàn)椋現(xiàn)rameLayout的measure過(guò)程最為簡(jiǎn)單,因此系統(tǒng)里很多地方默認(rèn)用的就是FrameLayout,比如窗口里的root view。
RelativeLayout
這個(gè)是最為復(fù)雜的,從設(shè)計(jì)的目的來(lái)看,RelativeLayout要解決的問(wèn)題也是提供了長(zhǎng)與寬兩個(gè)維度來(lái)約束子view。
總體過(guò)的過(guò)程就是要分別從vertical方向和horizontal方向,來(lái)進(jìn)行兩遍的measure,同時(shí)還要計(jì)算具體的坐標(biāo),實(shí)際上RelativeLayout的measure過(guò)程是把measure和layout一起做了。
自定義View如何實(shí)現(xiàn)onMeasure
如果是一個(gè)具體的View,那就相當(dāng)簡(jiǎn)單了,默認(rèn)的實(shí)現(xiàn)就可以了。
如果是ViewGroup會(huì)相對(duì)復(fù)雜一些,取決于如何從水平和垂直方向上約束子view,然后進(jìn)行遍歷,并把約束考慮進(jìn)去。可以參考LinearLayout和RelativeLayout的onMeasure實(shí)現(xiàn)。
渲染之layout
measure是確定控件的尺寸,下一步就是layout,也就是對(duì)控件進(jìn)行排列。
首先,需要理解現(xiàn)代GUI窗口的坐標(biāo)系統(tǒng),假設(shè)屏幕高為height,寬為width,那么屏幕左上角為坐標(biāo)原點(diǎn)(0,0),右下角為(width, height),屏幕從上向下為Y軸方向,從左向右則是X軸方向。安卓當(dāng)中,也是如此。每一個(gè)控件都是一個(gè)矩形區(qū)域,為了能知道如何渲染每一塊矩形(每 一個(gè)控件)就需要知道它的坐標(biāo),在前一步measure中,能知道它的寬與高,如果再能確定它的起始坐標(biāo)左上角,那么它在整個(gè)屏幕中的位置就可以確定了。
對(duì)于Android來(lái)說(shuō),view的渲染的第二步驟就是layout,其目的就是要確定好它的坐標(biāo),每一個(gè)View都有四個(gè)變量mLeft, mTop,mRight和mBottom,(mLeft, mTop)是它的左上角,(mRight, mBottom)是它的右下角,很明顯width=mRight-mLeft,而height=mBottom-mTop。這些數(shù)值是相對(duì)于父布局來(lái)說(shuō)的,每個(gè)View都是存在于view tree之中,知道相對(duì)于父布局的數(shù)值就足夠在渲染時(shí)使用了,沒(méi)必要用相對(duì)屏幕的絕對(duì)數(shù)值,而且用相對(duì)父布局的坐標(biāo)數(shù)值再加上父布局的坐標(biāo),就可以得到在屏幕上的絕對(duì)數(shù)值,如果需要這樣做的話。
[圖片上傳失敗...(image-be2431-1687781701075)]
layout過(guò)程依然是從根節(jié)點(diǎn)開始的,所以仍要從ViewRootImpl#performLayout作為起點(diǎn)來(lái)理順layout的邏輯。performLayout的參數(shù)是一個(gè)LayoutParam,以及一個(gè)windowWidth和desiredWindowHeight,調(diào)用performLayout是在performTraversal當(dāng)中,在做完performMeasure時(shí),傳入的參數(shù)其實(shí)就是窗口window的寬與高(因?yàn)楫吘故歉?jié)點(diǎn)嘛)。performLayout中會(huì)從根節(jié)點(diǎn)mView開開對(duì)整個(gè)view tree進(jìn)行l(wèi)ayout,其實(shí)就是調(diào)用mView.layout,傳入的是0, 0和view的經(jīng)過(guò)measure后寬與高。
單個(gè)View的layout方法實(shí)現(xiàn)較簡(jiǎn)單,把傳入的參數(shù)保存到mLeft,mTop,mRight和mBottom變量,再調(diào)用onLayout就完事了,這個(gè)很好理解,因?yàn)樽觱iew是由父布局確定好的位置,只要在measure過(guò)程把自己需要的大小告訴父布局后,父布局會(huì)根據(jù)LayoutParam做安排,傳給子view的就是計(jì)算過(guò)后的結(jié)果,每個(gè)子view記錄一下結(jié)果就可以了,不需要做啥額外的事情。
ViewGroup稍復(fù)雜,因?yàn)樗幚砥渥觱iew,并且要根據(jù)其設(shè)計(jì)的特點(diǎn)對(duì)子view進(jìn)行約束排列。還是可以看看常見的三個(gè)ViewGroup是如何做layout的。
LinearLayout
依然是兩個(gè)方向,因?yàn)長(zhǎng)inearLayout的目的就是在某一個(gè)方向上對(duì)子view進(jìn)行約束。看layoutVertical就可以了,水平方向上邏輯是一樣的。
遍歷一次子View即可,從父布局的left, top起始,考慮子view的height 以及上下的padding和margin,依次排列就可以了。需要注意的是,對(duì)于left的處理,理論上子view的left就應(yīng)該等于父布局,因?yàn)檫@畢竟是vertical的,水平上是沒(méi)有約束的,但是也要考慮Gravity,當(dāng)然也要把padding和margin考慮進(jìn)來(lái)。最后通過(guò)setChildFrame把排列好的坐標(biāo)設(shè)置給子view。
總體來(lái)看,線性布局的layout過(guò)程比其measure過(guò)程要簡(jiǎn)單不少。
FrameLayout
FrameLayout對(duì)子view的排列其實(shí)是沒(méi)有約束的,所以layout過(guò)程也不復(fù)雜,遍歷子view,子view的left和top初始均為父布局,依據(jù)其Gravity來(lái)做一下排布即可,比如如果Gravity是right,那么子view就要從父布局的右側(cè)開始計(jì)算,childRight=parentRight-margin-padding,childLeft=childRight-childWidth,以次類推,還是比較好理解的。
RelativeLayout
前面提到過(guò)RelativeLayout是在measure的時(shí)候就把坐標(biāo)都計(jì)算好了,它的layout就是把坐標(biāo)設(shè)置給子view,其余啥也沒(méi)有。
自定義View如何實(shí)現(xiàn)onLayout
如果是自定義View的話,不需要做什么。
如果是自定義的ViewGroup的話,要看設(shè)計(jì)的目的,是如何排列子view的。
總之,layout過(guò)程相較measure過(guò)程還是比較好理解的,約束規(guī)則越復(fù)雜的view,其measure過(guò)程越復(fù)雜,但layout過(guò)程卻不復(fù)雜。
渲染之draw
draw是整個(gè)渲染過(guò)程的核心也是最復(fù)雜的一步,前面的measure和layout只能算作準(zhǔn)備,draw才會(huì)真正進(jìn)行繪制。
draw的整個(gè)邏輯流程
與measure和layout的過(guò)程非常不一樣,雖然在performTraversals中也會(huì)調(diào)用performDraw,也就是說(shuō)看似draw流程的起點(diǎn)仍是ViewRootImpl#performDraw,但查看一下這個(gè)方法的實(shí)現(xiàn)就可以發(fā)現(xiàn),這里面其實(shí)并沒(méi)有調(diào)用到View#draw,就是說(shuō)它其實(shí)也是做一些準(zhǔn)備工作,整個(gè)View tree的draw觸發(fā),并不在這里。
從performDraw中并沒(méi)有做直接與draw相關(guān)的事情,它會(huì)調(diào)用另外一個(gè)方法draw()來(lái)做此事情,在draw方法中,它會(huì)先計(jì)算需要渲染的區(qū)域(dirty區(qū)域),然后再針對(duì) 此區(qū)域做渲染,正常情況下會(huì)走硬件加速方式去渲染,這部分比較復(fù)雜,它直接與一個(gè)叫做ThreadedRenderer打交道,稍后再作分析。
由于各種原因,假如硬件加速未沒(méi)有成功,那么會(huì)走到軟件渲染,這部分邏輯相對(duì)清晰一些,可以先從這里看起,會(huì)直接調(diào)用到drawSoftware(),這個(gè)方法有助于我們看清楚渲染的流程。這個(gè)方法里面會(huì)創(chuàng)建一個(gè)Canvas對(duì)象,是由ViewRootImpl持有的一個(gè)Surface對(duì)象中創(chuàng)建出來(lái)的,并調(diào)用view tree根節(jié)點(diǎn)的mView.draw(canvas),由此便把流程轉(zhuǎn)移到了view tree上面。
view tree的draw的過(guò)程
ViewRootImpl是直接調(diào)用根節(jié)點(diǎn)的draw方法,那么這里便是整個(gè)view tree的入口。可先從View#draw(canvas)方法看起。主要分為四步:1)畫背景drawBackground;2)畫自己的內(nèi)容通過(guò)onDraw來(lái)委派,具體的內(nèi)容是在onDraw里面做的;3)畫子view,通過(guò)dispatchDraw方法;4)畫其他的東西,如scroll bar或者focus highlight等??梢灾攸c(diǎn)關(guān)注一下這些操作的順序,先畫背景,然后畫自己,然后畫子view,最后畫scroll bar和focus之類的東西。
重點(diǎn)來(lái)看看dispatchDraw方法,因?yàn)槠渌麕讉€(gè)都相對(duì)非常好理解,這個(gè)方法主要要靠ViewGroup來(lái)實(shí)現(xiàn),因?yàn)樵赩iew里面它是空的,節(jié)點(diǎn)自己只需要管自己就可以了,只有父節(jié)點(diǎn)才需要關(guān)注如何畫子View。ViewGroup#dispatchDraw這個(gè)方法做一些準(zhǔn)備工作,如把padding考慮進(jìn)來(lái)并進(jìn)行clip,后會(huì)遍歷子View,針對(duì) 每個(gè)子view調(diào)用drawChild方法,這實(shí)際上就 是調(diào)用回了View#draw(canvas,parent,drawingTime)方法,注意這個(gè)方法是package scope的,也就是說(shuō)只能供view框架內(nèi)部調(diào)用。這個(gè)方法并沒(méi)有做具體的渲染工作(因?yàn)槊總€(gè)View的具體渲染都是在onDraw里面做的),這個(gè)方法里面做了大量與動(dòng)畫相關(guān)的各種變換。
Canvas對(duì)象是從哪里來(lái)的
View的渲染過(guò)程其實(shí)大都是GUI框架內(nèi)部的邏輯流程控制,真正涉及graphics方面的具體的圖形如何畫出來(lái),其實(shí)都是由Canvas對(duì)象來(lái)做的,比如如何畫點(diǎn),如何畫線,如何畫文字,如何畫圖片等等。一個(gè)Canvas對(duì)象從ViewRootImpl傳給View tree,就在view tree中一層一層的傳遞,每個(gè)view都把其想要展示的內(nèi)容渲染到Canvas對(duì)象中去。
那么,這個(gè)Canvas對(duì)象又是從何而來(lái)的呢?從view tree的一些方法中可以看到,都是從外面?zhèn)鬟M(jìn)來(lái)的,view tree的各個(gè)方法(draw, dipsatchDraw和drawChild)都只接收Canvas對(duì)象,但并不創(chuàng)建它。
從上面的邏輯可以看到Canvas對(duì)象有二個(gè)來(lái)源:一是在ViewRootImpl中創(chuàng)建的,當(dāng)走軟件渲染時(shí),會(huì)用Surface創(chuàng)建出一個(gè)Canvas對(duì)象,然后傳給view tree。從ViewRootImpl的代碼來(lái)看,它本身就會(huì)持有一個(gè)Surface對(duì)象,大概的邏輯就是每一個(gè)Window對(duì)象內(nèi),都會(huì)有一個(gè)用來(lái)渲染的Surface;
另外一個(gè)來(lái)源就是走硬件加速時(shí),會(huì)由hwui創(chuàng)建出Canvas對(duì)象。
draw過(guò)程的觸發(fā)邏輯
從上面的討論中可以看出draw的觸發(fā)邏輯有兩條路:
一是,沒(méi)有啟用硬件加速時(shí),走的軟件draw流程,也是一條比較好理解的簡(jiǎn)單流程:performTraversal->performDraw->draw->drawSoftware->View#draw。
二是,啟用了硬件加速時(shí),走的是performTraversal->performDraw->draw->ThreadedRenderer#draw,到這里就走進(jìn)了硬件加速相關(guān)的邏輯了。
硬件加速
硬件加速是從Android 4.0開始支持的,在此之前都是走的軟件渲染,也就是從ViewRoot(4.0版本以前是叫ViewRoot,后來(lái)才是ViewRootImpl)中持有的Surface直接創(chuàng)建Canvas,然后傳給view tree去做具體的渲染,與前面提到的drawSoftware過(guò)程類似。
硬件加速則要復(fù)雜得多,多了好多東西,它又搞出了一套渲染架構(gòu),但這套東西是直接與GPU聯(lián)系,有點(diǎn)類似于OpenGL,把view tree的渲染轉(zhuǎn)換成為一系列命令,直接傳給GPU,軟件渲染則是需要CPU把所有的運(yùn)算都做了,最終生成graphic buffer送給屏幕(當(dāng)然也是GPU)。
這一坨東西中最為核心就是RenderNode和RecordingCanvas。其中RenderNode是純新的東西,它是為了構(gòu)建 一個(gè)render tree(類似于view tree),用以構(gòu)建復(fù)雜的渲染邏輯關(guān)系。RecordingCanvas是Canvas的一個(gè)子類,它是專門用于硬件加速渲染的,但又為了兼容老的Canvas(軟件渲染),為啥叫recording呢?因?yàn)橛布铀俜绞戒秩荆瑢?duì)于view tree的draw過(guò)程來(lái)說(shuō)就是記錄一系列的操作,這其實(shí)就是給GPU的指令,渲染的最后一步就是把整個(gè)render tree丟給GPU,就完了。
前面說(shuō)的兩個(gè)是數(shù)據(jù)結(jié)構(gòu),還不夠,還有HardwareRenderer和ThreadedRenderer,這兩個(gè)用來(lái)建立和管理render tree的,也就是說(shuō)它們內(nèi)部管理著一組由RenderNode組成的render tree,并且做一些上下文環(huán)境的初始化與清理資源的工作。類似于OpenGL中GLSurfaceView的RenderThread做的事情。
硬件加速與原框架的切入點(diǎn)都是RenderNode和RecordingCanvas,View類中多了一個(gè)RenderNode成員,當(dāng)draw的時(shí)候,從RenderNode中得到RecordingCanvas,其余操作都與原來(lái)一致,都是調(diào)用Canvas的方法進(jìn)行g(shù)raphics的繪制,這樣整體渲染流程就走入到了硬件加速里面。
Choreographer與vsync
雖然在Android 4.0版本加入了硬件加速的支持,但這還是不夠,因?yàn)樗皇窍喈?dāng)于具體的渲染時(shí)間可能快了一些,舉例來(lái)說(shuō),可能是普通火車與高鐵之間的差異,雖然確實(shí)行程所花時(shí)間變短了,但是對(duì)于整體的效率來(lái)說(shuō)提升并不大。對(duì)于整體GUI的流暢度,響應(yīng)度,特別是動(dòng)畫這一塊的流程程度與其他平臺(tái)(如水果)差距仍是巨大的。一個(gè)最重要的原因就在于,GUI整體的渲染流程是缺少協(xié)同的,仍是按需式渲染:應(yīng)用層布局加載完了要渲染了,或者ViewRootImpl發(fā)現(xiàn)dirty了,需要重繪了,或者有用戶事件了需要響應(yīng)了,觸發(fā)整體渲染流程,更新graphic buffer,屏幕刷新了。
這一過(guò)程其實(shí)也沒(méi)有啥大問(wèn)題,對(duì)于常規(guī)的UI顯示,沒(méi)有問(wèn)題,我沒(méi)有更新,沒(méi)有變化 ,當(dāng)然 不需要重繪了,如果有更新有變化時(shí)再按需重新渲染,這顯然 沒(méi)有什么問(wèn)題。最大的問(wèn)題在于動(dòng)畫,動(dòng)畫是要求連續(xù)不停的重繪,如果僅靠客戶這一端(相較于graphic buffer和屏幕這一端來(lái)說(shuō))來(lái)觸發(fā),顯然FPS(幀率)是不夠的,由此造成流暢度肯定不夠好。
于是在Android 4.1 (Jelly Bean)中就引入了Choreographer以及vsync機(jī)制,來(lái)解決此問(wèn)題,它們兩個(gè)并不全完是一回事,Choreographer是純軟件的,vsync則是更為復(fù)雜的更底層的機(jī)制,有沒(méi)有vsync,Choreographer都能很好的工作,只不過(guò)有了vsync會(huì)更好,就好比硬件加速之于View的渲染,沒(méi)有硬件加速也可以渲染啊,有了硬件加速渲染會(huì)更加的快一些。
[圖片上傳失敗...(image-9787e-1687781701075)]
Choreographer
它的英文本意是歌舞的編舞者,有點(diǎn)類似于導(dǎo)演,但歌舞一般時(shí)間更短,所以對(duì)編舞者要求更高,需要在短時(shí)間內(nèi)把精華全部展現(xiàn)出來(lái)。它的目的就是要協(xié)調(diào)整個(gè)View的渲染過(guò)程,對(duì)輸入事件響應(yīng),動(dòng)畫和渲染進(jìn)行時(shí)間上的把控。文檔原文是說(shuō):Coordinates the timing of animations, input and drawing.,精華就在于timing這個(gè)詞上。
但其實(shí),這個(gè)類本身并不是很復(fù)雜,相較于其他frameworks層的東西來(lái)說(shuō)它算簡(jiǎn)單的了,它就是負(fù)責(zé)定時(shí)回調(diào),按照一定的FPS來(lái)給你回調(diào),簡(jiǎn)單來(lái)說(shuō)它就是做了這么一件事情。它公開的接口也特別少,就是postFrameCallback和removeFrameCallback,而FrameCallback也是一個(gè)非常簡(jiǎn)單的接口doFrame(long frameTimeNanos),里面的參數(shù)是當(dāng)前幀開始渲染的時(shí)間序列。
所以,它的工作就是在計(jì)時(shí),或者叫把控時(shí)間,到了每一幀該渲染的時(shí)候了,它會(huì)告訴你。有了它,那么GUI的渲染將不再是按需重繪了,而是有節(jié)奏的,可以以固定FPS定時(shí)刷新。ViewRootImpl那頭也需要做調(diào)整,每當(dāng)有主動(dòng)重繪時(shí)(view tree有變化,用戶有輸入事件等),也并不是說(shuō)立馬就去做draw,而是往Choreographer里post一個(gè)FrameCallback,在里面做具體的draw。
vsync(Vertical Synchronization)
垂直同步,是另外一套更為底層的機(jī)制,簡(jiǎn)單來(lái)理解就是由屏幕顯示系統(tǒng)直接向軟件層派發(fā)定時(shí)的脈沖信號(hào),用以提高整體的渲染流暢程度,屏幕刷新,graphic buffer和window GUI(view tree)三者在這個(gè)脈沖信號(hào)下,做到同步。
vsync是通過(guò)對(duì)Choreographer來(lái)發(fā)揮作用的。Choreographer有兩套timing機(jī)制,一是靠它自己實(shí)現(xiàn)的一套,另外就是直接傳導(dǎo)vsync的信號(hào)。通過(guò)DisplayEventReceiver(這個(gè)類對(duì)于App層是完全不可見的被hide了)就可以接收到vsync的信號(hào)了,調(diào)用其sheduleVsync來(lái)告訴vsync說(shuō)我想接收下一次同步的信號(hào),然后在重載onVsync方法以接收信號(hào),就能夠與vsync系統(tǒng)連接起來(lái)了。
渲染性能優(yōu)化
這是一個(gè)很大的話題
保持簡(jiǎn)單
最最重要的原則就是要保持簡(jiǎn)單,比如,UI頁(yè)面盡可能的簡(jiǎn)潔,view tree的層級(jí)要盡可能的少,能用顏色就別用背景圖片,能merge就merge。
動(dòng)畫也要盡可能的簡(jiǎn)單,并且使用標(biāo)準(zhǔn)的ValueAnimator接口,而不要簡(jiǎn)單粗暴的去修改LayoutParams(如height和width)。
減少重繪
這個(gè)要多用系統(tǒng)中開發(fā)者模式里面的重繪調(diào)試工具來(lái)做優(yōu)化,盡可能的減少重繪。
專項(xiàng)定制
有些時(shí)候,對(duì)于一些特殊需求的view要進(jìn)行定制優(yōu)化。舉個(gè)例子,比如一個(gè)巨復(fù)雜的頁(yè)面(如某寶的首頁(yè)),中有一個(gè)用于顯示倒計(jì)時(shí)的view,實(shí)現(xiàn)起來(lái)并不復(fù)雜,一個(gè)TextView就搞定了,一個(gè)Timer來(lái)倒計(jì)時(shí),不斷的刷新數(shù)字 就可以了。但是,這通常會(huì)導(dǎo)致整個(gè)頁(yè)面都跟著在重繪。因?yàn)閿?shù)字在變化,會(huì)導(dǎo)致TextView的大小在變化,進(jìn)而導(dǎo)致整個(gè)View tree都在不斷的跟著重繪。
像這種case,如果遇到了,就需要自定義一個(gè)專門用于此的View,并針對(duì)數(shù)字不斷刷新做專門的優(yōu)化,以不讓其影響整個(gè)view tree。
不要在意這個(gè)例子的真實(shí)性,要知道,當(dāng)某個(gè)View演變成了整個(gè)頁(yè)面的瓶頸的時(shí)候,就需要專門針對(duì) 其進(jìn)行特殊定制以優(yōu)化整體頁(yè)面的渲染性能。
更多的技巧可以參考這篇文章和后面的參考資料。
參考資料
列舉一下關(guān)于此話題的比較好的其他資源
- Android視圖繪制流程完全解析,帶你一步步深入了解View
- Android性能優(yōu)化第(四)篇---Android渲染機(jī)制
- 深入Android渲染機(jī)制
- Android進(jìn)階——性能優(yōu)化之布局渲染原理和底層機(jī)制機(jī)詳解及卡頓根源探究(四)
- View渲染機(jī)制
- Android屏幕刷新機(jī)制
- Android 基于 Choreographer 的渲染機(jī)制詳解
- Android圖形渲染之Choreographer原理