ViewRoot和Decor ViewRoot对应与VieRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程(Measure、Layout和Draw)均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
DecorView作为顶级View,一般情况下它会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(具体情况跟Android版本及主体Theme有关),上面为标题栏,如下图的TitleBar,下面为内容懒,如下图的ContentView。通过源码我们可以知道,DecorView其实是一个FrameLayotu,View层的事件都先经过DecorView,然后才传递给我们的View。
PhoneWindow为Window类的实现类,DecorView即顶级View,是PhoneWindow的子类。
View树的绘制机制 当Activity接收到焦点的时候,它会被请求绘制布局,该请求由Android Framework处理,绘制是从根节点开始,对布局树进行measure和draw。整个View树的绘图流程在ViewRoot.java 类中的**performTraversal()**函数展开,该方法所做的工作可简单概括为以下几点:
是否需要重新计算视图大小(measure)
是否需要重新安置视图的位置(layout)
是否需要重新绘制视图(draw)
performTraversals这个方法会一次调用performMeasure、performLayout、performDraw三个方法,这三个方法分别完成顶级View(DecorView)的Measure 、Layout 和Draw 这三大流程,其中在 performMeasure会调用measure方法,measure方法又会调用onMeasure方法,在onMeasure方法中会对所有子视图进行measure过程,子视图重复父视图的measure过程,如此反复就完成了整个View树的遍历。同理,performLayout和performDraw是类似的,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的,不过没有本质区别。
从整体上来看 Measure 和 Layout 两个步骤的执行:
总的来说,树的遍历是有序的,由父视图到子视图,每一个 ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身。
View绘制流程函数调用链
如果用户主动调用requestLayout(),只会触发measure和layout过程而不会执行draw过程。
MeasureSpec和LayoutParams MeasureSpec和LayoutParams是Measure过程中传递尺寸的两个类,在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对象的MeasureSpec ,然后再根据这个MeasureSpec来测量出View的宽和高。
MeasureSpec MeasureSpec代表一个32位的int值,高2位代表SpecMode,测量模式,低30位代表SpecSize,规格尺寸。
SpecMode有三类,每一类都表示特殊的含义:
UNSPECIFIED
父容器不对View有任何限制,它可以达到所期望的任意尺寸,这种情况一般用于系统内部,如ListView、ScrollView,一般自定义View用不到。
EXACTLY
父视图为子视图指定一个确切的尺寸,而且无论子视图期望多大,它都必须在该指定大小的边界内,对应的属性为 match_parent 或具体值,比如 100dp,父控件可以通过**MeasureSpec.getSize(measureSpec)**直接得到子控件的尺寸。
AT_MOST
父视图为子视图指定一个最大尺寸SpecSize,子视图必须确保它自己所有子视图可以适应在该尺寸范围内,对应的属性为 wrap_content。这种模式下,父控件无法确定子 View 的尺寸,只能由子控件自己根据需求去计算自己的尺寸,这种模式就是我们自定义视图需要实现测量逻辑的情况。
ViewGroup.LayoutParams 这个类我们很常见,就是用来指定视图的高度和宽度等参数。对于每个视图的 height 和 width,你有以下选择:
具体值
MATCH_PARENT 表示子视图希望和父视图一样大(不包含 padding 值)
WRAP_CONTENT 表示视图为正好能包裹其内容大小(包含 padding 值)
MeasureSpec和LayoutParams的对应关系 在View测量的时候,系统会将LayoutParams在父容器的约束下转换成相应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽和高。注意,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec ,从而进一步决定View的宽和高。
对于顶级View(DecorView)和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和自身的LayoutParams来共同确定;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽和高了。
注意:
图上有一处错误,当parent指定子View的MeasureSpecMode为UNSPECIFIED时,子View的尺寸实际上是没有限制的,而不是0。
也就是说子View的大小可以超过父VIew,当然子View实际上不能超过父View的区域进行绘制,这种情况我们经常接触,对应RecyclerView或者ListView内部的情况。
RecyclerView或者ListView将子View的SpecMode设置为UNSPECIFIED,表示子View可以不必受父View限制来测量自身大小,当子View超过父View的大小时,RecyclerView/ListView通过滚动的方式将子View完整展示出来。
三大流程分析 Measure过程决定了View测量后的宽和高 ,注意是测量后的宽和高,某些情况下并不等同于最终的宽和高 。Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽和高。
Layout过程决定了View的四个顶点的坐标和实际的View的宽和高, 完成以后,可以通过getTop、getButton、getLeft和getRight来拿到View的四个顶点的位置,并可以通过getWidth和getHeight方法拿到最终View的宽和高。
Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。
Measure过程 Measure过程要分情况来看。 如果只是一个原始的View,那么通过**measure(int widthMeasureSpec, int heightMeasureSpec)**方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子视图的measure方法,各个子视图再递归地去执行这个流程。
View的Measure过程 View的Measure过程由其measure方法来完成,meausre方法是一个final方法,不能被重写,measure方法中会调用View的onMeasure方法,onMeasure方法源码如下:
1 2 3 4 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
该方法就是我们自定义View中需要重写来实现测量逻辑的方法。可以看到Measure过程最终方法是setMeasuredDimension()方法,该方法会设置View宽高的测量值,看看getDefaultSize()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; }
对于我们来说只需要关心AT_MOST和EXACTLY两种情况,可以看到,返回的结果其实就是measureSpec中的specSize,而这个specSize就是View测量后的大小。至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,这种情况下返回的是第一个参数size,也就是getSuggestedMinimumWidth()和getSuggestedMinimumHeight()两个函数的返回值。
以getSuggestedMinimumWidth()为例介绍一下这个默认值是怎么来的:
View没有设置背景:返回android:minWidth 这个属性所指定的值,这个值可以为0
View设置了背景:返回android:minWidth 与背景的最小宽度 两者的最大值
从getDefaultSize方法的实现来看,View的宽高由specSize决定,所以可以得出如下结论:
直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
ViewGroup的Measure过程 对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子视图的measure方法,各个子视图再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren的方法,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protected void measureChildren (int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0 ; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
可以看到,ViewGroup在measure时,会调用measureChild对每一个子视图进行measure,measureChild源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 protected void measureChild (View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }public static int getChildMeasureSpec (int spec, int padding, int childDimension) { ......... return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
这里验证了我们上面所说的,子视图的measure需要父容器的MeasureSpec和自身的LayoutParams,通过getChildMeasureSpec方法来得到自身的MeasureSpec,然后将MeasureSpec传递给子视图的measure方法来进行测量。
View的Measure过程是三大流程中最复杂的一个,Measure完成以后,通过getMeasuredWidth/getMeasuredHeight方法就可以正确地获取到View的测量宽高。但是,在某些极端情况下,系统可能需要多次Measure才能确定最终的测量宽高,所以,比较稳妥的方法是在onLayout方法中去获取View的测量宽高或最终宽高。
Layout过程 Layout的作用是ViewGroup用来确定子视图的位置(相对于父视图 ),当ViewGroup的位置被确定后,它在onLayout会遍历所有的子视图并调用其layout方法,在layout方法中onLayout方法又会被调用。layout方法是用来确定View本身的位置的,onLayout方法则是用来确认所有子视图的位置的。View的layout方法如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public void layout (int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0 ) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); if (shouldDrawRoundScrollbar()) { if (mRoundScrollbarRenderer == null ) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this ); } } else { mRoundScrollbarRenderer = null ; } mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null ) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0 ; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this , l, t, r, b, oldL, oldT, oldR, oldB); } } } final boolean wasLayoutValid = isLayoutValid(); mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; ··· ··· }protected boolean setFrame (int left, int top, int right, int bottom) { ... }
首先会通过setFrame方法来设定View的四个顶点的位置,从而确定View在父容器中的位置,接着会调用onLayout方法来确定子视图的位置。跟onMeasure方法一样,onLayout的实现也跟具体布局有关,所以View和ViewGroup都没有实现onLayout方法,都是交给子类去实现。不过我们可以看看LinearLayout的onLayout方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Override protected void onLayout (boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); } } void layoutVertical (int left, int top, int right, int bottom) { ··· for (int i = 0 ; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null ) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); ...确定 childLeft、childTop 的值 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); } } } private void setChildFrame (View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }
可以看到,layoutVertical方法会遍历所有子视图并调用serChildFrame方法来为子视图指定对应的位置,其中childTop会逐渐增大,刚好符合竖直方向LinearLayou的特性。而setChildFrame方法其实就是调用了子视图的layout方法,这样子视图就能通过layout方法来确定自己的位置,这样一层一层传递下去就完成了整个View树的Layout过程。
Draw过程 Draw过程比较简单,它的工作就是讲View绘制到平面上面。View的绘制过程遵循如下几步:
绘制背景background.draw(Canvas canvas)
绘制自己(onDraw)
绘制children(dispatchDraw)
绘制装饰(onDrawScrollBars)
通过源码可以清晰的看出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public void draw (Canvas canvas) { / * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background if need * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view' s content * 4. Draw children (dispatchDraw) * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ if (!dirtyOpaque) { drawBackground(canvas); } final int viewFlags = mViewFlags; if (!verticalEdges && !horizontalEdges) { if (!dirtyOpaque) onDraw(canvas); dispatchDraw(canvas); onDrawScrollBars(canvas); if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } return ; } ... if (!dirtyOpaque) onDraw(canvas); dispatchDraw(canvas); onDrawScrollBars(canvas); }
由上面的处理过程,我们也可以得出一些优化的小技巧:当不需要绘制 Layer 的时候第二步和第五步会跳过。因此在绘制的时候,能省的 layer 尽可省,可以提高绘制效率。
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子视图的draw方法,将draw事件传递。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 dispatchDraw(Canvas canvas){ ... if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { final boolean buildCache = !isHardwareAccelerated(); for (int i = 0 ; i < childrenCount; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { final LayoutParams params = child.getLayoutParams(); attachLayoutAnimationParameters(child, params, i, childrenCount); bindLayoutAnimation(child); if (cache) { child.setDrawingCacheEnabled(true ); if (buildCache) { child.buildDrawingCache(true ); } } } } final LayoutAnimationController controller = mLayoutAnimationController; if (controller.willOverlap()) { mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE; } controller.start(); } for (int i = 0 ; i < childrenCount; i++) { int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null ) ? children[childIndex] : preorderedList.get(childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null ) { more |= drawChild(canvas, child, drawingTime); } } ... }protected boolean drawChild (Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this , drawingTime); } boolean draw (Canvas canvas, ViewGroup parent, long drawingTime) { ... }
其他方法补充 View.setWillNotDraw(boolean willNotDraw) 当我们的自定义View继承于ViewGroup并且本身不需要进行绘制时,就开启(true)这个标志位以便于系统进行后续的优化。
View.invalidate() 请求重绘 View 树,即 draw 过程,假如视图大小没有变化就不会调用Layout过程,并且只绘制那些调用了invalidate()方法的 View。
View.requestLayout() 当布局变化的时候,比如方向变化,尺寸的变化,会调用该方法,在自定义的视图中,如果某些情况下希望重新测量尺寸大小,应该手动去调用该方法,它会触发Measure和Layout过程,但不会进行 draw。
参考 How Android Draws Views
公共技术点之 View 绘制流程
《Android开发艺术探索》