View—View绘制的三大流程

作者: SharryChoo | 来源:发表于2018-01-18 00:36 被阅读40次

    View 三大流程的发起点

    从 Window 机制探索中, 我们看到在 WindowManagerGlobal 调用 addView 方法时, 会走到 ViewRootImpl.setView 方法中去, 从而触发 performTraversals 方法, 下面就来看看这个方法做了哪些处理

        /**
         * ViewRootImpl.performTraversals 方法
         */
        private void performTraversals() {
            final View host = mView;
            if (host == null || !mAdded)
                return;
            // 更新标记位, 正在 Traversal
            mIsInTraversal = true;
    
            // ... 这里省略了数百行代码
            WindowManager.LayoutParams lp = mWindowAttributes;
            if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult & WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    // 这个 host 为 Window 下的第一个 View, 它的宽高一般就是制定的 Window 的宽高
                    // ViewRootImpl, 其实就是用于处理 Window 下最直接的 View, 用 ViewRootImpl 来命名就显而易见了
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                    // 1. 调用 performMeasure 开启 View 树的测量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;
                    // 判断是否需要重新 performMeasure
                    // 保证宽度权重大于0
                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    // 保证高度权重大于0
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (measureAgain) {
                        // 从新测量一波
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }
                    layoutRequested = true;
                }
            }
    
            final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
            if (didLayout) {
                // 2. 调用 performLayout 确定 View 的位置
                performLayout(lp, mWidth, mHeight);
            }
            boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
            if (!cancelDraw && !newSurface) {
                // 3. 调用 performDraw 开启 View 的绘制
                performDraw();
            } else {
                if (isViewVisible) {
                    // Try again
                    scheduleTraversals();
                }
            }
            mIsInTraversal = false;
        }
    

    ViewRootImpl.performTraversals 方法一共有 700 多行代码, 这里只分析与 View 相关的核心部分, 这个方法做了如下的事情

    1. ViewRootImpl.performMeasure, 测量 View 的大小
      • 若宽高的权重不大于0, 则需要重新测量 View 大小
    2. ViewRootImpl.performLayout 确定 View 摆放的位置
    3. ViewRootImpl.performDraw 开启 View 的绘制

    下面逐个分析 View 的三大流程

    View 的测量

        /**
         * ViewRootImpl.performMeasure
         */
        private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            if (mView == null) {
                return;
            }
            try {
                // 很简单直接调用了 mView 的 measure 方法
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
        
        /**
         * View.measure
         */
        public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
                Insets insets = getOpticalInsets();
                int oWidth  = insets.left + insets.right;
                int oHeight = insets.top  + insets.bottom;
                widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
                heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
            }
            // 1 判断是否开启了强制 Layout
            final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
            // 2 与缓存的 mOldMeasureSpec 进行一系列的比较判断得到最终的Flag needsLayout
            // 2.1 判断 MeasureSpec 是否与缓存的一致
            final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                    || heightMeasureSpec != mOldHeightMeasureSpec;
            // 2.2.1 判断 MeasureSpec 的测量模式是否为 MeasureSpec.EXACTLY 
            final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                    && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
            // 2.2.2 判断 MeasureSpec 的测量值是否与当前View测量值一致
            final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                    && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
            // 2.3 得出最终 Flag 的值
            final boolean needsLayout = specChanged
                    && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
            // 若添加了强制测量的 Flag 或者 needsLayout = true 才会走下面的代码
            if (forceLayout || needsLayout) {
                int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // 3.1 无缓存: 这里回调用我们最最熟悉的 onMeasure 
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                } else {
                    // 3.2 有缓存: 直接调用 setMeasuredDimensionRaw
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                }
            }
            // 更新缓存的 MesureSpec
            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;
    
            mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL);
        }
    

    可以看到 measure 方法做了如下的事情:

    1. 获取 forceLayout 和 needLayout 这两个 Flag 的值
    2. 可以看到 needLayout 这里做了很多的判断, 可以根据其判断流程做一些优化, 防止过度 Measure
      • 判断 XXXMeasureSpec 是否与缓存的 mOldXXXMeasureSpec 一致
      • 判断 MeasureSpec 的测量模式是否为 MeasureSpec.EXACTLY
      • 判断 MeasureSpec 的测量值是否与当前View测量值一致
    3. 根据这个两个 Flag 来决定是否需要启动 View 的测量
      • 无缓存: onMeasure
      • 有缓存: setMeasuredDimensionRaw

    注意:

    ViewGroup extends View, ViewGroup 中默认是没有重写 onMeasure 的

    所以当我们继承 ViewGroup 写自定义 View 的时候必须要重写 onMeasure 方法, 一般步骤如下

        /**
         * 重写 View 的 onMeasure 方法
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            // 1. 获取从上层传递下来的 Mode 和 Size
            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
            // 2. 遍历测量子 View 大小
            int count = getChildCount();
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                // 调用了这个 measureChildWithMargins 
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            }
            // 3. 根据子 View 大小来设置自己的大小
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec,
                            childState << MEASURED_HEIGHT_STATE_SHIFT));
        }
        
        /**
         * ViewGroup.measureChildWithMargins
         */
        protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            // 测量 View 的大小, 连同其设置的 margin 参数
            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);
            // 可以看到最终又回调了 View 的 measure 方法, 这样就一层一层的将 View 的  measure 传递下去了
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    可以看到 ViewGroup 中的 Measure 主要做了两件事情:

    1. 遍历子 View, 将 measure 操作向下分发

    2. 子 View 全部测量好之后, 根据自身布局的特性, 设置自身大小

    接下来看看 View.onMeasure 方法

        /**
         * View.onMeasure
         */
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(
                getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
            );
        }
        
        /**
         * View.getSuggestedMinimumWidth
         */
        protected int getSuggestedMinimumWidth() {
            // 获取建议的最小宽度, 即背景的宽度
            return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
        }
        
        /**
         * View.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:// 未指定的
                // 当未指定的时候, View 的大小默认为 size, 通过上面得知为其 Background 的宽高
                result = size;
                break;
            // 可见如果不重写 View 的 onMeasure 方法的话, 其 wrap_content 的作用于 match_parent 是一样的
            case MeasureSpec.AT_MOST:// 最大的: wrap_content
            case MeasureSpec.EXACTLY:// 精确的: match_parent 和 10dp
                result = specSize;
                break;
            }
            return result;
        }
        
        /**
         * View.setMeasureDimension
         */
        protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
                Insets insets = getOpticalInsets();
                int opticalWidth  = insets.left + insets.right;
                int opticalHeight = insets.top  + insets.bottom;
    
                measuredWidth  += optical ? opticalWidth  : -opticalWidth;
                measuredHeight += optical ? opticalHeight : -opticalHeight;
            }
            // 调用者这个方法
            setMeasuredDimensionRaw(measuredWidth, measuredHeight);
        }
        
        /**
         * View.setMeasuredDimensionRaw
         */
        private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
            // 给成员变量赋值
            mMeasuredWidth = measuredWidth;
            mMeasuredHeight = measuredHeight;
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
        }
    

    总结:
    Measure 操作的过程

    1. 将 MeasureSpec 经过自身处理后, 分发到下层子 View
    2. 子 View 确定了大小之后, 再回溯到父容器中
    3. 父容器结合子 View 大小自身布局特性 来确定自己的大小, 一直回溯到顶层

    可以看到 View 的 onMeasure 中还是有一些细节的, 在我们自定义 View 中起到至关重要的作用:

    1. 当测量模式为 MeasureSpec.UNSPECIFIED 时, View 的大小默认为 getSuggestedMinimumWidth() 即背景的宽高
    2. View 默认是不支持 wrap_content 属性的, 源码中它直接走到了下面的 MeasureSpec.EXACTLY 才得以 break, 我们需要重写 onMeasure, 让其支持 wrap_content
    3. 当 View 的 setMeasureDimension() 执行后, 我们可以通过 getMeasureWidth/Height() 获取其初步的宽高值

    View 位置的确定

        /**
         * ViewRootImpl.performLayout
         */
        private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                int desiredWindowHeight) {
            mInLayout = true;
            final View host = mView;
            if (host == null) {
                return;
            }
            try {
                // 调用了 View.layout 方法, 将他的边界传入
                // 因为 ViewRootImpl 关联的是 Window 下的直接 View, 所以他的起始位置就是 0, 0
                host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
                mInLayout = false;
                // ... 省略了部分代码
            }
        }
        
        /**
         * ViewGroup.layout
         */
        @Override
        public final void layout(int l, int t, int r, int b) {
            if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
                if (mTransition != null) {
                    mTransition.layoutChange(this);
                }
                // 回调了 View.layout 方法, 可见 ViewGroup 的 layout 方法并没有什么实际作用 
                super.layout(l, t, r, b);
            } else {
                // record the fact that we noop'd it; request layout when transition finishes
                mLayoutCalledWhileSuppressed = true;
            }
        }
        
        /**
         * View.layout
         */
        public void layout(int l, int t, int r, int b) {
            // ... 
            // 1. 很重要的方法 View.setFrame()
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                // 2. 调用了 onLayout 方法
                onLayout(changed, l, t, r, b);
                // ...
            }
        }
        
        /**
         * View.setFrame
         */
        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;
                int oldWidth = mRight - mLeft;
                int oldHeight = mBottom - mTop;
                int newWidth = right - left;
                int newHeight = bottom - top;
                boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
                // Invalidate our old position
                invalidate(sizeChanged);
                // 1. 更新当前 View 四个顶点的位置, 更新了顶点位置之后, 便可以通过 getWidth/Height 来获取宽高了
                mLeft = left;
                mTop = top;
                mRight = right;
                mBottom = bottom;
            }
        }
        
        /**
         * View.onLayout
         */
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        }
    
        /**
         * ViewGroup.onLayout
         */
        @Override
        protected abstract void onLayout(boolean changed,
                int l, int t, int r, int b);
    

    看到这里, 我们就可以解开心中的疑惑了

    1. performLayout 调用了顶级 View 的 layout 方法
    2. View.layout 方法会先调用 View.setFrame() 方法, 更新当前 View 四个顶点的坐标,
      • 注意这个时候, 就已经可以通过 getWidth/getHeight 获取自身的宽高了, 但是还不能获取子 View 的宽高
    3. Vew.layout 再调用 View.onLayout() 方法,
      • 多肽性————若当前为 ViewGroup 实例, 则会调用 ViewGroup 的 layout 方法
      • 这也就揭示了为什么 ViewGroup 必须重写 onLayout 的原因
    4. 调用了 ViewGroup 中的 onLayout 又会遍历子 View, 调用其 layout 方法, 这样每个 View 就确定了自己的位置, 是一个从上往下分发的过程

    View 的绘制

        /**
         * ViewRootImpl.performDraw
         */
        private void performDraw() {
            // 只关注核心部分
            final boolean fullRedrawNeeded = mFullRedrawNeeded;
            mFullRedrawNeeded = false;
            mIsDrawing = true;
            try {
                draw(fullRedrawNeeded);
            } finally {
                mIsDrawing = false;
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
        
        /**
         * ViewRootImpl.draw 
         */
        private void draw(boolean fullRedrawNeeded) {
            // 调用了drawSoftware
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
            }
        }
        
        /**
         * ViewRootImpl.drawSoftware 
         */
        private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
                boolean scalingRequired, Rect dirty) {
            mView.draw(cavans)
        }
        
        
        /**
         * View.draw
         */
        public void draw(Canvas canvas) {
            final int privateFlags = mPrivateFlags;
            // 这个 dirtyOpaque 非常重要, 它是回调 onDraw 方法的必要条件 
            final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
            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)
             */
    
            int saveCount;
            // 1. 绘制背景, 如果需要的话
            if (!dirtyOpaque) {
                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
                // 2. 若满足条件则调用自身的 View.onDraw 方法
                if (!dirtyOpaque) onDraw(canvas);
    
                // Step 4, draw the children
                // 3. ViewGroup 中实现了该方法, 它会调用 drawChild
                // 从而回调 View.draw, 又回到该方法, 不断的往下分发
                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 (debugDraw()) {
                    debugDrawFocus(canvas);
                }
    
                // we're done...
                return;
            }
    

    好了, 代码里注释很清晰, 不过还是有一个值得关注的点:

    • 如果 dirtyOpaque = true 的话是不会回调 onDraw 方法的
    • 这也解释了为什么我们常常在 ViewGroup 中的 onDraw() 中绘图但无法得以执行的原因
    • 通过 setWillNotDraw(false) 来可以开启绘制
        public void setWillNotDraw(boolean willNotDraw) {
            setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
        }
    

    至此对 View 绘制的三大流程就有一个系统的认识了

    相关文章

      网友评论

        本文标题:View—View绘制的三大流程

        本文链接:https://www.haomeiwen.com/subject/jfbpoxtx.html