Android View 的工作原理浅析
文章目录
- 1. 基础概念
- 2. MeasureSpec
- 2.1 SpecMode 和 SpecSize
- 2.2 MeasureSpec 和 LayoutParams
- 2.3 margin 和 padding
- 3. performMeasure
- 3.1 View 的 measure 过程
- 3.2 ViewGroup 的 measure 过程
- 4. performLayout
- 5. performDraw
在 Android 的知识体系中,View 占据非常重要的作用,简单来理解,View 是 Android 在视觉上的呈现。为了更好的自定义控件,也就是自定义 View ,我们还需要掌握 View 的底层原理。在 Android 系统中 UI 显示的 3 要素是尺寸大小、位置和内容,分别对应于 View 的 measure、layout和 draw过程。
1. 基础概念
-
View 和 ViewRoot
从名字来理解,“ViewRoot” 似乎是 “View 树的根” 。这很容易让人产生误解,因为 ViewRoot 并不属于 View 树的一份子。从源码实现上来看,ViewRoot 和 View 对象并没有任何“血缘”关系,它即非 View 的子类,也并非 View 的父类。更确切的说,ViewRoot 可以被理解为“ view 树的管理者”——它有一个 mView 的成员变量,指向的是它所管理的View 树的根。ViewRoot 的核心任务就是与 WindowManagerService 进行通信。 -
Activity 和 Window
Activity 是支持 UI 显示的,那么它是直接管理 View 树或者 ViewRoot 呢?其实都不是,Activity 内部有一个Window的类型的成员成员 mWindow 。Window 就是“窗口”的意思, Window 是基类,根据不同的产品可以衍生出不同的子类——具体则是由系统在 Activity.attach 中调用 PolicyManager.makeNewWindow 决定的,目前版本的 Android 系统默认生成的都是 PhoneWindow。
ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。
View 的绘制流程是从 ViewRoot 的 performTraversals(执行遍历)方法开始的,它经过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高(尺寸大小),layout 用来确定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。
2. MeasureSpec
MeasureSpec 在 View 的测量过程中起到很大的作用,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响 View 的 MeasureSpec 的创建过程。在测量的过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后在根据这个MeasureSpec 来测量出 View 的宽/高。
2.1 SpecMode 和 SpecSize
MeasureSpec 代表一个 32 为 int 值,高 2 为代表 SpecMode,低 30 位代表 SpecSize,SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下 View 的大小。
SpecMode 和 SpecSize 是一个 int 值,一组 SpecMode 和 SpecSize 可以打包成一个 MeasureSpec,而一个 MeasureSpec 可以通过解包的形式来得出原始的 SpecMode 和 SpecSize,需要注意的是这里提到的 MeasureSpec 是指 MeasureSpec 所代表的 int 值,而并非 MeasureSpec 本身。
SpecMode 分为三类,每一个类都表示特殊的含义,分别如下:
- UNSPECIFIED : 父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
- EXACTLY : 父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和 具体的数组这两种模式。
- AT_MOST : 父容器指定了一个可用大小即 SpecSize ,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 warp_content。
2.2 MeasureSpec 和 LayoutParams
系统内部是通过 MeasureSpec 来进行 View 的测量,正常情况下,我们可以使用 View 指定 MeasureSpec ,但是我们还可以给 View 指定 LayoutParams,这使我们指定的 MeasureSpec 是不准确的,所以必须经过系统自己的测量。在 View 测量的时候,系统会将 LayoutParams 在父容器的约束下转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 来确定 View 测量后的宽/高。需要注意的是,MeasureSpec 不但由 LayoutParams 决定的,而且还和父容器一起才能决定 View 的 MeasureSpec。MeasureSpec 一旦确定后,omMeasure 中就可以确定 View 的测量的宽和高了。
对于普通 View 来说,这里值我们布局中的 View ,View 的 measure 过程由 ViewGroup 传递而来,先看一下 ViewGroup 的 measureChildWithMargins 方法:
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
从代码可以看出,在 ViewGroup 中调用子元素的 measure 方法之前会先通过 getChildMeasureSpec 方法来得到子元素的 MeasureSpec 。从代码来看,很显然,子元素的 MeasureSpec 的值是由父容器的 MeasureSpec 、 子元素本身的 LayoutParams 和 View 的 margin 及 padding (下面有两者的区别)共同决定的。下面根据getChildMeasureSpec 方法的主要内容用表格表现出来:
childLayoutParams \ parentSpecMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY = childSize | EXACTLY = childSize | EXACTLY = childSize |
match_parent | EXACTLY = parentSize | AT_MOST <= parentSize | UNSPECIFIED = 0 |
warp_content | AT_MOST <= parentSize | AT_MOST <= parentSize | UNSPECIFIED = 0 |
简要说明的,对于普通 View ,其 MeasureSpec 由父容器的 MeasureSpec 和 自身的 LayoutParams 来共同决定,那么针对不同的父容器和 View 本身不同的 LayoutParams,View 就可以有多种 MeasureSpec。具体概括就是:
- 当 View 采用固定宽或高的时候,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec都是精确模式并且View 的大小遵循 LayoutParams 中的大小。
- 当 View 的宽或高是 match_parent 时,如果父容器的模式时精确模式,那么 View 也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。
- 当 View 宽或高是 warp_content 时,不管父容器的 MeasureSpec 是什么,View 的模式总是最大化并且大小不会超过父容器的剩余空间。
2.3 margin 和 padding
对于 View 类而言,它只有 padding,没有 margin。这是因为 padding 值的是“内容”区域与外围边框的距离,分为 left、right、top、bottom 四个方向。而 margin 则是“内容”内部进一步细化——即“内容”中各元素之间的间距。可想而知,一个 View (非ViewGroup)实例的“内容”本身就是不可分割的,不存在内部对象间距的说法。对于 ViewGroup 由多个子对象组成,它们之间有时需要 margin 属性来将彼此区分开来。
3. performMeasure
measure 过程要分情况来看,如果只是一个原始的 View ,那么通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup ,除了完成自己的测量之外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程,下面针对这两种情况分别讨论。
3.1 View 的 measure 过程
View 的 measure 过程由其 measure 方法来完成,measure 方法是一个 final 类型的方法,这就意味着不能重写此方法,在 View 的 measure 方法中会去调用 View的 onMeasure 方法,因此只需要看 onMeasure 发实现即可, View 的 onMeasure 方法如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
setMeasuredDimension 方法会设置 View 宽和高的测量值,因此我们只需要看 getDefaultSize 方法即可:
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; }
可以看出,getDefaultSize 这个方法的逻辑很简单,对于我们来说,我们只需要看 AT_MOST 和 EXACTLY 这两种情况。简单的理解就是,getDefaultSize 返回的大小就是 MeasureSpec 中的 specSize,而这个 SpecSize 就是 View 测量后的大小,这里多次提到测量后的大小,是因为 View 最终的大小是在 layout 阶段确定的,所以这里必须要加以区分,但是几乎所有情况下 View 的测量大小和最终大小是相同的。
至于 UNSPECIFIED 这种情况,一般用于系统内部的测量过程,在这种情况下,View 大小为 getDefaultSize 的第一个参数 size,即宽和高为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 这两个方法的返回值,它们的源码是:
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
我们来分析 getSuggestedMinimumWidth 方法(height方法原理一样)。从代码可以看出如果 View 没有设置背景,那么 View 的宽度为 mMinWidth ,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值。如果这个属性不指定,那么 mMinWidth 则默认为 0 ;如果 View 指定了背景,则 View 的宽度为 max(mMinWidth, mBackground.getMinimumWidth()),mBackground.getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就放回 0 。那么 Drawable 在什么情况下有原始宽度呢?例如,ShapeDrawable 无原始宽和高,而 BitmapDrawable 有原始宽和高(图片的尺寸)。
从 getDefaultSize 方法的实现来看,View 的宽和高由 specSize 决定,所以我们得出结论: 直接继承 View 的自定义控件需要重新写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent 。 因为,如果 View 在布局中使用 wrap_content ,那么它的 specMode 是 AT_MOST ,在这种模式下,它的宽和高就等于 specSize。由上面的表格可以看出,View 的 specSize 是 parentSize ,而 parentSize 是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View 的宽和高就等于父容器当前剩余的空间大小,这种效果和布局中使用 match_parent 完全一致。解决办法为:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec);int mWidth = 0;//默认的宽,自己定义int mHeight= 0;//默认的高,自己定义 int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, mHeight); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, heightSpaceSize); } else if (heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpaceSize, mHeight); } }
对于非 wrap_content 的情形,我们沿用系统的测量值即可,至于这个默认的内部宽和高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。
3.2 ViewGroup 的 measure 过程
对于 ViewGroup 来说,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素在递归去执行这个过程。和 View 不同的是,ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它提供了一个叫 measureChildren 的方法,如下:
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 时,会对每一个子元素进行 measure,measureChild 这个方法的实现也很好理解:
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); }
很显然,measureChild 的思想就是取出子元素的 LayoutParams ,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。getChildMeasureSpec 就是上述 2.2 中的表格的内容。
我们知道,ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如 LinearLayout、RelativeLayout 等,为啥 ViewGroup 不像 View 一样对其 onMeasure 方法统一的实现呢?那是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法做统一实现。
4. performLayout
Layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。Layout 过程和 measure 过程相比就简单多了,layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置,先看 View 的 layout 方法,如下所示:
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); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(true); } }
layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、和 mBottom 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了;接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似,onLayout 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法。
5. performDraw
Draw 的过程就比较简单了,它的作用是将 View 绘制到屏幕上。View 的绘制过程遵循如下几步:
(1)绘制背景 background.draw(canvas)
(2)绘制自己(onDraw)
(3)绘制 children(dispatchDraw)
(4)绘制装饰(onDrawScrollBars)
draw 方法的源码是:
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); // Step 4, draw the children dispatchDraw(canvas); drawAutofilledHighlight(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); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; }
View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有子元素的 draw 方法,如此 draw 事件就一层层的传递下去。View 有一个特殊的方法 setWillNotDraw,先来看看它的源码:
/** * If this view doesn't do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. * * Typically, if you override {@link #onDraw(android.graphics.Canvas)} * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */ public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }
从 setWillNotDraw 这个方法的注释中可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记为 true 以后,系统会进行相应的优化。默认情况下,View 没有启用这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDwon 来绘制内容时,我们需要显示的关闭 WILL_NOT_DRAW 这个标记位。
站在巨人的肩膀上:
《Android 开发艺术探究》——任玉刚
更多相关文章
- Android中通过资源文件获取drawable的几种方法
- ListView取消和自定义分割线的方法
- Android L Preview 源码同步方法
- Android中检测网络连接状况的方法
- Android WebView的使用方法总结
- Android JNI 开启子线程后调用 Activity 方法更新UI
- Android屏幕旋转时Activity不重新调用onCreate的方法
- Android Studio中隐藏状态栏和标题栏的方法