View的工作原理

  1. 初识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方法开始的。

View的工作原理

 

 

performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成*View的measure、layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。

  1. 理解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_contentmatch_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);
}

 

  1. 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;
    }
    ...
 }