Android View的绘制流程

974 阅读21分钟

setContentView()

一般我们使用 setContentView() 来给 Activity 设置布局,初学者可能认为执行 setContentView() 之后就马上开始执行绘制流程了,其实并没有,我们来看看 setContentView() 做了什么,本文源码基于 Android 11.0。

public class Activity{

    private Window mWindow;
    
    final void attach(...) {
        ...
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
    }

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        ...
    }

    public Window getWindow() {
        return mWindow;
    }
}

setContentView() 调用了 PhoneWindow 的 setContentView() 方法:

public class PhoneWindow extends Window{
    
    // This is the view in which the window contents are placed. It is either
    // mDecor itself, or a child of mDecor where the contents go.
    ViewGroup mContentParent;

    @Override
    public void setContentView(int layoutResID) {
        ...
        // 第一次 mContentParent 为 null
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        // hasFeature() 方法默认返回 false
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            // 把 layoutResID 对应的布局添加到 mContentParent 中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...
    }
}

第一次进入 setContentView() 方法,mContentParent 为 null,此时调用 installDecor() 方法:

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
    
private void installDecor() {
    // mDecor 即 DecorView
    if (mDecor == null) {
        // 创建 mDecor
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        // setWindow() 为当前 PhoneWindow
        mDecor.setWindow(this);
    }

    if (mContentParent == null) {
        // mContentParent 来自 generateLayout()
        mContentParent = generateLayout(mDecor);
    }
    ...
}

在 installDecor() 方法中,判断如果 mDecor 为 null,则创建 mDecor;如果 mDecor 不为 null,直接与当前 PhoneWindow 关联上。mDecor 是什么,mDecor 是 DecorView,它是这个 PhoneWindow 对应的布局的根布局。接下来调用 generateLayout() 方法:

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

protected ViewGroup generateLayout(DecorView decor) {
    ...
    int layoutResource; 
    int features = getLocalFeatures();
    if ((features & (1 << FEATURE_NO_TITLE)) == 0)){
        ...
    }else if ((features & (1 << FEATURE_ACTION_BAR)) != 0)){
        ...
    }else{
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
    }
    mDecor.startChanging();
    // 将 layoutResource 对应的布局添加到 DecorView 中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    //mContentParent 对应的布局 id 为 com.android.internal.R.id.content;
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

    ...

    return contentParent;
}

从名字也可以看出来,generateLayout() 方法是用来生成布局的,mContentParent 就是通过 generateLayout() 生成的,这里会根据 features 来确定 layoutResource,然后调用 mDecor.onResourcesLoaded() 把 layoutResource 添加到 mDecor 中,mContentParent 对应的就是 layoutResource 中 id 为 com.android.internal.R.id.content 的部分。最后回到前面,把 layoutResID 对应的布局添加到 mContentParent 中。

其中 R.layout.screen_simple 的布局如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />

    <!-- 这个就是上面的 mContentParent -->
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以看到,setContentView(int layoutResID) 只是把 layoutResID 对应的布局添加到 DecorView 中 id 为 content 的 FrameLayout 里面,还没有开始执行绘制流程。

View 的绘制流程

那么执行绘制流程是从什么时候开始的呢?真正执行绘制流程在 Activity 的 onResume() 生命周期方法执行之后,我们来看看 ActivityThread 中的代码:

public final class ActivityThread{

    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
        ...
        // performResumeActivity() 里面调用了 Activity 的 onResume() 方法
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        ...
        final Activity a = r.activity;
        ...
        // decor 即 DecorView, 每个 PhoneWindow 对应一个 DecorView
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        ...
        wm.addView(decor, l);
        ...
    }
}

performResumeActivity() 里面调用了 Activity 的 onResume() 方法, 在 performResumeActivity() 之后执行了 wm.addView(decor, l),wm 来源于 Activity 的 getWindowManager() 方法:

public class Activity{

    private WindowManager mWindowManager;

    final void attach(...){
        ...
        mWindowManager = mWindow.getWindowManager();
        ...
    }

    /** Retrieve the window manager for showing custom windows. */
    public WindowManager getWindowManager() {
        return mWindowManager;
    }

}

最终追溯到 Window 的 getWindowManager() 方法:

public abstract class Window{

    private WindowManager mWindowManager;

     // 设置该 Window 的 WindowManager
     public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
                boolean hardwareAccelerated) {
        ...
        if (wm == null) {
             wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this); // 1
    }

    
    public WindowManager getWindowManager() {
        return mWindowManager;
    }

}

从代码第 12 行可以看到,mWindowManager 来自 WindowManagerImpl 的 createLocalWindowManager() 方法,createLocalWindowManager() 方法返回的是 WindowManagerImpl 对象,所以实际调用的是 WindowManagerImpl 的 addView() 方法:

public final class WindowManagerImpl implements WindowManager {

    // mGlobal 是 WindowManagerGlobal 的单例
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        ...
        mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                mContext.getUserId());
    }
}

接着执行 WindowManagerGlobal 的 addView() 方法:

public final class WindowManagerGlobal {
    // 存储的是所有 Window 所对应的根 View
    private final ArrayList<View> mViews = new ArrayList<View>();
    // 存储的是所有 Window 所对应的 ViewRootImpl
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    // 存储的是所有 Window 所对应的布局参数
    private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
    // 存储的是那些正在被删除的 View 对象,或者说是那些已经
    // 调用 remoView() 方法但是删除操作还未完成的 Window 对象
    private final ArraySet<View> mDyingViews = new ArraySet<View>();

    private static WindowManagerGlobal sDefaultWindowManager;

    public static WindowManagerGlobal getInstance() {
        synchronized (WindowManagerGlobal.class) {
            if (sDefaultWindowManager == null) {
                sDefaultWindowManager = new WindowManagerGlobal();
            }
            return sDefaultWindowManager;
        }
    }

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
        // 检查参数的合法性
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }
        ...
        // 如果是子 Window 还需要调整布局参数
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        }   

        ViewRootImpl root;
        ...
        // 创建 ViewRootImpl 实例
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView, userId);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
        }
    }
}

addView() 方法里面执行了 ViewRootImpl 的 setView() 方法:

public final class ViewRootImpl{

	final IWindowSession mWindowSession;

   public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
            
       ...
       // 发起绘制流程
       requestLayout();
       ...
       // 通过 WindowSession 最终完成 Window 的添加
       res = mWindowSession.addToDisplayAsUser(...) 
       ...

	}

	 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            // 检查线程
            checkThread();
            mLayoutRequested = true;
            // 绘制的入口
            scheduleTraversals();
        }
    }

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

	void scheduleTraversals() {
        // 添加了 mTraversalScheduled 标志位,所以连续两次 setText() 只会执行一次绘制流程
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            // 添加同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 提交给编舞者,会在下一帧绘制时调用 mTraversalRunnable ,运行其 run() 方法
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
		 ...
        }
    }

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

  final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

	void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            // 移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            ...
            performTraversals();
            ...
        }
    }

	private void performTraversals() {
        ......
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ...
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 测量
        ...
        performLayout(lp, mWidth, mHeight); // 布局
        ...
        performDraw(); // 绘制
        ...

	}

}

requestLayout() 方法会发起绘制流程,并最终调用 performTraversals() 方法。在 performTraversals() 方法中,首先通过 getRootMeasureSpec() 方法获取最外层根视图的 MeasureSpec 赋值给 childWidthMeasureSpec 和 childHeightMeasureSpec。

MeasureSpec

什么是 MeasureSpec? MeasureSpec 是一个 32 位 int 值,高 2 位表示 SpecMode ,低 30 位表示 SpecSize,SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。确切来说,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格,之所以说是很大程度上,因为这个过程还受父容器的影响,父容器影响 View 的 MeasureSpec 的创建过程,下面先来看看 MeasureSpec 内部的代码:

public class View{
    
    public static class MeasureSpec {
    
        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;

        // 根据传入的 size 和 mode 来确定 MeasureSpec 的值
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                // MODE_MASK为:11000000 00000000 00000000 00000000
                // ~MODE_MASK为:00111111 11111111 11111111 11111111
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

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

MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode 和 SpecSize 也是一个 int 值,一组 SpecMode 和 SpecSize 可以打包为一个 MeasureSpec,而一个 MeasureSpec 可以通过解包的形式来得出其原始的 SpecMode 和 SpecSize,需要注意的是这里提到的 MeasureSpec 是指 MeasureSpec 所代表的 int 值,而并非 MeasureSpec 本身。

SpecMode有三类:

  • UNSPECIFIED,父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
  • EXACTLY,父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。
  • AT_MOST,父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。

前面通过 getRootMeasureSpec() 方法来获取 DecorView 的 MeasureSpec,代码如下:

public final class ViewRootImpl{
  
    // DecorView 的 MeasureSpec
    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 中的宽/高的参数来划分:

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小:
  • 固定大小(比如100dp):精确模式,大小为 LayoutParams 中指定的大小。

由此可知,DecorView 的 MeasureSpec 由窗口尺寸(windowSize)和自身的 LayoutParams 共同决定。

measure 过程

接下来把 DecorView 的 MeasureSpec 传给了 performMeasure() 方法,代码如下:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
	 ...
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

接下来执行根 View 的 measure() 方法:

public class View{

    // final 方法,子类不可重写
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
    }
}

measure() 方法里面调用了 onMeasure() 方法,由于 DecorView 是 FrameLayout 的子类,接下来执行 FrameLayout 的 onMeasure() 方法(其他比如 LinearLayout、RelativeLayout 等 ViewGroup 的子类都会根据自己的特性重写 onMeasure() 方法):

public class FrameLayout extends ViewGroup {

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

	     int count = getChildCount();

        int maxHeight = 0;
        int maxWidth = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 先测量子 View 的宽高
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                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);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
        ...
        // 测量自己的宽高
	     setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
	}
}

可以看到,这里会遍历子视图并执行 measureChildWithMargins() 方法,在这个方法内部测量子视图的宽高,最后调用 setMeasuredDimension() 方法测量自己的宽高。measureChildWithMargins() 在 ViewGroup 中:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {

        // 获取子视图的 LayoutParams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        // 通过父视图的测量规格以及子视图本身的 LayoutParams 来共同决定子视图的测量规格
        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);
        // 执行子 View 的 measure() 方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}

在这里会获取子视图的 MeasureSepc 并调用子视图的 measure() 方法。可以看到,子视图的 MeasureSpec 由父容器的 MeasureSpec 和自己的 LayoutParams 共同决定。

其中 childWidthMeasureSpec 是通过 getChildMeasureSpec() 方法获取的,代码如下:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
}

上述方法不难理解,它的主要作用是根据父容器的 MeasureSpec 同时结合 View 本身的 LayoutParams 来确定子元素的 MeasureSpec,参数 padding 是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的尺寸减去 padding,具体代码如下所示:

int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);

getChildMeasureSpec() 清楚展示了普通 View 的 MeasureSpec 的创建规则,为了更清晰地理解 getChildMeasureSpec() 的逻辑,这里提供一个表,表中对 getChildMeasureSpec() 的工作原理进行了梳理。注意,表中的 parentSize 是指父容器中目前可使用的大小。 image.png 针对不同的父容器和 View 本身不同的 LayoutParams,View 就可以有多种 MeasureSpec。这里简单说一下,当 View 采用固定宽/高的时候,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 Layoutparams 中的大小。当 View 的宽/高是 match_parent 时,如果父容器的模式是精准模式,那么 View 也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。当 View 的宽/高是 wrap_content 时,不管父容器的模式是精准还是最大化,View 的模式总是最大化并且大小不能超过父容器的剩余空间。至于 UNSPECIFIED 模式,该模式主要用于系统内部多次 Measure 的情形,一般来说,我们不需要关注此模式。

接下来继续执行子 View 的 measure() 方法,如果子 View 为普通的 View ,则执行 View 的 onMeasure() 方法,代码如下:

public class View{

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

setMeasuredDimension() 方法最终调用了 setMeasuredDimensionRaw() 对 mMeasuredWidth 和 mMeasuredHeight 赋值。

上面的代码中我们看到 View 的 mMeasuredWidth 和 mMeasuredHeight 是通过 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() 方法中,SpecMode 为 MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 时都返回了 specSize,这个 specSize 就是 View 测量后的大小。这里多次提到测量后的大小,是因为 View 最终的大小是在 layout 阶段确定的,所以这里必须要加以区分,但是几乎所有的情况下,View 的测量大小和最终大小是相等的。

如果你通过直接继承 View 来实现自定义 View ,并在 xml 中配置宽高为 wrap_content 时,实际效果却是该自定义 View 把剩下的空间都占满了,跟配置宽高为 match_parent 没什么区别。为什么呢?因为如果在 xml 中使用 wrap_content,则通过 getChildMeasureSpec() 拿到的该 View 的 specMode 就是 AT_MOST 模式,specSize 为父容器中目前可使用的大小 parentSize,再结合 getDefaultSize() 方法拿到 View 的宽/高就等于父容器中目前可使用的大小。

如何解决这个问题呢,也很简单,代码如下:

public class MyView extends View {

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 你可以自己给 mWidth 和 mHeight 赋值为实际包裹内容的宽高
        int mWidth = 100;
        int mHeight = 100;
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        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);
        }
    }
}

我们只需要给 View 指定一个默认的内部宽/高(mWidth 和 mHeight),并在 wrap_content 时设置此宽/高即可。对于非 wrap_content 情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。如果查看 TextView、ImageView 等的源码就可以知道,针对 wrap_content 情形,它们的 onMeasure() 方法均做了特殊处理。

至于前面 getDefaultSize() 方法中 UNSPECIFIED 这种情况,一般用于系统内部的测量过程,在这种情况下,View 的大小为 getDefaultSize() 的第一个参数 size,即宽/高分别为 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 这两个方法的返回值,看一下它们的源码:

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

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

从 getSuggestedMinimumWidth() 的代码可以看出,如果 View 没有设置背景,那么 View 的宽度为 mMinWidth,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值。这个属性如果不指定,那么 mMinWidth 则默认为 0;如果 View 指定了背景,则 View 的宽度为 max(mMinWidth,mBackground.getMinimumWidth())。那么 mBackground.getMinimumWidth() 是什么呢?我们看一下 Drawable 的 getMinimumWidth() 方法,如下所示:

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

可以看出,getMinimumWidth() 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就返回 0。那么 Drawable 在什么情况下有原始宽度呢?举例说明一下,ShapeDrawable 无原始宽/高,而 BitmapDrawable 有原始宽/高(图片的尺寸)。

View 的 measure 过程是三大流程中最复杂的一个,measure 完成以后,通过 getMeasuredWidth() 或 getMeasuredHeight() 方法就可以正确地获取到 View 的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,在这种情形下,在 onMeasure() 方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在 onLayout() 方法中去获取 View 的测量宽/高或者最终宽/高。

总结一下,整个 View 视图是一个树结构的视图:

image.png

measure 过程就是从 DecorView 开始自顶向下遍历递归调用 View 的 measure() 方法( measure() 又调用 onMeasure() 方法)并最终对 mMeasuredWidth 和 mMeasuredHeight 赋值的过程。

layout 过程

Layout 的作用是 ViewGroup 用来确定子元素的位置的,当 ViewGroup 的位置被确定后,它在 onLayout() 中会遍历所有的子元素并调用其 layout 方法。

接下来分析 performLayout() 方法,如下:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
	...
   final View host = mView;
	...
	host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
	...
}

ViewGroup 的 layout() 方法为 final 类型,不可以重写:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    @Override
    public final void layout(int l, int t, int r, int b) {
        ...
        super.layout(l, t, r, b);
        ...
    }
}

接着调用 View 的 layout() 方法:

public class View{

    public void layout(int l, int t, int r, int b) {
        ......
        // 实质都是调用 setFrame() 方法把参数分别赋值给 
        // mLeft、mTop、mRight 和 mBottom 这几个变量,
        // 判断 View 的位置是否发生过变化,以确定有没有必要对当前的 View 进行重新 layout
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        // 需要重新 layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            // 调用 onLayout() 方法
            onLayout(changed, l, t, r, b);
            ...
        }
        ...
    }

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        ...
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            ...
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            ...
        }
        return changed;
    }
}

layout() 方法中根据 isLayoutModeOptical(mParent) 来判断执行 setOpticalFrame() 方法还是 setFrame() 方法,点进 setOpticalFrame() 方法,发现最终还是调用的 setFrame() 方法来初始化 mLeft、mTop、mRight 和 mBottom 这四个值,View 的四个顶点值一旦确定,那么 View 在父容器中的位置也就确定了。

接着执行 onLayout() 方法, View 的 onLayout() 方法是一个空方法, ViewGroup 的 onLayout() 方法是一个抽象方法,意味着 ViewGroup 的子类都要重写这个方法。 DecorView 是 FrameLayout 的子类,接下来调用 FrameLayout 的 onLayout() 方法:

public class FrameLayout extends ViewGroup {

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
      layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            ...
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

这里 onLayout() 方法又调用了 layoutChildren() 方法,在这里会遍历所有的子视图,并调用子视图的 layout() 方法来确定自己的位置,这样一层一层地传递下去,就完成了整个 View 树的 layout 过程。

那么 View 的测量宽/高和最终宽/高有什么区别呢?这个问题可以具体为:View 的 getMeasuredWidth() 与 getWidth() 有什么区别。我们来看一下 getWidth() 、 getHeight() 和 getMeasuredWidth() 、 getMeasuredHeight() 这两对方法在 View 源码中的具体实现:

public class View{

    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

    public final int getMeasuredHeight() {
         return mMeasuredHeight & MEASURED_SIZE_MASK;
    }

    public final int getWidth() {
        return mRight - mLeft;
    }

    public final int getHeight() {
        return mBottom - mTop;
    }
}

前面看到在 performLayout() 方法中,给 layout() 方法传递的参数是 host.getMeasuredWidth() 和 host.getMeasuredHeight(),再结合 mLeft、mRight、mTop、mBottom 这四个变量的赋值过程,可以发现:getWidth() 的返回值刚好就是 View 的测量宽度,而 getHeight() 的返回值也刚好就是 View 的测量高度。经过上述分析,现在我们可以回答这个问题了:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。因此,在日常开发中,我们可以认为 View的测量宽/高就等于最终宽/高,但是的确存在某些特殊情况会导致两者不一致,下面举例说明。

如果重写 View 的 layout() 方法,代码如下:

public final void layout(int l, int t, int r, int b) {
    super.layout(l, t, r + 100, b + 100);
}

上述代码会导致在任何情况下 View 的最终宽/高总是比测量宽/高大 100px,虽然这样做会导致 View 显示不正常并且也没有实际意义,但是这证明了测量宽/高的确可以不等于最终宽/高。另外一种情况是在某些情况下,View 需要多次 measure 才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同。

draw 过程

接下来分析 performDraw() 方法:

public final class ViewRootImpl{

    private void performDraw() {
       ...
       boolean canUseAsync = draw(fullRedrawNeeded);
       ...
    }
    
    private boolean draw(boolean fullRedrawNeeded) {
	   ...
      if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
          ...
          mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this); //硬件加速绘制
      }else{
          ...
          // 软件绘制
          if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                scalingRequired, dirty, surfaceInsets)) {
            return false;
          }
      }
      ...
    }
    
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
       ...
       mView.draw(canvas);
       ...
    }
}

接下来调用 View 的 draw() 方法:

public class View{

    public void draw(Canvas canvas) {
        /*
         * 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
        // 步骤 1. 绘制背景
        int saveCount;

        drawBackground(canvas);

        // skip step 2 & 5 if possible (common case)
        // 步骤 2. 如果需要的话,保存 canvas 的图层,为 fading 做准备
        // 步骤 5. 如果需要的话,绘制 fading edges 和恢复图层
        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
            // 步骤 3. 绘制 View 的内容
            onDraw(canvas);

            // Step 4, draw the children
            // 步骤 4. 绘制 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)
            // 步骤 6. 绘制装饰 (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            // 步骤 7. 绘制默认的焦点高亮显示
            drawDefaultFocusHighlight(canvas);

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

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

        ...
    }
}

draw 过程的作用是将 View 绘制到屏幕上,主要分为以下几步:

  1. 绘制背景 background.draw(canvas)。
  2. 绘制自己(onDraw)。
  3. 绘制 children(dispatchDraw)。
  4. 绘制装饰(onDrawForeground)。

View 的 onDraw() 方法是一个空方法,说明 View 默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。View 的绘制过程的传递是通过 dispatchDraw() 来实现的,在 ViewGroup 中对 dispatchDraw() 方法进行了重写,代码如下:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    protected void dispatchDraw(Canvas canvas) {
        ...
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ...
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    // 调用 drawChild() 方法
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
            }

        }
        ...
        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            ...
            for (int i = disappearingCount; i >= 0; i--) {
                final View child = disappearingChildren.get(i);
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ...
    }

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
}

dispatchDraw() 方法里面遍历每个子 View,调用子 View 的 draw() 方法,如此 draw 事件就一层一层地传递了下去。

View 中有一个特殊的方法 setWillNotDraw(),其源码如下所示:

/**
 * If this view doesn't do any drawing on its own, set this flag to
 * allow further optimizations. By default, this flag is not set on
 * View, but could be set on some View subclasses such as ViewGroup.
 *
 * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
 * you should clear this flag.
 *
 * @param willNotDraw whether or not this View draw on its own
 */
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

从注释可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 tue 以后,系统会进行相应的优化。默认情况下,View 没有启用这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDraw() 来绘制内容时,我们需要显式地关闭 WILL_NOT_DRAW 个标记位。