前言
我們都知道Android的View的工作流程主要是指measure、layout、draw這三大流程数初,即測(cè)量、布局和繪制梗顺,其中measure確定View的測(cè)量寬高妙真,layout根據(jù)測(cè)量的寬高確定View在其父View中的四個(gè)頂點(diǎn)的位置,而draw則將View繪制到屏幕上荚守,這樣通過ViewGroup的遞歸遍歷珍德,一個(gè)View樹就展現(xiàn)在屏幕上了。
Android的視圖結(jié)構(gòu)
Window的基本概念
Window表示的是一個(gè)窗口的概念矗漾,它是站在WindowManagerService角度上的一個(gè)抽象的概念锈候,Android中所有的視圖都是通過Window來呈現(xiàn)的,不管是Activity敞贡、Dialog還是Toast泵琳,只要有View的地方就一定有Window。
這里需要注意的是誊役,這個(gè)抽象的Window概念和PhoneWindow這個(gè)類并不是同一個(gè)東西获列,PhoneWindow表示的是手機(jī)屏幕的抽象,它充當(dāng)Activity和DecorView之間的媒介蛔垢,就算沒有PhoneWindow也是可以展示View的击孩。
拋開一切,僅站在WindowManagerService的角度上鹏漆,Android的界面就是由一個(gè)個(gè)Window層疊展現(xiàn)的巩梢,而Window又是一個(gè)抽象的概念,它并不是實(shí)際存在的艺玲,它是以View的形式存在括蝠,這個(gè)View就是DecorView。
DecorView的概念
DecorView是整個(gè)Window界面的最頂層View饭聚,View的測(cè)量忌警、布局、繪制秒梳、事件分發(fā)都是由DecorView往下遍歷這個(gè)View樹法绵。DecorView作為頂級(jí)View,一般情況下它內(nèi)部會(huì)包含一個(gè)豎直方向的LinearLayout端幼,在這個(gè)LinearLayout里面有上下兩個(gè)部分(具體情況和Android的版本及主題有關(guān))礼烈,上面是【標(biāo)題欄】,下面是【內(nèi)容欄】婆跑。在Activity中我們通過setContentView所設(shè)置的布局文件其實(shí)就是被加載到【內(nèi)容欄】中的此熬,而內(nèi)容欄的id是content,因此指定布局的方法叫setContent().
ViewRoot的概念
ViewRoot對(duì)應(yīng)于ViewRootImpl類,它是連接WindowManager和DecorView的紐帶犀忱,View的三大流程均是通過ViewRoot來完成的募谎。在ActivityThread中,當(dāng)Activity對(duì)象被創(chuàng)建完之后阴汇,會(huì)將DecorView添加到Window中数冬,同時(shí)會(huì)創(chuàng)建對(duì)應(yīng)的ViewRootImpl,并將ViewRootImpl和DecorView建立關(guān)聯(lián)搀庶,并保存到WindowManagerGlobal對(duì)象中拐纱。
View的繪制流程是從ViewRoot的performTraversals方法開始的,它經(jīng)過measure哥倔、layout和draw三個(gè)過程才能最終將一個(gè)View繪制出來秸架,大致流程如下圖:
一、Activity的啟動(dòng)流程
所有Android應(yīng)用都是由多個(gè)Activity和Fragment組成咆蒿,而一個(gè)頁面的承載都是由Activity去承載东抹。我們知道了上訴幾個(gè)概念之后,就可以來分析一下Activity是怎么由創(chuàng)建到UI全部渲染出來的過程沃测。下午是我閱讀源碼分析得出的Activity時(shí)序圖缭黔,有興趣的可以自己去看,文章里不再貼代碼蒂破。
從上面的時(shí)序圖馏谨,可以得知,渲染的最關(guān)鍵一步是通過WindowManager實(shí)例把之前創(chuàng)建的DecorView實(shí)例添加到根視圖中寞蚌,下面我們?cè)敿?xì)來看這塊的代碼的執(zhí)行過程:
可以得知Activity最后的渲染是通過ViewRootImpl來實(shí)現(xiàn)計(jì)算田巴、布局、繪制到屏幕挟秤。
二、UI的刷新
上一章節(jié)抄伍,我們知道了Activity的啟動(dòng)到頁面的整體UI展現(xiàn)到屏幕上的流程艘刚。那么Activity是怎么實(shí)現(xiàn)UI頁面的刷新的呢?
我們知道Android上的刷新無非3種方法invalidate()截珍、postInvalidate()攀甚、requestLayout(),而各種控件最后調(diào)用的刷新方式岗喉,也是通過這3種方法來實(shí)現(xiàn)秋度。
- invalidate()
postInvalidate()和invalidate()區(qū)別在于,invalidate()只能UI線程中調(diào)用钱床,而postInvalidate()可以在子線程中調(diào)用荚斯。postInvalidate()最后其實(shí)通過ViewRootImpl里的handler切換到UI線程,最終執(zhí)行invalidate()。invalidate()只會(huì)觸發(fā)視圖的onDraw()方法事期,而不會(huì)觸發(fā)onMeasure()滥壕、onLayout()。
所以我們只需要分析invalidate()即可兽泣,我們首先來看View中invalidate()的部分源碼:
public void invalidate() {
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
//invalidateCache 使繪制緩存失效
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
...
//設(shè)置了跳過繪制標(biāo)記
if (skipInvalidate()) {
return;
}
//PFLAG_DRAWN 表示此前該View已經(jīng)繪制過 PFLAG_HAS_BOUNDS表示該View已經(jīng)layout過绎橘,確定過坐標(biāo)了
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
//默認(rèn)true
mLastIsOpaque = isOpaque();
//清除繪制標(biāo)記
mPrivateFlags &= ~PFLAG_DRAWN;
}
//需要繪制
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
//1、加上繪制失效標(biāo)記
//2唠倦、清除繪制緩存有效標(biāo)記
//這兩標(biāo)記在硬件加速繪制分支用到
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
//記錄需要重新繪制的區(qū)域 damge称鳞,該區(qū)域?yàn)樵揤iew尺寸
damage.set(l, t, r, b);
//p 為該View的父布局
//調(diào)用父布局的invalidateChild
p.invalidateChild(this, damage);
}
...
}
}
從上可知,當(dāng)前要刷新的View確定了刷新區(qū)域后即調(diào)用了父布局的invalidateChild(xx)方法稠鼻。該方法為ViewGroup里的final方法冈止。
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
//1、如果是支持硬件加速枷餐,則走該分支
onDescendantInvalidated(child, child);
return;
}
//2靶瘸、軟件繪制
ViewParent parent = this;
if (attachInfo != null) {
//動(dòng)畫相關(guān),忽略
...
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
...
parent = parent.invalidateChildInParent(location, dirty);
//動(dòng)畫相關(guān)
} while (parent != null);
}
}
由上可知毛肋,在該方法里區(qū)分了硬件加速繪制與軟件繪制怨咪,分別來看看兩者區(qū)別。
硬件加速繪制分支
如果該Window支持硬件加速润匙,則走下邊流程:
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);
if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {
//此處都會(huì)走
mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;
//清除繪制緩存有效標(biāo)記
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
if (mLayerType == LAYER_TYPE_SOFTWARE) {
//如果是開啟了軟件繪制诗眨,則加上繪制失效標(biāo)記
mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
//更改target指向
target = this;
}
if (mParent != null) {
//調(diào)用父布局的onDescendantInvalidated
mParent.onDescendantInvalidated(this, target);
}
}
onDescendantInvalidated 方法的目的是不斷向上尋找其父布局,并將父布局PFLAG_DRAWING_CACHE_VALID 標(biāo)記清空孕讳,也就是繪制緩存清空匠楚。
而我們知道,根View的mParent指向ViewRootImpl對(duì)象厂财,因此來看看它里面的onDescendantInvalidated()方法芋簿。
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
// TODO: Re-enable after camera is fixed or consider targetSdk checking this
// checkThread();
if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
mIsAnimating = true;
}
invalidate();
}
@UnsupportedAppUsage
void invalidate() {
//mDirty 為臟區(qū)域,也就是需要重繪的區(qū)域
//mWidth璃饱,mHeight 為Window尺寸
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
//開啟View 三大流程
scheduleTraversals();
}
}
軟件繪制分支
如果該Window不支持硬件加速与斤,那么走軟件繪制分支:
parent.invalidateChildInParent(location, dirty) 返回mParent,只要mParent不為空那么一直調(diào)用invalidateChildInParent(xx)荚恶,實(shí)際上這也是遍歷ViewTree過程撩穿,來看看關(guān)鍵invalidateChildInParent(xx)
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
//dirty 為失效的區(qū)域,也就是需要重繪的區(qū)域
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
//該View繪制過或者繪制緩存有效
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
!= FLAG_OPTIMIZE_INVALIDATE) {
//修正重繪的區(qū)域
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
//如果允許子布局超過父布局區(qū)域展示
//則該dirty 區(qū)域需要擴(kuò)大
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
final int left = mLeft;
final int top = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
//默認(rèn)會(huì)走這
//如果不允許子布局超過父布局區(qū)域展示谒撼,則取相交區(qū)域
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
//記錄偏移食寡,用以不斷修正重繪區(qū)域,使之相對(duì)計(jì)算出相對(duì)屏幕的坐標(biāo)
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
} else {
...
}
//標(biāo)記緩存失效
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
if (mLayerType != LAYER_TYPE_NONE) {
//如果設(shè)置了緩存類型廓潜,則標(biāo)記該View需要重繪
mPrivateFlags |= PFLAG_INVALIDATED;
}
//返回父布局
return mParent;
}
return null;
}
與硬件加速繪制一致抵皱,最終調(diào)用ViewRootImpl invalidateChildInParent(xx)善榛,來看看實(shí)現(xiàn)
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
if (dirty == null) {
//臟區(qū)域?yàn)榭眨瑒t默認(rèn)刷新整個(gè)窗口
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
...
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
//合并臟區(qū)域叨叙,取并集
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
//開啟View的三大繪制流程
scheduleTraversals();
}
}
-
requestLayout()
顧名思義锭弊,重新請(qǐng)求布局。來看看View.requestLayout()方法
public void requestLayout() {
//清空測(cè)量緩存
if (mMeasureCache != null) mMeasureCache.clear();
...
//添加強(qiáng)制layout 標(biāo)記擂错,該標(biāo)記觸發(fā)layout
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
//添加重繪標(biāo)記
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
//如果上次的layout 請(qǐng)求已經(jīng)完成
//父布局繼續(xù)調(diào)用requestLayout
mParent.requestLayout();
}
...
}
可以看出味滞,這個(gè)遞歸調(diào)用和invalidate一樣的套路,向上尋找其父布局钮呀,一直到ViewRootImpl為止剑鞍,給每個(gè)布局設(shè)置PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED標(biāo)記。查看ViewRootImpl requestLayout()
public void requestLayout() {
//是否正在進(jìn)行l(wèi)ayout過程
if (!mHandlingLayoutInLayoutRequest) {
//檢查線程是否一致
checkThread();
//標(biāo)記有一次layout的請(qǐng)求
mLayoutRequested = true;
//開啟View 三大流程
scheduleTraversals();
}
}
很明顯爽醋,requestLayout目的很單純:
- 1蚁署、向上尋找父布局、并設(shè)置強(qiáng)制layout標(biāo)記
- 2蚂四、最終開啟三大繪制流程
和invalidate()一樣的配方光戈,當(dāng)刷新信號(hào)來到之時(shí),調(diào)用doTraversal()->performTraversals()遂赠,而在performTraversals()里真正執(zhí)行三大流程久妆。
private void performTraversals() {
//mLayoutRequested 在requestLayout時(shí)賦值為true
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
//measure 過程
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
...
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
//layout 過程
performLayout(lp, mWidth, mHeight);
}
...
}
由此可見:
- 1、requestLayout 最終將會(huì)觸發(fā)Measure跷睦、Layout 過程筷弦。
- 2、由于沒有設(shè)置重繪區(qū)域抑诸,因此Draw 過程將不會(huì)觸發(fā)烂琴。
之前設(shè)置的PFLAG_FORCE_LAYOUT標(biāo)記有啥用呢?回憶一下measure 過程:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
//requestLayout時(shí)蜕乡,PFLAG_FORCE_LAYOUT 標(biāo)記被設(shè)置
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
...
if (forceLayout || needsLayout) {
...
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//測(cè)量
onMeasure(widthMeasureSpec, heightMeasureSpec);
} else {
...
}
...
}
}
}
PFLAG_FORCE_LAYOUT 標(biāo)記打上之后奸绷,會(huì)觸發(fā)onMeasure()測(cè)量自身及其子布局。
試想一下层玲,假設(shè)View的尺寸改變了健盒,變大了,那么調(diào)用了requestLayout后因?yàn)樽吡薓easure称簿、Layout 過程,測(cè)量惰帽、擺放倒是重新設(shè)置了憨降,但是不調(diào)用Draw出不來效果啊。實(shí)際上该酗,View layout時(shí)候已經(jīng)考慮到了授药。在View.layout(xx)->setFrame(xx)里
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
...
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
//尺寸發(fā)生改變 調(diào)用invalidate 傳入true士嚎,否則傳入false
invalidate(sizeChanged);
...
}
...
return changed;
}
也就是說:
- 1、requestLayout調(diào)用后悔叽,可能會(huì)觸發(fā)invalidate莱衩。
- 2、若是觸發(fā)了invalidate()娇澎,不管傳入true還是false笨蚁,都會(huì)走重繪流程。
總結(jié)
- invalidate調(diào)用后只會(huì)觸發(fā)Draw 過程趟庄。
- requestLayout 會(huì)觸發(fā)Measure括细、Layout過程,如果尺寸發(fā)生改變戚啥,則會(huì)調(diào)用invalidate奋单。
- 當(dāng)涉及View的尺寸、位置變化時(shí)使用requestLayout猫十。
- 當(dāng)僅僅需要重繪時(shí)調(diào)用invalidate览濒。
- 如果不確定requestLayout 是否觸發(fā)invalidate,可在requestLayout后繼續(xù)調(diào)用invalidate拖云。
三贷笛、自定義ViewGroup的onDraw為什么不走?
由前面的幾節(jié)內(nèi)容可以知道江兢,所有的視圖的繪制最終都會(huì)經(jīng)過ViewRootImpl來實(shí)現(xiàn)昨忆,最后都會(huì)走到View的onDraw方法里,下面我們?cè)敿?xì)來看源碼:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);//如果有背景色杉允,走onDraw()方法邑贴,如果沒有背景色,不走onDraw()方法
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
......
}
從代碼的注釋中能清楚的看到繪制的順序:
1叔磷、畫背景
2拢驾、畫canvas的圖層(非必須)
3、畫View自己
4改基、畫子View
5繁疤、如果2執(zhí)行了,這步要回復(fù)圖層(非必須)
這個(gè)并不是重點(diǎn)秕狰,下面的這塊代碼才是重點(diǎn):
//如果有背景色稠腊,走onDraw()方法,如果沒有背景色鸣哀,不走onDraw()方法
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
總結(jié)
1架忌、自定義ViewGroup的onDraw()方法需要設(shè)置背景才會(huì)調(diào)用
2、自定義ViewGroup正常情況下應(yīng)該實(shí)現(xiàn)dispatchDraw()