基于源码分析 Android View 绘制机制

2,184 阅读8分钟

在 Android 的知识体系中,View 扮演者很重要的角色。View 是 Android 在视觉上的呈现。本文结合 android-28 的源码来分析 View 的绘制过程。

ViewRootImpl

ViewRootImpl 类是连接 WindowManagerDecorView 的纽带,View 的绘制流程均是通过 ViewRootImpl 来完成的。

// ActivityThread.java

    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        
        ...
        
        if (r.window == null && !a.mFinished && willBeVisible) {
            // 获取 WindowManager 及 DecorView
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    // 将 DecorView 添加到当前 Window 中
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }

        ...
    }

上面的代码说明,在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 通过 WindowManager 添加到 Window 中。

// WindowManagerImpl.java

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

可以知道最终是通过 WindowManagerGlobaladdView 方法来将 DecorView 添加到 Window

// WindowManagerGlobal

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
       ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...

            // 初始化 ViewRootImpl 并将 ViewRootImpl 对象和  DecorView 建立关联
            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);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

上述代码创建了 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。最终在 setView 方法中,会执行 ViewRootImplrequestLayout 方法来执行 View 的绘制流程

// ViewRootImpl.java

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

scheduleTraversals 方法最终会调用 performTraversals 方法,经过 measurelayoutdraw 三个过程才能最终将一个 View 绘制出来

  • meausre: 用来测量 View 的宽和高
  • layout: 用来确定 View 在父容器中的放置位置
  • draw: 负责将 View 绘制在屏幕上

如图所示,performTraversals 会依次调用 performMeasureperformLayoutperformDraw 三个方法,这三个方法分别完成顶级 Viewmeasurelayoutdraw 这三大流程。其中在 performMeasure 中会调用 measure 方法,在 measure 方法中又会去调用 onMeasure 方法,在 onMeasure 方法中又会对所有的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到了子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。通过 performLayoutperformDraw 的传递流程跟 performMeasure 类似

MeasureSpec

为了更好地理解 View 的测量过程,我们还需要理解 MeasureSpecMeasureSpec 参与了 Viewmeasure 过程,在很大程度上决定了一个 View 的尺寸规格,但父容器也会影响 ViewMeasureSpec 的创建过程。在测量过程中,系统会将 ViewLayoutParams 根据父容器所设置的规则转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 来测量出 View 的宽和高。

// View.java

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        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(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode 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);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        
        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }
        
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }

MeasureSpec 代表一个 32 位的 int 值,高 2 位代表 SpecMode, 低 30 位代表 SpecSize

  • SpecMode: 测量模式
  • SpecSize: 在某种测量模式下的规格大小

MeasureSpec 通过将 SpecModeSpecSize 打包成一个 int 值来避免过多的对象内存分配makeMeasureSpec 是打包方法,getModegetSize 则为解包方法。

SpecMode 有三类,每一类都标识特殊的含义

UNSPECIFIED

父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,标识一种测量的状态

EXACTLY

父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式

AT_MOST

父容器指定了一个可用大小即 SpecSizeView 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content

MeasupreSpec 和 LayoutParams

MeasureSpec 不是唯一由 LayoutParams 决定的,LayoutParams 需要和父容器一起才能决定 ViewMeasureSpec,从而进一步决定 View 的宽和高。

对于 DecorView,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同确定;对于普通的 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同局诶的那个,MeasureSpec 一旦确定后, onMeasure 中就可以确定 View 的测量宽和高

DecorView 创建 MeasureSpec

对于 DecorView 来说,它的 MeasureSpec 创建过程是由 getRootMeasureSpec 方法来完成的

// ViewRootImpl.java

    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: // 精确模式,大小为 LayoutParams 中指定的大小
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

通过上述代码,DecorViewMeasureSpec 的产生过程就很明确了,具体来说其遵守如下规则,根据它的 LayoutParams 中的宽和高参数来划分

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

ViewRootImplperformTraversals 方法中调用 getRootMeasureSpec 获取到 childWidthMeasureSpecchildHeightMeasureSpec 后,会传给 performMeasure 方法,最终调用 DecorViewmeasure 方法

普通 View 创建 MeasureSpec

对于普通 View 来说,即布局中的 ViewViewmeasure 过程由 ViewGroup 传递而来,在 ViewGroupmeasureChildWithMargins 方法

// ViewGroup.java

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

measureChildWithMargins 方法一般会在自定义 Layout 组件的 onMeasure 方法中调用(如 FrameLayout, LinearLayout),来测量子元素的规格。在调用子元素的 measure 方法之前会先通过 getChildMeasureSpec 方法来得到子元素的 MeasureSpec。通过上面代码可知,子元素的 MeasureSpec 的创建和父容器的 MeasureSpec 和子元素本身的 LayoutParams 有关,此外还和 Viewmarginpadding 有关

// ViewGroup.java

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 父容器的 mode 和 size
        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);
    }

getChildMeasureSpec 函数主要的作用是根据父容器的 MeasureSpec 同时结合 View 本身的 LayoutParams 来确定子元素的 MeasureSpec

View 采用固定宽和高的时候,不管父容器的 MeasureSpec 是什么,ViewMeasureSpec 都是精确模式并且其大小遵循 LayoutParamas 中的大小。当 View 的宽和高是 match_parent 时,如果父容器的模式是精确模式,那么 View 也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。如果父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。当 View 的宽和高是 wrap_content 时,不管父容器的模式是精准还是最大化,View 的模式总是最大化并且大小不能超过父容器的剩余空间。

View 工作流程