View测量、布局及绘制原理

ActivityThread.handleResumeActivity

1 、View绘制的三大过程

//View绘制的三大过程开始位置
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                                 String reason) {
// 将DecorView添加到Window上,紧接着进入绘制三大过程,实际上是调用WindowManagerImpl的addView方法,然后调用WindowManagerGlobal的addView方法。
                // 出发绘制的单打过程的条件是:当DecorView被添加到Window中时。
                wm.addView(decor, l);

}

这块是什么意思呢?通俗讲就是就是setContentView源码理解将开发者定义XML布局解析或构建成Android的View树,当Activity声明周期执行到handleResumeActivity时,把根View添加到窗口中,紧接着将根View交由ViewRootImpl管理绘制等。这样我们就把Activity.onCreate中setContentView和View的绘制流程串联起来了。
当然这里面会构建很多关于View绘制过程所需的参数,比如:LayoutParams 等。

WindowManagerImpl.addView
 // 参数view是顶层视图(DecorView)
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
WindowManagerGlobal
 // 参数view是顶层视图(DecorView)
 public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

ViewRootImpl root;
View panelParentView = null;

root = new ViewRootImpl(view.getContext(), display);
// view就是顶层视图(DecorView)
view.setLayoutParams(wparams);
// 将View和和ViewRooot已将View的 params收集到容器中管理
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

try {

 root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
        }
}
小结

我省略了一些不重要的代码,触发View的绘制过程的条件是ActivityTherad.handleResumeActivity方法开始将DecorView添加到Window上时,紧接着在WindowManagerGlobal的addView方法中创建ViewRootImpl 对象并调用ViewRootImpl的setView方法,将顶层视图(DecorView)做参数传入,进入绘制三大过程,实际上是调用WindowManagerImpl的addView方法,然后调用WindowManagerGlobal的addView方法。触发View绘制三大过程的条件是:当DecorView被添加到Window中时,上面最后一步提到调用ViewRootimpl的setView并传入DecorView,那么我们看一下ViewRootImpl.setView方法做了什么?

ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
            requestLayout();
}

我们看到setView方法里面调用了requestLayout方法,代码如下:

 @Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

在requestLayout方法中又调用 checkThread(),看看就知道检查当前线程,看一下代码:

 void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

你现在应该知道为什么不能再工作线程中更新UI了吗?当然你要是Activity的onResume之前是可以在工作线程更新UI的,我们看到在requestLayout方法中还调用checkThread结束还调用scheduleTraversals方法,点击去看看代码:

void scheduleTraversals() {

     mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

}

在scheduleTraversals方法中其他的不要看,你就mTraversalRunnable代码。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

在TraversalRunnable 中又调用doTraversal方法。

void doTraversal() {

  performTraversals();

}

可以看到在doTraversal方法中调用performTraversals方法。

private void performTraversals() {

final View host = mView;

// 因为顶层View,这里需要根据窗口的宽高以及View自身的LayoutParams计算MeasureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                
// performMeasure  测量
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
//再次测量的标志
boolean measureAgain = false;
//有权重,就会被测量两次,为了性能,想线性布局中使用Weight
if (lp.horizontalWeight > 0.0f) {
    width += (int) ((mWidth - width) * lp.horizontalWeight);
    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
    measureAgain = true;
 }
if (lp.verticalWeight > 0.0f) {
   height += (int) ((mHeight - height) * lp.verticalWeight);
   childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
   measureAgain = true;
}
if (measureAgain) {
    //有权重,就会被测量两次,为了性能,想线性布局中使用Weight
   // performMeasure
   performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// performLayout
 performLayout(lp, mWidth, mHeight);


// performDraw
 performDraw();

}

可以看到在performTraversals方法中又分别调用:performMeasure(测量)、performLayout(布局)和performDraw(绘制),执行了View的绘制三大过程,那么接下来我们分别介绍这些过程。

2、 测量(performMeasure)

首先看看performTraversals方法在执行performMeasure之前做的测量准备,代码如下;

// 因为顶层View,这里需要根据窗口的宽高以及View自身的LayoutParams计算MeasureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                
// performMeasure  测量
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
//再次测量的标志
boolean measureAgain = false;
//有权重,就会被测量两次,为了性能,想线性布局中使用Weight
if (lp.horizontalWeight > 0.0f) {
    width += (int) ((mWidth - width) * lp.horizontalWeight);
    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
    measureAgain = true;
 }
if (lp.verticalWeight > 0.0f) {
   height += (int) ((mHeight - height) * lp.verticalWeight);
   childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
   measureAgain = true;
}
if (measureAgain) {
    //有权重,就会被测量两次,为了性能,想线性布局中使用Weight
   // performMeasure
   performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}

首先我知道测量时需要MeasureSpec,所以在performTraversals方法中执行performMeasure方法之前,即在测量之前需要准备MeasureSpec,具体代码:

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, 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;
}

可以看到在创建DecorView的MeasureSpec时,需要根据自己的LayoutParams和Parent的测量模式(model)最终决定DecorView的MeasureSpec。

2.1 、MeasureSpec的介绍

模式(Mode) + 尺寸(Size)->MeasureSpec  32位int值
00000000 00000000 00000000 00000000
SpecMode(前2位)   +  SpecSize(后30)
mode + size --> MeasureSpec

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

父容器不对View做任何限制,系统内部使用
public static final int UNSPECIFIED = 0 << MODE_SHIFT; 00000000 00000000 00000000 00000000

父容器检测出View的大小,Vew的大小就是SpecSize LayoutPamras match_parent 固定大小
public static final int EXACTLY     = 1 << MODE_SHIFT; 01000000 00000000 00000000 00000000

父容器指定一个可用大小,View的大小不能超过这个值,LayoutPamras wrap_content
public static final int AT_MOST     = 2 << MODE_SHIFT; 10000000 00000000 00000000 00000000

由上述可知MeasureSpe封装了测量model和size,而前2位表示SpecMode,后30位表示(SpecSize),MeasureSpe中还定义了三种MeasureSpe,分别是:

  • UNSPECIFIED 父容器不对View做任何限制,系统内部使用,当然你在ScrollView中也是可以看到的。
  • EXACTLY 父容器检测出View的大小,Vew的大小就是SpecSize LayoutPamras.match_parent 固定大小
  • AT_MOST 父容器指定一个可用大小,View的大小不能超过这个值,LayoutPamras.wrap_conten

在MeasureSpe中定义了从MeasureSpe获取model和size的方法,我们就不看了。

2.2、performMeasure方法
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    try {
        //调用DecorView的measure方法进行调度测量,那么DecorView是继承了FrameLayout
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

可以看到performMeasure方法中直接调用了View.measure,而这个measure方法是View的测量调度方法:

2.2.1 默认的情况
//View的measure方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        resolveRtlPropertiesIfNeeded();

        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        /**
         * 没有缓存直接调用onMeasure进行测量操作
         *  onMeasure  -> setMeasuredDimension -> setMeasuredDimensionRaw(保存测量的结果)
         */
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            // 测量自己,这应该将mPrivateFlags设置为PFLAG_MEASURED_DIMENSION_SET标志回传
            // 不复写使用默认大小,因为目前是针对的DecorView,所以我们要看DecorView的OnMeasure的实现,即Framelayout
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {//缓存存在
            long value = mMeasureCache.valueAt(cacheIndex);
            // Casting a long to int drops the high 32 bits, no mask needed
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        // flag not set, setMeasuredDimension() was not invoked, we raise
        // an exception to warn the developer
        /**
         * 在setMeasuredDimensionRaw中设置 将mPrivateFlags为了PFLAG_MEASURED_DIMENSION_SET标志
         */
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("View with id " + getId() + ": "
                    + getClass().getName() + "#onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

}



protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //不复写使用默认大小
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}


protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

为了简洁我把一些优化的算法给删掉,在measure方法中又调用onMeasure测量自己,然后调用setMeasuredDimension和setMeasuredDimensionRaw 方法保存测量结果,并将mPrivateFlags设置为PFLAG_MEASURED_DIMENSION_SET标志回传,如果你不复写View.onMeasure方法,就使用默认大小,如果你自定义View,不复写onMeasure方法,最后你会发现:wrap_content 和 match_parent的效果是一样的,可以进去看看onMeasure源码,因为目前是针对的DecorView,所以我们要看DecorView的OnMeasure的实现,即Framelayout,当你需要自定义Viewroup时你需要根据child的布局属性来测量自己的宽和高,而当你自定义View时,你仅仅只需要测量自己就可以了,最后总结:

ViewGroup : measure --> onMeasure(测量子控件的宽高measureChild)--> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)
View measure --> onMeasure(测量自己) --> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)

最后我们来看看FrameLayout的onMeasure是怎么实现的:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

    final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        // GONE 是不进行测量的
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            /**
             * 调用measureChildWithMargins测量Child
             */
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);


            /**
             * 根据FrameLayout的布局特点,为了计算自己的大小,它只需要计算最大的宽度和高度
             */
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);

            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
    //设置测量结果,进行回传
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
}

 // 测量child  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);
}

可以看到在FrameLayout.onMeasure方法中是通过循环遍历所有的child,来决定自己的最终宽度和高,那么DecorView是继承FrameLayout的,所以这也就是DecorView的测量规范,由measureChildWithMargins方法得知Child的ModeSpec是由parentWidthMeasureSpec 和 Child的LayoutParam决定,然后在顶用child.measure方法将计算出来的ModeSpec传给Child,最终会回调到child的onMeasure方法中。

3、布局 performLayout

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,  int desiredWindowHeight) {

      //View和ViewGroup 也是会调用的onLayout,layout方法是摆放自己,而onLayout是根据自己的要求摆放child的
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}

顶层视图(DecorView)host调用layout方法摆放自己

public void layout(int l, int t, int r, int b) {

   // onLayout
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
       }
}

在layouyt方法中有会调用onLayout,对child进行摆放,不管是View还是ViewGroup都会回调onLayout方法,但是onLayout方法是对child进行摆放的,在View中是空实现,你自定义实现了也没有实际的意义,所以layout的过程是比较简单的,接下来我们看看performDraw方法,
ViewGroup.layout(来确定自己的位置,4个点的位置) -->onLayout(进行子View的布局)
View.layout(来确定自己的位置,4个点的位置)。

4、绘制performDraw

 private void performDraw() {
       boolean canUseAsync = draw(fullRedrawNeeded);
  }

performDraw方法很简单,直接调用了draw方法:

 private boolean draw(boolean fullRedrawNeeded) {
           if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                    scalingRequired, dirty, surfaceInsets)) {
                return false;
            }
 }

draw方法代码很多,我就挑重点讲,在draw方法中调用了drawSoftware方法:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
    mView.draw(canvas);
}

drawSoftware方法代码非常多,主要就是初始化Canvas相关的操作,只挑重点讲,在drawSoftware方法中经过Canvas的初始化,直接调用了View的mView.draw(canvas);并将canvas作为参数传进去:

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;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
      //ViewGroup不会回调onDraw这个方法,如果你设置背景onDraw是会被回调的,具体请看ViewGroup的构造方法initViewGroup
        if (!dirtyOpaque) onDraw(canvas);


     //在ViewGroup实现了这个方法,在View中是空实,意思就是:如果是ViewGroup那么它会分发绘制的给child,而绘制有child自己完成。
        dispatchDraw(canvas);
   

       // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
 }

在draw方法中,你可能会看到privateFlags 这个标志,实际上这个表示是用来表示是否需要绘制,即回调onDraw方法,在ViewGroup的构造函数中将此标志设置为 WILL_NOT_DRAW,所以为什么ViewGroup默认不会回调onDraw方法的原因,当然如果你设置背景onDraw是会被回调的。

最后关于自定义View相关的:

  • ViewGroup
    1、绘制背景 drawBackground(canvas);

    2、绘制自己onDraw(canvas),实际上ViewGroup 并不需要去关心这个方法;

    3、绘制子View dispatchDraw(canvas) ViewGroup 会在dispatchDraw方法中循环遍历所有的child并调用ViewGroup .drawChild方法紧接着调用child.draw方法让child自己绘制,dispatchDraw方法是在ViewGroup中才会有的,所以View并不存在此方法;

    4、绘制前景,滚动条等装饰onDrawForeground(canvas)

  • View
    1、绘制背景 drawBackground(canvas)

2、绘制自己onDraw(canvas)

3、绘制前景,滚动条等装饰onDrawForeground(canvas)

你可能感兴趣的:(View测量、布局及绘制原理)