Android 控件-4 ViewRootImpl-2 performTraversals 之 预测量

Posted by xflyme on June 21, 2015

简介

performTraversals() 是一个保罗万象的方法。ViewRootImpl 中接收的各种变化,如来自 WMS 的窗口属性的变化,来自控件树的尺寸变化以及重绘请求等都会引发 performTraversals() 的调用。View 及其子类的 onMeasure()、onLayout()、onDraw() 都是在 performTraversals() 执行过程中直接或间接的引发。可以说是 performTraversals() 驱动着整个控件树有条不紊的运行,这几个小节我们重点学习下 performTraversals(),本节由 measure 开始。

源码

整个 performTraversals() ,有 800 多行,我们去掉不重要的代码,重点代码加上注释,一步步看一下。

private void performTraversals() {
    // 下面有很多次用到 mView 将它存储到局部变量里,提高访问效率
    final View host = mView;

       ...

    // 这两个变量是 mView SPEC_SIZE 的候选
    int desiredWindowWidth;
    int desiredWindowHeight;

    ...
    
    //mWinFrame 表示窗口的最新尺寸
    Rect frame = mWinFrame;
    if (mFirst) {
    //表示 第一次,此时窗口刚被添加到 WMS ,窗口尚未进行 relayout ,因此 mWinFrame 中没有存储窗口的有效尺寸
        mFullRedrawNeeded = true;
        mLayoutRequested = true;

        final Configuration config = mContext.getResources().getConfiguration();
        if (shouldUseDisplaySize(lp)) {
            // NOTE -- system code, won't try to do compat mode.
            Point size = new Point();
            //屏幕的真实尺寸,不包含任何 DecorView
            mDisplay.getRealSize(size);
            desiredWindowWidth = size.x;
            desiredWindowHeight = size.y;
        } else {
             // 第一次 使用 config 配置的宽高
             desiredWindowWidth = dipToPx(config.screenWidthDp);
             desiredWindowHeight = dipToPx(config.screenHeightDp);
        }

        //由于是第一次「遍历」,填充 mAttachInfo 的一些字段
    } else {
    // 非第一次的情况下,采用窗口的最新尺寸作为 SPEC_SIZE 候选
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        
        //如果窗口尺寸和控件树中的尺寸不同,说明 WMS 单方面改变了窗口尺寸,将会导致一下三个结果
        if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
            if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
            //需要重新进行完整的重绘以适应新的窗口尺寸
            mFullRedrawNeeded = true;
            //需要对控件树进行重新布局
            mLayoutRequested = true;
            //控件树有可能拒绝接受新的窗口尺寸,比如在随后的预测量中给出不同于窗口尺寸的测量结果。产生这种情况就需要在窗口布局阶段尝试设置新的窗口尺寸。
            windowSizeMayChange = true;
        }
    }

   ...

    // Execute enqueued actions on every traversal in case a detached view enqueued an action
    getRunQueue().executeActions(mAttachInfo.mHandler);

    boolean insetsChanged = false;

    boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
    // 仅当 layoutRequested 为 true 时才进行预测量,layoutRequested 为true 表示进行「遍历」之前 requestLayout() 方法被调用过,
    if (layoutRequested) {

        final Resources res = mView.getContext().getResources();

        if (mFirst) {
            // make sure touch mode code executes by setting cached value
            // to opposite of the added touch mode.
            mAttachInfo.mInTouchMode = !mAddedTouchMode;
            ensureTouchModeLocally(mAddedTouchMode);
        } else {
            if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
                insetsChanged = true;
            }
            if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) {
                insetsChanged = true;
            }
            if (!mPendingStableInsets.equals(mAttachInfo.mStableInsets)) {
                insetsChanged = true;
            }
            if (!mPendingDisplayCutout.equals(mAttachInfo.mDisplayCutout)) {
                insetsChanged = true;
            }
            if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {
                mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
                if (DEBUG_LAYOUT) Log.v(mTag, "Visible insets changing to: "
                        + mAttachInfo.mVisibleInsets);
            }
            if (!mPendingOutsets.equals(mAttachInfo.mOutsets)) {
                insetsChanged = true;
            }
            if (mPendingAlwaysConsumeNavBar != mAttachInfo.mAlwaysConsumeNavBar) {
                insetsChanged = true;
            }
            // 当窗口的 width 或 height 被设置为 WRAP_CONTENT 表示这是一个悬浮窗口,此时会对 desiredWindowWidth/desiredWindowHeight 进行调整
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                windowSizeMayChange = true;

                if (shouldUseDisplaySize(lp)) {
                    // NOTE -- system code, won't try to do compat mode.
                    Point size = new Point();
                    mDisplay.getRealSize(size);
                    desiredWindowWidth = size.x;
                    desiredWindowHeight = size.y;
                } else {
                    Configuration config = res.getConfiguration();
                    desiredWindowWidth = dipToPx(config.screenWidthDp);
                    desiredWindowHeight = dipToPx(config.screenHeightDp);
                }
            }
        }

        // 通过 measureHierarchy 进行预测量
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                desiredWindowWidth, desiredWindowHeight);
    }

    //其他阶段的处理
    ...
}

上面不同情形下,为 desiredWindowWidth/desiredWindowHeight 选择了不同的候选尺寸,这些尺寸有什么不同?

协商测量

measureHierarchy() 用于测量整个控件树,下面看一下它的源码:

ViewRootImpl#measureHierarchy()

 private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        //合成用于描述宽度的 MeasureSpec
        int childWidthMeasureSpec;
        //合成用于描述高度的 MeasureSpec
        int childHeightMeasureSpec;
        //测量结果可能导致窗口变化
        boolean windowSizeMayChange = false;
        //测量结果能满足控件树充分显示内容的要求
        boolean goodMeasure = false;
        
        // 只有在 LayoutParams.width 被设置为 WRAP_CONTENT 才会发生协商测量
        // 比如一个弹窗,我们并不希望弹窗占满整个屏幕去显示一行文字
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            //第一次协商, measureHierarchy 使用它期望的宽度进行测量,这种宽度限制是一种系统预先配置的宽度,在/frameworks/base/core/res/res/values/confit.xml 中可以找到它  
                   res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            int baseSize = 0;
            //宽度存储在 baseSize 中
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize
                    + ", desiredWindowWidth=" + desiredWindowWidth);
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
            //baseSize 比 desiredWindowWidth 小,先使用 baseSize 合成 MeasureSpec 看能否满足控件要求
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                //第一次测量,由 performMeasure 完成
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
                        + host.getMeasuredWidth() + "," + host.getMeasuredHeight()
                        + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec)
                        + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
    //测量结果可以由 getMeasuredWidthAndState 获得,如果控件树对这个测量结果不满意,会在返回值中添加 MEASURED_STATE_TOO_SMALL
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                //控件树对测量结果满意
                    goodMeasure = true;
                } else {
                    //控件树对测量结果不满意
                    //适当放宽 baseSize
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize="
                            + baseSize);
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                    //第二次测量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
                            + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                    //再次检查结果能否满足控件树的要求
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        if (DEBUG_DIALOG) Log.v(mTag, "Good!");
                        goodMeasure = true;
                    }
                }
            }
        }
    //最终测量, measureHierarchy 放弃所有限制做最终测量
    //这一次不在检查控件树是否满意,因为即使不满意,也没有更多的空间了
    
        if (!goodMeasure) {
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            //最后如果测量结果和 ViewRootImpl 中的窗口尺寸不一致,表示随后可能会进行窗口尺寸的调整。
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }

        return windowSizeMayChange;
    }

LayoutParams.Width 被设置为 WRAP_CONTENT 时存在协商过程,非 WRAP_CONTENT 不存在协商。 存在协商的情况下 measureHierarchy 最多可进行两次让步,即在最不利的情况下,ViewRootImpl 中的一次遍历,控件树需要进行三次测量。

图1

performMeasure()

接下来通过 performMeasure() 看控件树如何测量,其实 performMeasure() 代码比较简单,它直接调用 mView.measure() 将 measureHierarchy 生成的 MeasureSpec 传进去。

ViewRootImpl#performMeasure


    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            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);
        }

        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        
        // MeasureSpec 发生变化或强制重新布局时才会进行测量
        //强制重新布局是指一个子控件内容发生变化,在这种情况下,子控件的父控件(父控件的父控件)提供的 MeasureSpec 必定与上次相同,导致父控件的 measure()方法无法得到执行,进而导致子控件无法测量其尺寸和布局。
        //怎么解决?子控件内容发生变化时,依次调用父控件的 requestLayout(),这个方法将会在 mPrivateFlags 加入 PFLAG_FORCE_LAYOUT 从而使父控件的 measure() 方法得以执行。
        
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        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);

        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            //准备,从 mPrivateFlags 将 PFLAG_MEASURED_DIMENSION_SET 标志去除
            //该标志用于检查是否已经调用 setMeasuredDimension 存储测量结果
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                //对本控件进行测量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            //将 PFLAG_LAYOUT_REQUIRED 标志加入 mPrivateFlags
            //有了这个标志,后面的布局才得以放行
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
        //记录旧的 MeasureSpec 以便后续检查 measure 是否有必要进行。
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

从以上代码中可以看出 View.measure() 没有实现任何测量算法,它只是一个调度方法,它的作用是:

触发 onMeasure 对 onMeasure 的结果进行检查,并将之通过 setMeasuredDimension 进行保存。

继续,看一下 setMeasuredDimension:

View#setMeasuredDimension

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


 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

很简单,不在一一解释了,不过有一点需要注意。

测量结果不仅仅是一个尺寸,而是一个状态与尺寸的复合型变量,0到30位表示测量的结果尺寸,31位和32位表示控件对测量结果是否满意。

确定是否需要改变窗口尺寸

接下来回到 performTraversals 方法,在 measureHierarchy 执行完毕之后,ViewRootImpl 了解了控件树所需要的空间。便可以确定是否需要改变窗口尺寸以满足控件树的要求。上面的代码中多处设置 windowSizeMayChange 为 true,windowSizeMayChange 仅表示有可能需要调整窗口尺寸,而接下来的这段代码用来确定是否需要改变窗口尺寸。

  private void performTraversals() {
        
        ...

        if (layoutRequested) {
            // Clear this now, so that if anything requests a layout in the
            // rest of this function we will catch it and re-run a full
            // layout pass.
            mLayoutRequested = false;
        }

        boolean windowShouldResize = layoutRequested && windowSizeMayChange
            && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
                || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
                        frame.width() < desiredWindowWidth && frame.width() != mWidth)
                || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
                        frame.height() < desiredWindowHeight && frame.height() != mHeight));
       
       ...
    }

windowShouldResize 的条件看起来比较复杂,我们拆开来看一下:

  • layoutRequested 为 true ,即 requestLayout() 被调用过,当控件内容发生变化需要调整尺寸时,requestLayout() 回溯到 ViewRootImpl 从而引发 performTraversals。这是一个必要条件是因为有可能控件只需要重绘,不需要重新布局。 比如通过 invalidate() 回溯到根部,此时通过 scheduleTraversals() 触发 performTraversals() 而不是通过 requestLayout() 触发,此时只重绘,尺寸不发生变化。
  • windowSizeMayChange 为 true,这表示控件树测量结果和窗口尺寸不一致。
  • 再满足以上两条条件之后,一下条件满足一个即可:
    • 测量结果和 ViewRootImpl 中保存的当前尺寸不一致。
    • 悬浮窗口的测量结果与窗口的最新尺寸有差异。

总结

  • 预测量的触发我们上一节已经分析过了,目的是确保在「 window 被添加到 WindowManager」之前触发一次遍历,以便后续接收各种事件。
  • 第一次测量的时候 mWinFrame 还未被赋值,因此预测量使用的 SPEC_SIZE 分量是从 Configuration 配置文件中取出的。
  • 如果 lp.width 为 WRAP_CONTENT measureHierarchy 的时候会进行协商测量,逐步放宽测量条件看是否能满足控件树要求。
  • setMeasuredDimension 保存的结果不仅仅是一个数值,它是一个复合变量,其 31 和 32 位用来表示测量结果能否满足控件要求,如果不能满足还需要重新测量。