View的工作原理

初识ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,这个过程可参看源码:

1
2
root = new ViewRootImpl(view.getContext, display);
root.setView(view, wparams, panelParentView);

View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout、draw三个过程最终才将一个View绘制出来的。其中measure用来测量View的宽和高,layout用来确定View在父容器的放置位置,而draw则负责将View绘制在屏幕上,针对perfromTraversals的大致流程,可用下列流程图来表示:


performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三个流程、其中在performMeasure中会调用measure方法,measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有子元素进行measure过程,完成整个View树的遍历。同理,performLayout和performDraw的流程类似,唯一不同的是,performDraw的传递过程是在draw方法中的dispatchDraw来实现的,不过并没有本质区别。

DecorView作为顶级View,一般情况下内部包含一个竖直方向的LinearLayout,在这个LinearLayout中有上下两部分,上面是标题栏,下面是内容栏。在Activity中我们通过setContentView所设置的布局就是被加到内容栏中的。可通过如下代码得到我们所设置的布局;

1
2
ViewGroup content = findViewById(R.android.id.content);
content.getChildAt(0);

DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后传递给我们的View。

理解MeasureSpec

MeasureSpec

MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指某种测量模式下的规格大小。下面看一下MeasureSpec内部的一些常量定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

SpecMode有三类,每一类都有特殊的含义:
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于layoutParams中的match_parent和具体的数值这两种模式
AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同的View的具体实现,它对应于LayoutParams中的Wrap_content

MeasureSpec和LayoutParams的对应关系

对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定的;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定的。MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽和高。
普通View的MeasureSpec的创建规则如下表:(表中的parentSize是指父容器中目前可使用的大小)

  • 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循其LayoutParams中的大小。
  • 当View的宽高是match_parent时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View是最大模式并且其大小不会超过父容器的剩余空间。
  • 当View的宽高是wrap_content时,不管父容器的模式是精准还是最大,View的模式总是最大化模式,并且其大小不会超过父容器的剩余空间。
  • (UNSPECIFIED模式是系统内部多次Measure的情况,一般来说,我们不需要关注此模式)

View的工作流程

View的工作流程主要是指measure、layout、draw这三大流程。其中measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,而draw则将View绘制到屏幕上。

measure过程

如果是一个View,则通过measure方法就完成了其测量过程;如果是ViewGroup,除了要完成自己的测量过程外,还要遍历调用所有子元素的measure执行测量。

View的measure过程

View的measure过程是由measure方法来完成的,measure方法是个final类型的方法,不能重写,measure方法调用了onMeasure方法,实现如下:

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

getDefaultSize方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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方法的实现来看,View的宽高是由specSize决定的,所以,直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身的大小,否则在布局中使用wrap_content就相当于使用match_content。解决方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
//自定义空间最小值
setMeasuredDimension(200,200);
}else if (widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200,heightSpecSize);
}else if (heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,200);
}
}

ViewGroup的measure过程

1
2
3
4
5
6
7
8
9
10
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);
}
}
}

和View不同的是,ViewGroup是一个抽象类,因此它没有重写onMeasure方法,但是它提供了一个叫measureChildren的方法。
measureChild的思想就是取出子元素的LayoutParams,然后通过传递进来的父容器的MeasureSpec和子元素的LayoutParams创建资源速度恶MeasureSpec,接着将MeasureSpec直接传递给子View的measure进行测量。
ViewGroup并没有定义具体的测量过程,是因为不同的ViewGroup子类(如LinearLayout、RelativeLayout)有不同的布局特性,实现细节不一样。

View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽高就是0。

解决的方法有如下:

  1. Activity/View#onWindowFocusChanged
    当Activity的窗口得到焦点和失去焦点的时候都会调用一次,且View已经初始化完毕了。
  2. view.post(runnable)
    通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了
  3. ViewTreeObserver
    使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalalayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,该接口方法将会被回调。
  4. view.measure(int widthMeasureSpec,int heightMeasureSpec)
    通过手动对View进行measure来得到View的宽高……

layout过程

layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。
layout方法的大致流程如下:
首先会通过setFrame方法来设定View的四个顶点的位置,View的四个顶点的位置一旦确定,那么View在父容器中的位置也就确定了;接着会调用onLayout方法,即父容器确定子元素的位置,和onMeasure方法类似,通过onLayout方法去调用子元素的layout方法,子元素又通过自己的layout方法确定自己的位置,这样一层层传递下去就完成了整个View树的layout过程。onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。

在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高是形成于View的measure过程,而最终宽高形成于View的layout过程,即两者的赋值时机不同,测量宽高稍微早些。因此,我们可以认为View的测量宽高等于最终宽高,但是的确存在某些特殊情况会导致两者不同。

draw过程

View的绘制过程遵循如下几步:

  1. 绘制背景 background.draw(canvas)
  2. 绘制自己 onDraw
  3. 绘制children dispatchDraw
  4. 绘制装饰 onDrawScrollBars

View的绘制过程的传递是通过dispatchDraw来实现,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递了下去。

自定义View须知

  • 让View支持wrap_content
  • 如果有必要,让你的View支持平padding
  • 尽量不要在View中使用Handler,没必要
  • VIew中如果有线程或者动画,需要及时停止,参考View#onDetachedfromWindow
  • View带有滑动嵌套时,需要处理好滑动冲突