View的measure、layout、draw三大流程

1,716

三大流程触发

View的三大流程依次为measure(测量)—>layout(布局)—>draw(绘制),

结合activity的启动流程,activity对象被创建,然后经过create、start、resume阶段后,在resume中调用wm.addView(decor, l)。该方法中创建了ViewRootImpl对象,并调用ViewRootImpl的setVIew(Decorview)方法,之后调用requestLayout、scheduleTraversals()。

scheduleTraversals()中首先发送了同步屏障消息,然后发送异步消息来出发三个流程,msg的runnable为TraversalRunnable,回调里调用了doTraversal,之后调用了performTraversals,然后又调用了measureHierarchy,最后调到performMeasure,performLayout,performDraw开始了三个流程测量、布局、绘制。

先写个简单的布局,下面会用到

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <FrameLayout
       android:id="@+id/layout1"
       android:layout_width="match_parent"
       android:layout_height="300dp">

       <TextView
           android:id="@+id/text"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="Hello World" />
   </FrameLayout>

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="我是按钮" />

</FrameLayout>

测量 measure

ViewRootImpl#performMeasure执行流程:DecorView#measure(实际是View#measure)->View#onMeasure(实际是DecorView#onMeasure)->FrameLayout#onMeasure->子View#measure。

重点方法的调用顺序:
基于上面的布局,measure中重要的方法调用如下:

R.id.layout 的measure被执行
R.id.layout 的onMeasure被执行
    R.id.layout1 的measure被执行
    R.id.layout1 的onMeasure被执行
            R.id.text 的measure被执行
            R.id.text 的onMeasure被执行
            R.id.text 的setMeasuredDimension被执行
    R.id.layout1 的setMeasuredDimension被执行
    R.id.button 的measure被执行
    R.id.button 的onMeasure被执行
    R.id.button 的setMeasuredDimension被执行
R.id.layout 的setMeasuredDimension被执行

因为父View需要子View测量后的尺寸来设置自己的尺寸,因此父view的setMeasuredDimension总是比子View的晚。

子View测量规格的计算:
MeasureSpec类由模式和尺寸组成,通过使用二进制,将mode和size打包成一个int值。一个int型有32位,其中31和32两位表示mode,前面30位表示size。 MeasureSpec类用一个变量携带两个数据(size,mode)来减少对象内存分配,并提供了打包和解包的方法,

onMeasure调用child.measure一般如下:
onMeasure -> measureChildWithMargins -> getChildMeasureSpec -> child.measure

关键看getChildMeasureSpec方法,里面的switch-case 和 if-else 可以汇总成下面的表。

横:父View测量模式
纵:子View的LayoutParms
EXACTLY(精确)AT_MOST(至多)UNSPECIFIED (未指明)
具体数值EXACTLY+childDimensionEXACTLY+sizeEXACTLY+size
match_parentEXACTLY+sizeAT_MOST+sizeUNSPECIFIED+0
wrap_contentAT_MOST+sizeAT_MOST+sizeUNSPECIFIED+0

size为父的测量尺寸-子View的内外边距,与0取最大值

子View得到宽高的MeasureSpec后,会根据自己的特性进行计算,默认的计算方式如下:

getSuggestedMinimumWidth根据最小宽和背景计算,哪个大用哪个,因此当测量模式是UNSPECIFIED,就是根据这个建议的尺寸,另外两个模式时,则使用测量尺寸(见上方表格)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
    
    
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;
}

布局 layout

ViewRootImpl#performLayout执行流程:DecorView#layout(实际是ViewGroup#layout)->View#layout->DecorView#onLayout->FrameLayout#onLayout。

基于上面的布局,layout中重要的方法调用如下:

R.id.layout 的layout被执行
R.id.layout 的setFrame被执行
R.id.layout 的sizeChange被执行
R.id.layout 的onLayout被执行
    R.id.layout1 的layout被执行
    R.id.layout1 的setFrame被执行
    R.id.layout1 的sizeChange被执行
    R.id.layout1 的onLayout被执行
            R.id.text 的layout被执行
            R.id.text 的setFrame被执行
            R.id.text 的sizeChange被执行
            R.id.text 的onLayout被执行
            R.id.textonLayoutChange()被执行
    R.id.layout1onLayoutChange()被执行
    R.id.button 的layout被执行
    R.id.button 的setFrame被执行
    R.id.button 的sizeChange被执行
    R.id.button 的onLayout被执行
    R.id.buttononLayoutChange()被执行
R.id.layoutonLayoutChange()被执行

layout->setFrame(保存l、t、r、b)->sizeChange(宽高变化)->onLayout->onLayoutChange()

触发onMeasure后会标记PFLAG_LAYOUT_REQUIRED,因此必会触发onLayout。

绘制 draw

ViewRootImpl#performDraw执行流程:DecorView#draw(实际是View#draw)->View#drawBackground画背景->View#onDraw->View#dispatchDraw(实际上子View#draw)->View#onDrawForeground画前景。

performDraw到view的draw还需要判断PFLAG_INVALIDATE,下面会分析。

基于上面的布局,draw中重要的方法调用如下:

R.id.layout 的draw被执行
R.id.layout 的drawBackground被执行
R.id.layout 的onDraw被执行
R.id.layout 的dispatchDraw被执行
   R.id.layout1 的draw被执行
   R.id.layout1 的drawBackground被执行
   R.id.layout1 的onDraw被执行
   R.id.layout1 的dispatchDraw被执行
           R.id.text 的draw被执行
           R.id.text 的drawBackground被执行
           R.id.text 的onDraw被执行
           R.id.text 的dispatchDraw被执行
           R.id.text 的onDrawForeground被执行
   R.id.layout1 的onDrawForeground被执行
   R.id.button 的draw被执行
   R.id.button 的drawBackground被执行
   R.id.button 的onDraw被执行
   R.id.button 的dispatchDraw被执行
   R.id.button 的onDrawForeground被执行
R.id.layout 的onDrawForeground被执行

requestLayout和invalidate

requestLayout

调用某个View的requestLayout,会不断调用parent的requestLayout,最终调到ViewRootImp的requestLayout。

//View#requestLayout
public void requestLayout() {
    ……
    //添加了PFLAG_FORCE_LAYOUT的标记
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    //添加了PFLAG_INVALIDATED的标记
    mPrivateFlags |= PFLAG_INVALIDATED;

    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    ……
}
//ViewRootImp#requestLayout
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}
  • 在View的requestLayout方法中,会给View设置PFLAG_FORCE_LAYOUT,这个flag会让View的measure方法中的forceLayout为true,从而触发onMeasure测量。而测量后会设置上PFLAG_LAYOUT_REQUIRED,这个flag会让View触发onLayout。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ……
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    ……
    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        ……
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        ……
        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }
    ……
}

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

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        ……
    }
……
}
  • 在ViewRootImp的requestLayout方法中,设置mLayoutRequested为true,在performTraversals时候会用mLayoutRequested来判断是否调用measureHierarchy,该方法会触发performMeasure、performLayout、performDraw。

performMeasure、performLayout会执行onMeasure和onLayout。而在performDraw内部draw的过程中发现mDirty为空,所以所有View的draw不会被调用。

//ViewRootImp#draw
private boolean draw(boolean fullRedrawNeeded) {
    ……
    //requestLayout时,dirty为empty,无法触发view的draw方法
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        ……
    }
    ……
}

值得注意的是:在layout方法调用中,会调用setFrame,该方法有个逻辑会触发invalidate,invalidate会触发onDraw,因此可以说requestLayout只有当某个view的l,t,r,b发生改变时,才会触发invalidate,从而触发onDraw。

//伪代码
if(l,t,r,b 发生改变) { 
……
    invalidate 
……
}

requestLayout是不断往上调用的,因此子view没有机会标记PFLAG_FORCE_LAYOUT,所以不会用forceLayout为true来判断是否要测量,而是走needsLayout的判断条件。

final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec) && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

根据上面的代码:

  1. specChanged 表示宽或高的测量规格发生变化,可以理解测量规格发生变化是needsLayout的前提。
  2. 后面还跟着三个条件,满足其一即可:
    • 版本小于等于6.0
    • 该view的宽或高的测量规格不是精确的
    • 该view的宽或高的测量尺寸发生了变化

invalidate

invalidate相对于requestLayout会比较复杂,调用某个View的invalidate(),内部调用invalidateInternal来修改标记,然后调用 parent的invalidateChild(this, damage);

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
    ……
        //添加 PFLAG_DIRTY
        mPrivateFlags |= PFLAG_DIRTY;
    
        if (invalidateCache) {
            //添加 PFLAG_INVALIDATED,只有该View加了这个标记,parent都没有加
            mPrivateFlags |= PFLAG_INVALIDATED;
            //移除 PFLAG_DRAWING_CACHE_VALID
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        } 
    ……
            p.invalidateChild(this, damage);
    ……
}

invalidateChild内部有个do while循环,不停调parent的invalidateChildInParent,一直到调用ViewRootImpl的invalidateChildInParent。

public final void invalidateChild(View child, final Rect dirty) {
    ……
        if (child.mLayerType != LAYER_TYPE_NONE) {
            //添加 PFLAG_INVALIDATED
            mPrivateFlags |= PFLAG_INVALIDATED;
            //移除 PFLAG_DRAWING_CACHE_VALID
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }
        ……
        do {
            ……
            if (view != null) {
                if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
                    //mPrivateFlags 移除PFLAG_DIRTY_MASK,添加PFLAG_DIRTY
                    view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;
                }
            }
                
            parent = parent.invalidateChildInParent(location, dirty);
            ……
        } while (parent != null);
    }
}
//ViewGroup#invalidateChildInParent
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    // mPrivateFlags 包含 PFLAG_DRAWN 或者 PFLAG_DRAWING_CACHE_VALID的标记
    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
        //省去计算dirty,硬件渲染时不需要用到dirty
        ……
        //移除 PFLAG_DRAWING_CACHE_VALID
        mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        ////未设置LayerType,因此未添加 PFLAG_INVALIDATED
        if (mLayerType != LAYER_TYPE_NONE) {
            mPrivateFlags |= PFLAG_INVALIDATED;
        }
        return mParent;
    }
    return null;
}

ViewGroup的invalidateChildInParent方法主要是计算了dirty,移除了PFLAG_DRAWING_CACHE_VALID,注意是没有添加了PFLAG_INVALIDATED。

ViewRootImpl的invalidateChildInParent内部调用了invalidateRectOnScreen,之后调用scheduleTraversals,进而 performDraw->draw,mDirty非空就会调mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);最终调到mThreadedRenderer的updateViewTreeDisplayList

private void updateViewTreeDisplayList(View view) {
    //添加了 View.PFLAG_DRAWN;
    view.mPrivateFlags |= View.PFLAG_DRAWN;
    //判断是否有 PFLAG_INVALIDATED, 有的话为true
    view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)== View.PFLAG_INVALIDATED;
    //移除PFLAG_INVALIDATED
    view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
    //调了view的updateDisplayListIfDirty
    view.updateDisplayListIfDirty();
    view.mRecreateDisplayList = false;
}

View中的updateDisplayListIfDirty();

public RenderNode updateDisplayListIfDirty() {
    ……

    if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
            || !renderNode.hasDisplayList()
            || (mRecreateDisplayList)) {
        // 只有添加了PFLAG_INVALIDATED标记的mRecreateDisplayList为true
        if (renderNode.hasDisplayList()
                && !mRecreateDisplayList) {
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            dispatchGetDisplayList();

            return renderNode; // no work needed
        }
        //mRecreateDisplayList为true才有机会走到下面的draw
        ……
                    draw(canvas);
        ……
    return renderNode;
}

ViewGroup中的dispatchGetDisplayList,主要就是for循环调用recreateChildDisplayList,recreateChildDisplayList调用child.updateDisplayListIfDirty(); 如此不断往child走,直到遇到有PFLAG_INVALIDATED标记的view(即被调invalidate的那个view),才开始调用draw。

image.png

其他问题

  1. 调用两次requestLayout,会导致流程执行两遍吗?

    requestLayout会调到scheduleTraversals,scheduleTraversals使用了标记字段(mTraversalScheduled)控制, 如果上一个scheduleTraversals发出的消息被执行,mTraversalScheduled会置为false,那么第二次requestLayout会生效。如果上一个scheduleTraversals发出的消息没有被执行,那么第二次requestLayout不会生效;生效与否通过标记位(mTraversalScheduled)控制。

  2. 可以在非主线程更新UI吗?

    在Activity启动流程的resume阶段及之前,通过异步线程进行某些UI操作来调用requestLayout时,因为parent那时还没分分配,所以不会调到ViewRootImpl,也就不会调到requestLayout里的checkThread(),所以不会抛出异常。checkThread里判断了当前现场和创建ViewRootImpl的线程是否是同一个线程。

  3. getMeasureWidth()和getWidth()的区别?

    • getMeasureWidth()是在measure后有值,getWidth()是在layout后有值;
    • View#getWidth()通过mRight - mLeft计算而来,View#layout方法为public,可以通过自定义参数修改mRight、mLeft的值,导致getMeasureWidth()和getWidth()获得的值不一致。
  4. onDraw在什么时候情况下不会被调用?如何让它调用?

    在draw方法中,判断是透明,就不再走onDraw方法。在ViewGroup初始化的时候,它调用了一个私有方法:initViewGroup,它里面会有一句setFlags(WILLL_NOT_DRAW,DRAW_MASK);相当于调用了setWillNotDraw(true),所以说,对于ViewGroup,它就认为是透明的了。所以ViewGroup 一般不会绘制自身,只会绘制子 View,所以不会回调 onDraw(),这也是出于性能和效率的考虑。 如果希望调用,则需要存在背景或者需要在初始化时候手动调用setWillNotDraw(false)。