深入理解 Android - ViewRootImpl(转载)

505 阅读14分钟

ViewRootImpl 有如下功能yViewRootImpl 有如下功能:

  • 一方面, ViewRootImpl 实现了 ViewParent 接口,作为整个控件树的根部,它是控件树正常运作的动力所在。ViewRootImpl 提供了控件的测量、布局、绘制以及输入事件的配发处理的功能。
  • 另一方面,它是 WindowManagerGlobal 工作的实际实现者,因此它还需要负责与 WMS 交互通信以调整窗口的位置大小,以及对来自 WMS 的事件(如窗口尺寸改变等)做出相应的处理。

ViewRootImpl 的创建及其重要成员

ViewRootImpl 创建于 WindowManagerGlobaladdView() 方法中你那个,而调用 addView() 方法的线程就是此 ViewRootImpl 所掌控的控件树的 UI 线程。ViewRootImpl 的构造函数主要初始化了一些重要成员,如下:

public ViewRootImpl(Context context, Display display) {
    mContext = context;
    // 1
    mWindowSession = WindowManagerGlobal.getWindowSession();
    // 2
    mDisplay = display;
    mBasePackageName = context.getBasePackageName();
    // 3 
    mThread = Thread.currentThread();
    ......
    // 4 
    mDirty = new Rect();
    mTempRect = new Rect();
    mVisRect = new Rect();

    // 5
    mWinFrame = new Rect();

    // 6
    mWindow = new W(this);
    
    ......

    // 7
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);
    
    // 8
    mFallbackEventHandler = new PhoneFallbackEventHandler(context);

    // 9
    mChoreographer = Choreographer.getInstance();
    
    ......
}
    1. WindowManagerGlobal 中获取的一个 IWindowSession 的实例。它是 ViewRootImplWMS 进行通信的代理。
    1. 保持参数 mDisplay,在后面 setView() 调用中将会把窗口添加到这个 Diaplay 上。
    1. 保持当前线程到 mThread,这个赋值操作体现了创建 ViewRootImpl 的线程如何成为 UI 线程。这个 UI 线程就是 ActivityThread 运行的线程。
    1. mDirty 用于收集窗口的无效区域。所谓无效区域是指由于数据或状态发生改变时而需要进行重绘的区域。距离说明,当应用程序修改了一个 TextView 的文字时,TextView 会将自己的区域标记为无效区域,并通过 view.invalidate() 方法将这块区域添加到这里的 mDirty 中。当下次绘制时,TextView 便可以将新的文字绘制在这块区域上。
    1. mWinFrame 描述了当前窗口的位置和尺寸。与 WMS 中的 WindowState.mFrame 保持一致。
    1. 创建一个 W 类型的实例,WIWindow.Stub 的子类。即它将在 WMS 中作为新窗口的 ID,并接收来自 WMS 的回调。
    1. 创建 mAttachInfomAttachInfo 是控件系统中很重要的对象。它存储了当前控件树所贴附的窗口的各种有用信息,并且会派发给控件树中的每一个控件。这些控件会将这个对象保存在自己的 mAttachInfo 变量中。mAttachInfo 中所保存的信息有 WindowSession、窗口的实例(即 mWindow)、ViewRootImpl 实例、窗口所属的 Display、窗口的 Surface 以及窗口在屏幕上的位置等。所以,当需要在一个 View 中查询与当前窗口相关的信息时,非常值得在 mAttachInfo 中搜索一下。
    1. 创建 FallbackEventHandler。这个类同 PhoneWindowManager 一样,定义在 android.policy 包中,其实现为 PhoneFallbackEventHandlerFallbackEventHandler 是一个处理未经任何人消费的输入事件的场所。
    1. 创建一个依附于当前线程,即主线程的 Choreographer,用于通过 VSYNC 特性安排重绘行为。

在构造函数之外,还有另外两个重要的成员被直接初始化:

    1. mHandler:类型为 ViewRootHandler。一个依附于创建 ViewRootImpl 的线程,即主线程上的,用于将某些必须在主线程进行的操作调度主线程中执行。mHandlermChoreographer 同时存在看似有些重复,其实它们拥有明确不同的分工与意义。由于 mChoreographer 处理消息时具有 VSYNC 特性,因此它主要用于处理与重绘相关的操作。但是由于 mChoreographer 需要等待 VSYNC 的垂直同步事件来触发对下一条消息的处理,因此它处理消息的即时性稍逊与 mHandler
    1. mSurface:类型为 Surface。采用无参构造函数创建的一个 Surface 实例。mSurface 此时是一个没有任何内容的空壳子,在 WMS 通过 relayoutWindow() 为其分配一块 Surface 之前尚不能使用。
    1. mWinFramemPendingContentInsetmPendingVisibleInset:这几个成员存储了窗口布局相关的信息。其中 mWinFramemPendingContentInsetmPendingVisibleInset 与窗口在 WMS 中的 FrameContentInsetsVisibleInsets 是保持同步的。这是因为这三个成员不仅会作为 relayoutWindow() 的传出参数,而且 ViewRootImpl 在收到来自 WMS 的回调 IWindow.Stub.resize() 时,立即更新这三个成员的取值。因此这三个成员体现了窗口在 WMS 中的最新状态。

performTraversals()

  • mAppVisibilityChanged:用于单独跟踪应用的可见性更新,以防我们得到双倍的变化。这将确保我们总是为相应的窗口调用relayout。

performTraversals() 方法可以分为如下几个阶段:

  • 预测量阶段:这是进入 performTraversals() 方法后的第一个阶段,它会对控件树进行第一次测量。测量结果可以通过 mView.getMeasureWidth()/Height() 获得。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸。在这个阶段中,View 及其子类的 onMeasure() 方法将会沿着控件树依次得到回调。
  • 布局窗口阶段:根据预测量的结果,通过 IWindowSession.relayout() 方法向 WMS 请求调整窗口的尺寸等属性,这将引发 WMS 对窗口进行重新布局,并将布局结果返回给 ViewRootImpl
  • 最终测量阶段:预测量结果是控件树所期望的窗口尺寸。然而由于在 WMS 中影响窗口布局的因素很多,WMS 不一定会将窗口准确地布局为控件树所要求的尺寸,而迫于 WMS 作为系统服务的强势地位,控件树不得不接受 WMS 的布局结果。因此在这个阶段,performTraversals() 将以窗口的实际尺寸对控件进行最终测量。在这个阶段中,View 及其子类的 onMeasure() 方法将会沿着控件树依次被回调。
  • 布局控件树阶段:完成最终测量之后便可以对控件树进行布局。测量确定的是控件的尺寸,而布局则是确定控件的位置。在这个阶段中,View 及其子类的 onLayout() 方法将会被回调。
  • 绘制阶段:这是 performTraversals() 的最终阶段。确定控件的位置和尺寸后,便可以对控件树进行绘制。在这个阶段中,View 及其子类的 onDraw() 方法将被调用。

performTraversals() 过程中除了控件的 测量布局绘制 三个阶段外,还有控件树对窗口尺寸的期望、WMS 对窗口尺寸做出最终的确定、最后控件树以 WMS 给出的结果为准再次进行测量的协商过程。这个协商过程充分提现了 ViewRootImpl 作为 WMS 与控件树的中间人的角色。

预测量和测量原理
测量参数

预测量和最终测量的区别仅在于参数不同而已。实际的测量工作在 View 或其子类的 onMeasure() 方法中完成,并且其测量结果需要受限于其父控件的指示。这个指示由 onMeasure() 方法的两个参数传达:widthSpecheightSpec。它们是被称为 MeasureSpec 的复合型整形变量,用于指导控件对自身进行测量。它有两个分量,结构如下:

Image

1 ~ 30 位给出了父控件建议尺寸。建议尺寸对测量结果的影响依 SPEC_MODE 的不同而不同。SPECE_MODE 可取值如下:

  • MeasureSpec.UNSPECIFIED:表示控件在进行测量时,可以无视 SPEC_SIZE 的值。控件可以是它所期望的任意尺寸。
  • MeasureSpec.EXACTLY:表示子控件必须为 SPEC_SIZE 所指定的尺寸。当控件的 LayoutParams.width/height 为一确定值,或者是 MATCH_PARENT 时,对应的 MeasureSpec 参数会使用这个 SPEC_MODE
  • MeasureSpec.AT_MOST:表示子控件可以是它所期望的尺寸,但是不得大于 SPEC_SIZE。当控件的 LayoutParams.width/heightWRAP_CONETENT 时,对应的 MeasureSpec 参数会使用这个 SPEC_MODE
测量协商

预测量在 measureHierarchy() 方法中执行。在预测量过程中,若窗口的 widthheight 被指定为 WRAP_CONTENT,表示这是一个悬浮窗口。这时为了不出现如下丑陋的窗口会进行测量协商。

Image

显然,对于非悬浮窗口,即当 LayoutParams.width 被设置为 MATCH_PARENT 时,不存在协商过程,直接使用给定的 desiredWindowWidth/Height 进行测量即可。而对于悬浮窗口 measureHierarchy 可以最多连续进行两次让步。在最不利的情况下,在 ViewRootImpl 的一次遍历中,控件树需要进行三次测量,即控件树中的每一个 View.onMeasure() 会被连续调用三次之多。所以相对于 onLayout()onMeasure() 方法对性能的影响比较大。如下:

Image

performTraversals() 方法总结

本节通过将 performTraversals() 方法拆分为五个阶段进行详细介绍。阶段关系如下:

Image

可见,前四个阶段以 layoutRequested 为执行条件,即在 遍历 之前调用了 requestLayout() 方法。这是由于前四个阶段的主要工作目的是确定控件的位置和尺寸。因此,仅当一个或多个控件有改变位置或尺寸的需求时(此时会调用 requestLayout())才有执行的必要。

即使 requestLayout() 未被调用过,绘制阶段也会被执行。因为很多时候需要在不改变控件尺寸和位置的的情况下进行重绘,例如某个控件改变了其文字颜色或背景色。与 requestLayout() 方法相对,这种情况发生在控件调用了 invalidate() 方法的时候。

注意:即使 requestLayout() 没有被调用过,有可能由于 LayoutParamsmView 的可见性等与窗口有关的属性发生变化时,“遍历” 流程仍会进入第二阶段。由于这些属性与布局没有直接联系,因此上图并没有体现这一点。

ViewRootImpl 总结

本节主要介绍了 ViewRootImpl 的创建,以及核心 performTraversals() 方法的实现原理。读者从中可以对 ViewRootImpl 架构与工作方式有深入理解。作为整个控件树管理者,ViewRootImpl 十分复杂与庞大,不过其很多工作都会落在本节所介绍的 5 个阶段中完成,所以深入理解这 5 个阶段的实现可以为学习 ViewRootImpl 的其他工作打下坚实的基础。在本章后面的内容中将介绍控件树的绘制与输入事件派发两个方面的内容,那时会重新回到 ViewRootImpl,探讨那些位于 5 个阶段之外的工作。

绘制

performDraw() 中绘制分为:

  • 软件绘制
  • 硬件绘制
软件绘制

软件绘制drawSoftware() 方法中实现,drawSoftware() 方法主要有四步工作:

  • 第一步,通过 Surface.lockCanvas() 获取一个用于绘制的 Canvas
  • 第二步,对 Canvas 进行变换以实现滚动效果。
  • 第三步,通过 mView.draw() 将根控件绘制在 Canvas 上。
  • 第四步,通过 Surface.unlockCanvasAndPost() 显示绘制后的内容。

其中第二步与第三步是控件绘制过程中的两个基本阶段,即首先通过 Canvas 的变换指令将 Canvas 的坐标系变换到控件自身的坐标系下,然后再通过控件的 View.draw(Canvas) 方法将控件的内容绘制在这个变换后的坐标系中。

注意:在 View 中还有 draw(Canvas) 方法的另一个重载,即 View.draw(ViewGroup, Canvas, long)。这个方法被 ViewGroup.drawChild() 调用,用以绘制 Child View 自身。而且这个重载方法会根据控件的位置、旋转、缩放以及动画对 Canvas 进行坐标系的变换,使得 Canvas 的坐标系从哪个父控件的坐标系变换到本控件的坐标系,并且会在变换完成后调用 draw(Canvas) 来在变换后的坐标系中进行绘制。View.draw(Canvas) 方法更纯粹就是将控件的内容绘制到给定的 Canvas,而 View.draw(ViewGrop, Canvas, long) 除了变换坐标系还包含了硬件加速、绘图缓存以及动画计算等工作。

View.draw(Canvas) 的绘制过程主要有以下四步:

  • 绘制背景,注意背景不会受到滚动的影响。
  • 通过调用 onDraw() 方法绘制控件自身的内容。
  • 通过调用 dispatchDraw() 绘制其子控件。
  • 绘制控件的装饰,即滚动条。

一般情况下开发人员只需重写 onDraw() 方法,以保证背景、子控件和装饰器得以正确绘制。

onDraw() 方法仅绘制了控件自身的内容。而子控件的绘制是通过 dispatchDraw() 方法实现的。其在 View 类中是一个空方法,而 ViewGroup 实现了重写了它。dispatchDraw() 方法在之后的版本中更新了实现方式,但具体的逻辑并没有改变,最新实现如下:

@Override
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;

    ...//动画相关

    /**
     * 1. 设置裁剪区域. 
     */
    int clipSaveCount = 0;
    final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
    if (clipToPadding) {
        clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
        canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                mScrollX + mRight - mLeft - mPaddingRight,
                mScrollY + mBottom - mTop - mPaddingBottom);
    }

    // We will draw our child's animation, let's reset the flag
    mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
    mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

    boolean more = false;
    final long drawingTime = getDrawingTime();

    if (usingRenderNodeProperties) canvas.insertReorderBarrier();
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    int transientIndex = transientCount != 0 ? 0 : -1;
    // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
    // draw reordering internally
    final ArrayList<View> preorderedList = usingRenderNodeProperties
            ? null : buildOrderedChildList();
    /**
     * 2. 确定子控件遍历是依序遍历还是按开发者指定方式遍历
     */
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    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) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }

        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            /**
             * 3. 调用 drawChild() 绘制子控件
             */ 
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    while (transientIndex >= 0) {
        // there may be additional transient views after the normal views
        final View transientChild = mTransientViews.get(transientIndex);
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                transientChild.getAnimation() != null) {
            more |= drawChild(canvas, transientChild, drawingTime);
        }
        transientIndex++;
        if (transientIndex >= transientCount) {
            break;
        }
    }
    if (preorderedList != null) preorderedList.clear();

    // Draw any disappearing views that have animations
    if (mDisappearingChildren != null) {
        final ArrayList<View> disappearingChildren = mDisappearingChildren;
        final int disappearingCount = disappearingChildren.size() - 1;
        // Go backwards -- we may delete as animations finish
        for (int i = disappearingCount; i >= 0; i--) {
            final View child = disappearingChildren.get(i);
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    if (usingRenderNodeProperties) canvas.insertInorderBarrier();

    if (debugDraw()) {
        onDebugDraw(canvas);
    }

    /**
     * 4. 撤销裁剪操作
     */ 
    if (clipToPadding) {
        canvas.restoreToCount(clipSaveCount);
    }

    // mGroupFlags might have been updated by drawChild()
    flags = mGroupFlags;

    if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
        invalidate(true);
    }

    ...//动画相关
}

这里重点说一下 CLIP_TO_PADDING_MASK 标签,在文中作者的解释如下:

有时候子控件可能部分或者完全位于 ViewGroup 之外。在默认情况下,ViewGroup 下的代码通过 Canvas.clipRect() 方法将子控件的绘制限制在自身区域之内。超出此区域的绘制内容会被剪裁。是否需要进行越界内容的剪裁取决于 ViewGroup.mGroupFlags 中是否包含 CLIP_TO_PADDING_MASK 标记,因此开发者可以通过 ViewGroup.setClipToPadding() 方法修改这一行为,使得子控件超出的内容仍然得以显示。

但这个解释更适应于 FLAG_CLIP_CHILDREN 标签的解释。而 CLIP_TO_PADDING_MASK 标签如下:

protected static final int CLIP_TO_PADDING_MASK = FLAG_CLIP_TO_PADDING | FLAG_PADDING_NOT_NULL;

它由 FLAG_CLIP_TO_PADDINGFLAG_PADDING_NOT_NULL 标签组成,FLAG_CLIP_TO_PADDING 标签在 ViewGroup 初始化时就添加到了 mGroupFlags 中。FLAG_PADDING_NOT_NULL 标签当为控件设置了 padding 属性时也会添加到 mGroupFlags 中,也就是说如果此 ViewGroup 设置了 padding 属性那么在 dispatchDraw() 方法中 clipToPadding 变量的值就是 true。此时 Canvas 会裁减掉 padding 代表的部分,后续子控件只能在裁剪后的区域内绘制。如果 clipToPaddingfalse ,则后续子控件就可以在父控件的完全区域绘制包括 padding 代表的区域。

这个设置对于可滑动组件效果明显,比如 RecyclerView 设置了 paddingTop 属性,那么 paddingTop 部分永远滑不到。

如果设置了:

android:clipToPadding="false"recyclerView.clipToPadding = false

就可以了。

dispatchDraw() 方法中最重要的就是它定义的重绘顺序。在最新版本中重绘顺序在 getAndVerifyPreorderedIndex() 方法中实现:

/**
 * @param childrenCount 子控件数量
 * @param i 要重绘的第 i 个子控件
 * @param customOrder 是否使用呢自定义绘制顺序
 */
private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
    final int childIndex;
    if (customOrder) {
        final int childIndex1 = getChildDrawingOrder(childrenCount, i);
        if (childIndex1 >= childrenCount) {
            throw new IndexOutOfBoundsException("getChildDrawingOrder() "
                    + "returned invalid index " + childIndex1
                    + " (child count is " + childrenCount + ")");
        }
        childIndex = childIndex1;
    } else {
        childIndex = i;
    }
    return childIndex;
}

参数 customOrder 的取值计算如下:

final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();

这里可以通过修改 isChildrenDrawingOrderEnabled() 方法的返回值设置是否使用自定义绘制顺序。重写 getChildDrawingOrder() 方法可以实现自定义的逻辑。

以软件方式绘制控件树的完整流程如下:

Image

在整个绘制过程中,dispatchDraw() 是使得绘制工作得以在父子控件之间延续的纽带,draw(ViewGroup, Canvas, long) 是准备坐标系的场所,而 draw(Canvas) 则是实际绘制的地方。

上图中描绘的整个绘制流程中,各个控件都使用了同一个 Canvas,并且他们的内容通过这个 Canvas 直接绘制到了 Surface 之上,这一特点如下图:

Image

在随后讨论硬件加速与绘图缓存时将会看到与之结构类似但又有所不同图。将他们对比将有助于深刻理解这几种不同的绘制方式之间的异同以及优缺点。