【Android】View 的工作原理

58 阅读8分钟

ViewRootImpl 和 DecorView

ViewRootImpl 是连接 WindowManager 和 DecorView 的纽带,View 绘制流程的 onMeasure、onLayout 和 onDraw 均通过 ViewRootImpl 完成。Activity 创建后,DecorView 会添加到 Window 中,同时会创建 ViewRootImpl 对象,将其与 DecorView 相关联。

root = new ViewRootImpl(view.getContext(), display); 
/* display标识显示设备 */
root.setView(view, wparams, panelParentView); 
/* WindowManager.LayoutParams wparams, Window panelParentView */

View 的绘制从 ViewRootImpl 的 performTraversals 方法开始,该方法分别依次调用 performMeasure、performLayout 和 performDraw,这 3 个方法本质完成了 mView.measure、mView.layout 和 mView.draw,mView 是关联源码中的 view,也就是程序的 DecorView。

在 measure、layout 和 draw 各自方法内部会调用 onMeasure、onLayout 和 onDraw,而在 onMeasure 等方法再次对所有子元素进行 measure 等过程,此时绘制流程就从父元素传递到子元素中了,如此反复即可完成整个 View 树的遍历绘制。

DecorView 会包含竖直方向的 LinearLayout,该布局有上方标题栏和下方内容栏两部分,Activity 中通过 setContentView 方法设置的布局文件会被加到内容栏中,内容栏的 Android id 是 content,布局类型为 FrameLayout。可以通过 ViewGroup content = findViewById(android.R.id.content) 获得 content 实例,进一步 content.getChildAt(0) 获得我们 setContentView 的布局实例。

MeasureSpec

MeasureSpec 受父容器施加规则的影响,其决定 View 的尺寸规格。MeasureSpec 标识 32 位 int 值,高 2 位表示测量模式 SpecMode,低 30 位表示测量模式下的规格大小 SpecSize。

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

/* 3 种测量模式 */
private static final int UNSPECIFIED = 0 << MODE_SHIFT; /* 00 */
private static final int EXACTLY = 1 << MODE_SHIFT;     /* 01 */
private static final int AT_MOST = 2 << MODE_SHIFT;     /* 10 */

public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
    /*  MODE_MASK = 11 000000000000000000000000000000 
        ~MODE_MASK = 00 111111111111111111111111111111
        size & ~MODE_MASK将size的高2位清空,mode & MODE_MASK同理
    */
}

public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

SpecMode 有 UNSPECIFIED、EXACTLY 和 AT_MOST 3 类:

  1. UNSPECIFIED:未指定大小限制,一般用于 ListView 这种可滚动容器;
  2. EXACTLY:父容器告诉子 View 必须遵守的明确尺寸;
  3. AT_MOST:父布局给子 View 的最大尺寸限制。

父容器和子 View 的 LayoutParams 共同决定子 View 的 MeasureSpec,进一步决定 View 的宽高。例外是 DecorView,其 MeasureSpec 由 Window 尺寸和自身的 LayoutParams 确定,代码块的 getRootMeasureSpec 方法得知,DecorView 根据它的 LayoutParams 的宽高参数来划分:match_parent 对应 EXACTLY 模式,大小就是窗口大小、wrap_content 对应 WRAP_CONTENT 模式,大小不能超过窗口大小;硬解码尺寸对应 EXACTLY 模式,大小为 LayoutParams 指定的大小。

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

private static int getRootMeasureSpec(int windowSize, int measurement, int privateFlags) {
    int measureSpec;
    final int rootDimension = (privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0
            ? MATCH_PARENT : measurement;
    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 来说,其 measure 过程由 ViewGroup 的 measureChildWithMargins 和 getChildMeasureSpec 方法完成。

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);
} 
/* mPaddingLeft等属性是父容器的padding */
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 them 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 = 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 = size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
子尺寸 & 父模式EXACTLYAT_MOSTUNSPECIFIED
具体尺寸EXACTLY childSizeEXACTLY childSizeEXACTLY childSize
match_parentEXACTLY parentSizeAT_MOST parentSizeUNSPECIFIED 0
wrap_contentAT_MOST parentSizeAT_MOST parentSizeUNSPECIFIED 0

View 的工作流程

工作流程即前面提到的 measure 测量、layout 布局和 draw 绘制。measure 确定 View 的测量宽高、layout 确定 View 的最终宽高和 4 顶点的位置、draw 将 View 绘制到屏幕上。

measure 过程

View 的 measure 过程由 measure 方法完成,measure 方法是 final 方法,会去调用 View 的 onMeasure 方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasureDimenson(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
        getDefaultSize(getSuggestedMinimumHeight(), HeightMeasureSpec));
}

setMeasureDimenson 方法会设置 View 的宽高测量值,参数内的 getDefaultSize 方法返回的大小就是 measureSpec 的 specSize,specSize 相当于 View 测量后的大小。

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

注意到 AT_MOST 和 EXACTLY 模式均返回 SpecSize,这表示基本 View 设置宽高参数为 match_parent 和 wrap_content 是相同的逻辑,如果要自定义 View,则大多数情况下都要重新 onMeasure 方法。

对于 UNSPECIFIED 情况,View 的大小为 getDefaultSize 的第一个参数 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 的返回值。

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

若 View 没有设置背景,则 getSuggestedMinimumWidth 返回 mMinWidth,如果有设置背景,则 getSuggestedMinimumWidth 返回背景 Drawable 的原始尺寸 getIntrinsicWidth。


ViewGroup 的 measure 过程会另外遍历调用子元素的 measure 方法,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);
        }
    }
}

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 方法要各个子类去重写。

measure 过程完成后,通过 getMeasuredWidth 和 getMeasuredHeight 方法可以获取 View 的测量尺寸,前面我们提到,Activity 创建后,DecorView 会添加到 Window 中,同时创建并关联 ViewRootImpl 对象,ViewRootImpl 对象调用 performTraversals 方法开始绘制 DecorView 以及整个 View 树,其具体的发生时机是 ActivityThread 内部回调 Activity 的 onResume 方法后,调用 WindowManager 的 addView 方法,内部会由 ViewRootImpl 接手执行 View 绘制逻辑。

所以 View 的 measure 过程和 Activity 的生命周期不是同步的,无法在 onCreate、onStart 和 onResume 方法获得正确的测量宽高,大多数情况下会返回 0。有 3 种方法来解决这个问题:

  1. Activity 和 View 的 onWindowFocusChanged 方法均在当前窗口的焦点获得情况发生变化时回调,窗口获得焦点时,View 树的绘制流程是完成的,所以可以通过重写 onWindowFocusChanged 方法来获得 View 的测量宽高。
  2. View 的 post 方法可以将 Runnable 发送到当前 Thread 的消息队列的尾部,因为 View 的绘制工作都会发送到主线程的消息队列按次序处理,所以不用在 Runnable 保证 View 一定完成了 measure 流程,直接在 Activity 的回调方法中操作即可。
  3. ViewTreeObserver 类有众多回调接口,使用 OnGlobalLayoutListener 接口会在 View 树的状态发生改变或 View 树内部的 View 的 Visibility 发生改变时回调内部的 onGlobalLayout 方法,也就是在 View 树完成 layout 流程后,接下来的 draw 流程前回调。
protected void onStart() {
    super.onStart();
    
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        
        @SuppressWarnings("deorecation")
        @Override
        public void onGlobalLayout() {
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    })
}

layout 过程

layout 是 ViewGroup 用来确定子元素的位置的流程,当 ViewGroup 的位置确定后, 其在 onLayout 会遍历所有子元素且调用子元素的 layout 的方法。

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_MEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_MEEDED_BEFORE_LAYOUT;
    } /* View是否重新测量的相关决策 */
    
    int oldL = mLeft;
    int oldT = mTop;
    int oldR = mRight;
    int oldB = mBottom;
    
    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 4 顶点的位置,初始化 mLeft、mTop、mRight 和 mBottom 4 个值,View 的各顶点确定,其在父容器的位置也确定,接下来会调用 onLayout 方法,和 onMeasure 相似的,onLayout 方法在 ViewGroup 同样没有具体实现,要各子类重写。

draw 过程

draw 过程会经历 4 步:绘制背景 background.draw(canvas)、绘制自己 onDraw、绘制 children 的 dispatchDraw 和绘制装饰 onDrawScrollBars。View 绘制过程的传递通过 dispatchDraw 方法实现的,dispatchDraw 会遍历调用所有子元素的 draw 方法,所以 View 3 种工作流程的 View 树传递机制都是类似的

@CallSuper
public void draw(@NonNull Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    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)
*      7. If necessary, draw the default focus highlight
*/

 // Step 1, draw the background, if needed
int saveCount;

    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
onDraw(canvas);

        // Step 4, draw the children
dispatchDraw(canvas);

        drawAutofilledHighlight(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);

        // Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);

        if (isShowingLayoutBounds()) {
            debugDrawFocus(canvas);
        }

        // we're done...
return;
    }
    
    ...
}