View的工作原理
- 初识ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,这个过程可参考如下源码:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
View的绘制流程是从ViewRoot的PerformTraversals方法开始的。
performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成*View的measure、layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。
- 理解MeasureSpec
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,SpecSize指某种测量模式下的规格大小。
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(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
SpecMode有三类,每一类都有特殊的含义,如下所示:
测量模式 |
表示意思 |
UNSPECIFIED |
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部, 表示一种测量状态 |
EXACTLY |
父容器已经测量出View所需要的精确大小,这个时候View的最终大小就是 SpecSize所指定的值。它对应LayoutParams中match_parent和具体的数值 这两种模式。 |
AT_MOST |
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体 是什么值要看不同View的具体实现。它对应于LayoutParam中wrap_content. |
而上面的测量模式跟我们的布局时的wrap_content、match_parent以及写成固定的尺寸有什么对应关系呢?
match_parent—>EXACTLY。怎么理解呢?match_parent就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。
wrap_content—>AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父View给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。
固定尺寸(如100dp)—>EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。
3、MeasureSpec和LayoutParams的对应关系
系统内部是通过MeasureSpec来进行View的测量。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。对于*View(即DecorView)和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayouParams来共同确定,对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。
对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有如下一段代码,它展示了DecorView的MeasureSpec的创建过程,其中desiredWindowWidth和desiredWindowHeiht是屏幕的尺寸:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width);
childHeightMeasureSpec=getRootMeasureSpec(desireWindowHeight,lp.height);
performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
接着再看一下getRootMeasureSpec方法的实现:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
对于普通的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);
}
上述方法会对子元素进行measure,在调用子元素measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。从代码来看,很显然,子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin及padding有关,具体情况可以看一下ViewGroup的getChildMeasureSpec方法,如下所示。
public 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);
}
4、View的工作流程
View的工作流程主要是指measure、layout、draw这三大流程。
- 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));
}
我们接着看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方法的实现来看,View的宽/高由specSize决定,所以我们可以得出如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent.如何解决这个问题呢?也很简单,代码如下所示。
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if(widthSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSpecSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,mHeight);
}
}
- 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);
}
我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,比如LinearLayout、RelativeLayout等,为什么ViewGroup不像View一样对其onMeasure方法做统一的实现呢?那是因为不同的ViewGroup子类有不同的布局特性,这导致他们测量细节各不相同,比如LinearLayout和RelativeLayout这两者的布局特性显然不同,因此ViewGroup无法做统一实现。下面就通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程,其他Layout类型读者可以自行分析。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
查看一下measureVertical方法
for(int i=0;i<count;i++){
final View child = getVirtualChildAt(i);
...
measureChildBeforeLayout(child,i,widthMeasureSpec,0,heightMeasureSpec,
totalWeight==0?mTotalLength:0);
if(oldHeight!=Integer.MIN_VALUE){
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength,totalLength+childHeight+lp.topMargin
+lp.bottomMargin+getNextLocationOffset(child))
}
从上面一段代码可以看出,系统会遍历子元素并对每个子元素执行measureChildBeforeLayout方法,这个方法内部户调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来储存LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括子元素的高度以及子元素在竖直方向上的margin等。当子元素测量完毕后,LinearLayout会测量自己的大小,源码如下所示。
//Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
//check against our minimum height
heightSize = Math.max(heightSize,getSuggestedMinimumHeight());
int heightSizeAndState = resolverSizeAndState(heightSize,heightMeasureSpec,0);
heightSize = heightSizeAndState&MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth,widthMeasureSpec,childState)),hightSizeAndState);
View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasured-Width/Hight方法就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽/高,在这种情形下,在onMeasure方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终宽/高。
5、layout过程
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__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);
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;
}
Layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,View的四个顶点一旦确定,那么View在父容器中的位置也确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体布局有关,所以View和ViewGroup均没有真正实现onLayout方法。接下来,我们可以看一下LinearLayoutde onLayout方法,如下所示。
@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);
}
}
接着我们看layoutVertical方法:
void layoutVertical(int left,int top,int right,int bottom){
...
final int count = getVirtualChildCount();
if(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.getMeasureWidth();
fianl int childHeight = child.getMeasureHeight();
final LiearLayout.LayoutParams lp =
(LinearLayout.LayoutParams)child.getLayoutParams();
...
if(hasDividerBeforeChildAt(i)){
childTop += mDividerHeight;
}
childTop +=lp.topMargin;
setChildFrame(child,childLeft,childTop+getLocationOffset(child),
childWidth,childHeigth);
childTop +=childHeight + lp.bottomMargin + getNextLocationOffset(child);
i + = getChildrenSkipCount(child,i);
}
}
}
这里分析一下layoutVertical的代码逻辑,可以看到,此方法会遍历所有子元素并调用setChildFrame方法来为子元素指定相应的位置,其中childTop会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合竖直方向的LinearLayout的特性。至于setChildFrame,它仅仅是调用子元素的layout方法而已,这样父元素在layout方法中完成自己的定位以后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过自己的layout方法来确定自己的位置,这样一层一层地传递下去就完成了整个View树的layout过程。
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
- draw过程
View的绘制过程遵循如下几步:
- 绘制背景background.draw(canvas).
- 绘制自己(onDraw).
- 绘制children(dispatchDraw)。
- 绘制装饰(onDrawScollBars)。
View有一个特殊的方法setWillNotDraw,当我们自定义控件继承于ViewGroup时,ViewGrop需要通过onDraw来绘制内容时,我们需要显示的关闭WILL_NOT_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);
// 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;
}
...
}