美文网首页
Android9.0系统源码WMS(四)使用performTra

Android9.0系统源码WMS(四)使用performTra

作者: AFinalStone | 来源:发表于2022-12-11 17:23 被阅读0次

前言

performTraversals作为View三大流程的入口方法,只要子View执行了requestLayout,就必然会调到ViewRootImpl的performTraversals。如此重要的方法,除了协调测量、布局和绘制这三大流程以外,performTraversals还做了什么呢?
本篇文章我们将聚焦performTraversals方法,梳理其大致的流程,以便我们更深入理解performTraversals的主要职责。

一、从WindowManager的addView方法到ViewRootImpl的performTraversals方法

1、一般情况下,如果我们不想通过Activity或者Dialog,而想要直接将自定义视图mView添加到Window上的时候,我们可以获取WindowManager实例对象,然后调用该对象的addView方法将我们的视图添加到窗口上。

        WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        //调用WindowManager的addView方法将自定义视图mView添加到窗口上
        windowManager.addView(mView, mLayoutParams);

2、WindowManager是一个接口,该接口有一个叫WindowManagerImpl的实现类。

frameworks/base/core/java/android/view/WindowManagerImpl.java

public final class WindowManagerImpl implements WindowManager {
    @UnsupportedAppUsage
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                mContext.getUserId());
    }
}

WindowManagerImpl的addView方法内部直接调用WindowManagerGlobal的addView方法。

3、WindowManagerGlobal的addView方法如下所示:

frameworks//base/core/java/android/view/WindowManagerGlobal.java

public final class WindowManagerGlobal {
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...代码省略...
        ViewRootImpl root;
        ...代码省略...
         root = new ViewRootImpl(view.getContext(), display);
        ...代码省略...
         root.setView(view, wparams, panelParentView);
        ...代码省略...
        }
    }
}

WindowManagerGlobal的addView方法会创建ViewRootImpl实例对象,并调用该对象的setView方法。

4、ViewRootImpl的setView方法如下所示:

frameworks/base/core/java/android/view/ViewRootImpl.java

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
              ...代码省略...
              //对视图进行测量、布局、绘制
              requestLayout();
              ...代码省略...
               //mWindowSession的addToDisplay方法会进一步调用WMS的addWindow方法,将视图添加到窗口上,而且会对mWinFrame进行赋值
               res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
              ...代码省略...
            }
        }
    }
}

ViewRootImpl的setView方法会调用一个关键方法requestLayout,该方法会对视图进行测量、布局、绘制,然后调用IWindowSession的addToDisplay方法,该方法会进一步调用调用WindowManagerService的addView将视图添加到窗口上。

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

requestLayout方法会进一步调用scheduleTraversals方法。

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

    void scheduleTraversals() {
              ...代码省略...
            //调用编舞者的postCallback方法,会回调mTraversalRunnable
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
              ...代码省略...
    }

scheduleTraversals方法会调用一个关键对象mChoreographer的postCallback方法,该方法会在适当的时候触发mTraversalRunnable的run方法,最终会调用doTraversal方法。

    void doTraversal() {
        if (mTraversalScheduled) {
              ...代码省略...
            performTraversals();
        }
    }

doTraversal方法会继续调用performTraversals方法,到这里终于来到了我们想要关注的方法。

二、确认窗口大小

1、performTraversals方法所要做的第一步,便是确认窗口大小的逻辑

    private void performTraversals() {
        final View host = mView;
       ...代码省略...
        WindowManager.LayoutParams lp = mWindowAttributes;
       ...代码省略...
        Rect frame = mWinFrame;
        //如果是第一次执行
        if (mFirst) {
           // 需要重新draw
            mFullRedrawNeeded = true;
            //重新layout
            mLayoutRequested = true;
            final Configuration config = mContext.getResources().getConfiguration();
            //根据窗口类型来判断是否需要使用屏幕宽高,包含状态栏区域
            if (shouldUseDisplaySize(lp)) {
                Point size = new Point();
                //获取屏幕的真实尺寸,存储到size中
                mDisplay.getRealSize(size);
                //将设备宽度作为期望的窗口宽度
                desiredWindowWidth = size.x;
                //将设备高度作为期望的窗口高度
                desiredWindowHeight = size.y;
            } else {
                //使用的屏幕的可用宽高,是去除掉装饰区的(如果含有状态栏、导航栏,那就需要把这部分去除掉)
                desiredWindowWidth = mWinFrame.width();
                desiredWindowHeight = mWinFrame.height();
            }
      } else {//如果不是第一次执行
            desiredWindowWidth = frame.width();
            desiredWindowHeight = frame.height();
            if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
                mFullRedrawNeeded = true;
                mLayoutRequested = true;
                windowSizeMayChange = true;
            }
        }
       ...代码省略...
}
    //判断是否使用屏幕宽高,如果是状态栏控制台弹窗,输入法弹窗,音量调节弹窗则返回true。
    private static boolean shouldUseDisplaySize(final WindowManager.LayoutParams lp) {
        return lp.type == TYPE_STATUS_BAR_PANEL
                || lp.type == TYPE_INPUT_METHOD
                || lp.type == TYPE_VOLUME_OVERLAY;
    }

1)简单概括一下上面代码所做的事情,首先判断是否是第一次执行performTraversals方法:

  • 如果是第一次执行,则将需要重新绘制和需要重新布局的标记变量设置为true,随后根据窗口类型layoutParams的type字段来判断是否需要使用包含状态栏区域的屏幕宽高。如果是,则将包含装饰区域的屏幕宽度和高度作为窗口期望的宽度和高度;如果否,将不包含装饰区域的屏幕宽度和高度作为窗口期望的宽度和高度。
  • 如果不是第一次执行,则使用已经存在的mWinFrame的宽和高,也就是上次执行performTraversals方法所得到的窗口宽高作为窗口期望的宽高。mWidth和mHeight是用来描述窗口当前宽度和高度的,它们的值是由应用程序进程上一次主动请求WindowManagerService计算得到的,并且会一直保持不变到应用程序进程下一次再请求WindowManagerService重新计算为止,如果mWidth和高度mHeight不等于窗口当前的期望宽度和期望高度,那就说明窗口的大小发生了变化,这时候就会将mFullRedrawNeeded 、mLayoutRequested 、windowSizeMayChange 设置为true,以便接下来可以对Activity窗口的大小变化进行处理。

2)为什么需要预测量?
不管是Activity还是Dialog,阅读源码可以发现它们最终也都是调用了WindowManager的addView方法,最终走到WindowManagerGlobal,将DecorView与新建的ViewRootImpl绑定在一起,然后添加到当前的窗口;文章开头我们讲过,除了Activity和Dialog,我们还可以直接调用WindowManager的addView将视图添加到窗口上。这就意味着一个窗口下是可以有多个ViewRootImpl的,这些ViewRootImpl所管理的顶级View也不一定是DecorView。所以ViewRootImpl在设计时,不能只考虑DecorView这种情况,还要考虑顶级View不是DecorView的情况。DecorView和其他View的一个重要的区别是,DecorView需要撑满窗口,而其他View,比如Dialog或者直接调用WindowManager的addView方法添加的View,基本都不需要撑满窗口。为了避免这种用户体验不佳的情况,performTraversals需要根据给定的顶级View优化desiredWindowWidth和desiredWindowHeight。

2、performTraversals方法继续往下执行。

    private void performTraversals() {
        ...代码省略...
        boolean insetsChanged = false;
        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
        if (layoutRequested) {
            final Resources res = mView.getContext().getResources();
            if (mFirst) {
                //确保窗口的触摸模式已经打开
                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 (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {
                    mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
                }
                if (!mPendingOutsets.equals(mAttachInfo.mOutsets)) {
                    insetsChanged = true;
                }
                if (mPendingAlwaysConsumeNavBar != mAttachInfo.mAlwaysConsumeNavBar) {
                    insetsChanged = true;
                }
                if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                        || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    windowSizeMayChange = true;
                    //是否使用包含状态栏、导航栏的屏幕宽高
                    if (shouldUseDisplaySize(lp)) {
                        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);
                    }
                }
            }

            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
        }
        ...代码省略...
  }

简单概括一下上面代码所做的事情,首先判断是否需要对视图重新进行布局。
1)layoutRequested为true,需要重新布局,继续判断是否是第一次执行performTraversals方法,

  • 是第一次执行,则确保窗口的触摸模式已经打开;
  • 不是第一次执行,则比较mPending..Insets和上次存在mAttachInfo中的是否改变,insetsChanged默认false,如果改变则将insetsChanged置为true。然后会判断窗口宽高是否有设置WRAP_CONTENT。
    • 如果有设置,则设置windowSizeMayChange为true,并判断是否使用包含了状态栏的屏幕宽高
      • 如果使用,则设置窗口期望宽高为包含了状态栏的屏幕宽高
      • 如果没有使用,则设置窗口期望宽高为不包含状态栏的屏幕宽高

layoutRequested为true的条件分支,最终会调用到measureHierarchy方法。看看measureHierarchy方法:
在需要对视图进行重新布局分支的最后,会调用measureHierarchy()去测量窗口宽高。

3、measureHierarchy方法如下所示。

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;
        boolean goodMeasure = false;
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            // 在大屏幕上,我们不希望对话框直接填充整个屏幕,例如一行文字,刚开始设置一个较小的布局会更合适
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                //第一次预测量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    goodMeasure = true;
                } else {
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
 //第二次预测量                          
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        if (DEBUG_DIALOG) Log.v(mTag, "Good!");
                        goodMeasure = true;
                    }
                }
            }
        }

        if (!goodMeasure) {
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            //第三次预测量
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }
        return windowSizeMayChange;
    }

可以看到measureHierarchy内部三次调用performMeasure方法进行测量操作。

1)第一次测量

如果View的宽度是WRAP_CONTENT的,就会进行第一次预测量。

在第一次预测量,会做最乐观的估计,先以baseSize(R.dimen.config_prefDialogWidth)作为宽度,让View去测量。

如果baseSize足够View去展示,就使用baseSize作为宽度,预测量结束;否则就进行第二次预测量。

2)第二次预测量

如果给定的baseSize不足以让View展示,就会进行第二次预测量。如何知道给定的baseSize不足以让View测量?

如果View对给定的baseSize不满意,就会反馈在state中,可以调用getMeasuredWidthAndState方法来获取。如果测量结果与View.MEASURED_STATE_TOO_SMALL按位与的结果不为0,说明View对给定的大小不满意,即给定的尺寸不够展示

第二次进一步扩大了baseSize的大小,以原有baseSize和desiredWindowWidth的均值为baseSize,再一次进行尝试测量。

经过测量后,如果给定的尺寸足以让View展示,就以给定的尺寸为宽度,并将goodMeasure置为true,预测量结束;否则就进入第三次预测量。

3)第三次预测量

前两次预测量发现给定的baseSize都不足以让View展示,或者是View的宽度不是wrap_content,因此使用接近屏幕宽度的desiredWindowWidth进行测量,将desiredWindowWidth设置为宽度结果,然后结束流程。

三、测量、布局、绘制

1、performTraversals方法继续往下执行,开始对视图进行测量

private void performTraversals() {
        if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
          ...代码省略...
            if (mWidth != frame.width() || mHeight != frame.height()) {
                mWidth = frame.width();
                mHeight = frame.height();
            }
          ...代码省略...
            if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;

                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    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;
                }
            }
        } else {
            maybeHandleWindowMove(frame);
        }
          ...代码省略...
}

能够进行测量,需要满足六个条件其中之一:

| 条件 | 含义 | 备注 |
| ---- | ---- |
|mFirst| 首次进行performTraversals| -|
| windowShouldResize | 窗口尺寸是否需要改变 | 跟预测量有关|
| viewVisibilityChanged| View的可见性是否改变| -|
| cutoutChanged | 刘海屏的缺口是否改变 | -|
| params != null| 窗口的lp是否改变 | -|
| mForceNextWindowRelayout| WMS是否强制改变了窗口尺寸| -|

紧接着如果不是stop状态,就正式开始测量步骤。

1)测量 - 不带权重

如果布局不带有权重(weight),那只需一次测量即可。

2)测量 - 带有权重

经过一次测量后,如果发现LayoutParams中带有宽度权重 or 高度权重,则需要根据测量的宽度 or 高度乘以权重值,然后再执行一次测量。

2、开始对视图进行*布局
经过测量后,就可以进行布局了。

private void performTraversals() {
    ...代码省略...
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
        performLayout(lp, mWidth, mHeight);
    ...代码省略...
    } 
    ...代码省略...
}

3、开始对视图进行*绘制

经过布局后,就可以进行绘制了。

private void performTraversals() {
    ...代码省略...
    mFirst = false;
    
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    
    if (!cancelDraw) {
    ...代码省略...
        performDraw()
    } else {
        if (isViewVisible) {
            //再次尝试
            scheduleTraversals();
        }
    ...代码省略...
    }
}

四 总结

总体而言,performTraversals的逻辑大致可以分为五块:

1)确认窗口大小
2)预测量
3)测量
4)布局
5)绘制

首先,为了确认窗口大小,performTraversals会根据LayoutParams的宽高及其各种标志位,配合WMS给定的窗口大小,最终计算出可供View展示的大小;

然后呢,为了优化顶级View不是DecorView下的展示体验,performTraversals会进行预测量,经过预测量,会给出一个合理的尺寸,让View进行测量;

经过预测量后,接下来就是View的三大流程:测量、布局以及绘制。

此后performTraversals结束。

相关文章

网友评论

      本文标题:Android9.0系统源码WMS(四)使用performTra

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