View的工作原理
ViewRoot和DecorView
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绘制到屏幕上。
performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。其中performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。

measure过程决定了View的宽/高,完成后可通过getMeasuredWidth/getMeasureHeight方法来获取View测量后的宽/高。Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过getTop、getBotton、getLeft和getRight拿到View的四个定点坐标。Draw过程决定了View的显示,完成后View的内容才能呈现到屏幕上。
DecorView作为顶级View,一般情况下它内部包含了一个竖直方向的LinearLayout,里面分为两个部分(具体情况和Android版本和主题有关),上面是标题栏,下面是内容栏。在Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。
理解MeasureSpec
MeasureSpec
MeasureSpec代表一个32位的int值,高2位为SpecMode,低30位为SpecSize,SpecMode是指测量模式,SpecSize是指在某种测量模式下的规格大小。
为了方便操作其提供了打包和解包的方法,一组SpecMode和SpecSize可以打包为MeasureSpec,MeasureSpec也可以解包为一组SpecMode和SpecSize,这里的MeasureSpec是指其所代表的int值
MpecMode有三类:
- UNSPECIFIED 父容器不对View进行任何限制,要多大给多大,一般用于系统内部
- EXACTLY 父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的match_parent和具体数值这两种模式。
- AT_MOST 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,不同View实现不同,对应LayoutParams中的wrap_content。
MeasureSpec和LayputParams的对应关系
对于DecorView,它的MeasureSpec由Window的尺寸和其自身的LayoutParams来共同确定,对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams来共同确定。
当View采用固定宽/高的时候,不管父容器的MeasureSpec的是什么,View的MeasureSpec都是精确模式兵其大小遵循Layoutparams的大小。 当View的宽/高是match_parent时,如果他的父容器的模式是精确模式,那View也是精确模式并且大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且起大小不会超过父容器的剩余空间。 当View的宽/高是wrap_content时,不管父容器的模式是精确还是最大化,View的模式总是最大化并且不能超过父容器的剩余空间。
这里参数中的padding是指父容器的padding,这里是父容器所占用的空间,所以子view能使用的空间要减去这个padding的值。同时这个方法内部其实就是根据父容器的MeasureSpec结合子view的LayoutParams来确定子view的MeasureSpec
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
66
67
68public 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 him 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;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}普通View的MeasureSpec创建规则

View的工作流程
measure过程
View的measure
- 一般我们只需要看MeasureSpec.AT_MOST和MeasureSpec.EXACTLY两种情况,这两种情况返回的result其实都是measureSpec中取得的specSize,这个specSize就是View测量后的大小,这里之所以是View测量后的大小,是因为View的最终大小是在layout阶段确定的,所以要加已区分,一般情况下View测量大小和最终大小是一样的。
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
- View的宽高由specSize决定,如果我们通过继承View来自定义控件需要重写onMeasure方法,并设置WRAP_CONTENT时的大小,否则在布局中使用WRAP_CONTENT相当于使用MATCH_PARENT
ViewGroup的measure
ViewGroup是一个抽象类,它没有重写View的onMeasure方法,而是自己提供了一个measureChildren方法
1
2
3
4
5
6
7
8
9
10
11protected 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);
}
}
}里面会对子元素进行遍历,然后调用measureChild方法去测量每一个子元素的宽高
1
2
3
4
5
6
7
8
9
10
11
12protected 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接着将其传递给view的measure方法进行测量
ViewGroup并没有去定义测量的具体过程,这是因为ViewGroup是一个抽象类,其onMeasure方法需要各个子类去具体实现,因为每个ViewGroup子类有不同的布局特性,这导致他们的测量细节各不相同,因此ViewGroup无法做到统一实现
有时候onMeasure中拿到的测量宽高可能是不准确的,比较好的习惯是在onLayout中去获取View的测量宽高和最终宽高
在Activity中,在onCreate,onStart,onResume中均无法正确获得View的宽高信息,这是因为measure和Activity的生命周期是不同步的,所以很可能View没有测量完毕,获得的宽高是0,有四种解决该问题的方法:
Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义:View已经初始化完毕,宽高都已经准备好了,,这个时候去获取宽高都是没有问题的,需要注意的是这个方法会被多次调用,当Activity窗口得到焦点和失去焦点时均会被调用,如果频繁地进行onResume和onPause那么这个方法就会频繁地被调用
1
2
3
4
5
6
7
8
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = myView.getMeasuredWidth(); // 此时测量已完成,可以拿到值
int height = myView.getMeasuredHeight();
}
}
view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化完成
1
2
3
4
5
6
7
8
9
10
11
12protected void onStart(){
super.onStart();
view.post(new Runnable() {
public void run() {
// 此时 View 已经关联到 Window,且完成了测量和布局
int width = myView.getWidth();
int height = myView.getHeight();
Log.d("TAG", "View 的宽高是: " + width + " x " + height);
}
});
}
ViewTreeObserver
使用ViewTreeObserver这个的众多回调也可以实现这个功能,比如使用OnGlobalLayoutListener这个接口,当 View 树的状态发生改变或者View树内部的View的可见性发生改变时,此方法发生回调,因此是获取view宽高的好时机,需要注意的时,伴随着View树状态改变此方法会被多次调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18protected void onStart(){
super.onStart();
// 1. 获取观察者并添加监听
ViewTreeObserver observe=view.getViewTreeObserver();
observe.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
// 2. 此时可以百分之百拿到准确的宽高
int width = view.getWidth();
int height = view.getHeight();
// 3. 【极其重要】及时移除监听
// 因为任何微小的布局变化(如键盘弹出)都会再次触发此方法,不移除会造成性能浪费甚至死循环
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
view.measure(int widthMeasureSpec,int heightMeasureSpec)
通过手动对View进行measure,需要根据LayoutParams分情况处理
**match_parent:**直接放弃,无法measure出宽高
具体数值:
1
2
3int widthSpec = MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);wrap_content:
1
2
3
4// 使用 AT_MOST 模式,大小传入屏幕能提供的最大值(移位运算后的最大值)
int widthSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);//1<<30即2的30次方
int heightSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
view.measure(widthSpec, heightSpec);
总结
measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程。
MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:
1
2
3MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。
ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。
只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
View的布局大小由父View和子View共同决定。
使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
layout过程
layout
- Layout作用是ViewGroup用来确定子元素位置的,ViewGroup的位置确定后,它在onLayout中会遍历所有的子元素并调用子元素layout方法,子元素layout方法中又会调用onLayout方法,View的layout方法确定自身的位置,而onLayout方法确定所有子元素的位置
- layout方法的大致流程如下:首先会通过setFrame方法来确定mLeft;mTop;mBottom; mRight;只要这四个点一旦确定,那么View在父容器中的位置就确定了,接着会调用onLayout方法,该方法目的是父容器来确定子元素的位置,无论是View还是ViewGroup都没有实现onLayout方法
总结
- layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。
- View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。
- measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
- 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的。
- 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。
draw过程
draw
步骤
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
View的绘制过程的传递是通过dispatchDraw实现的,dispatchdraw会遍历调用所有子元素的draw方法。如此draw事件就一层一层的传递下去。
总结
- 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
- View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。
- View的绘制是借助onDraw方法传入的Canvas类来进行的。
- 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。
- 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。
自定义View
自定义View的分类
- 继承View重写onDraw方法
- 这种方法主要用于实现一些不规则的效果,这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态的显示一些不规则的图形,很显然这需要通过绘制的方法来实现,重写onDraw 方法,采用这种方式需要自己支持wrap_content,并且padding也是需要自己处理。
- 继承ViewGroup派生特殊的Layout
- 这种方法主要用于实现自定义的布局,出了LinerarLayout、RelativeLayout、FrameLayout 这几种系统的布局之外,我们重新定义一种新布局、当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现,采用这种方式稍微复杂一些,需要合适的处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
- 继承特定的View(比如TextView ImageVIew)
- 这种方法比较常见,一般适用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实现,这种方法不需要自己支持 wrap_content 和 padding等;
- 继承特定的VIewGroup(比如LinearLayout)
- 这种方法比较常见,当某种效果看起来很像几个View组合在一起的时候,可以 采用这种方式来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程,需要注意这种方法和第二种方法的区别,一般来说方法二种实现的效果方法四中也可以实现,两者的主要差别在于方法二更接近View的底层。
自定义View注意事项
- 让View支持wrap_content
- 因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content中做一些的特殊处理,那么当外界在布局中使用wrap_content 时就无法达到预期的效果。
- 如果有必要,让View支持padding
- 如果不在draw方法中处理padding,那么padding属性是无法起作用的,另外,直接继承自ViewGroup 的控件需要在onMeasure 和onLayout中考虑padding和子元素的margin对其造成影响,不然将导致padding和子元素的margin失效。
- 尽量不要再View中使用Handler
- 因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非自己很明确的要使用Handler来发送消息。
- View中如果有线程或者动画,需要及时停止
- 如果有线程或者动画需要停止时候,那么onDetachedFromWindow是一个很好的时机,当包含View的Activity退出或者当前VIew被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow,当包含此View的Activity启动的时候,View的onAttachedToWindow方法会被调用,同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
- View带有滑动嵌套情形时,需要处理好滑动冲突
- 如果有滑动冲突的时候,那么要合适的处理滑动冲突,否则将会严重影响View的效果。




