ViewRootImpl 有如下功能yViewRootImpl 有如下功能:
- 一方面,
ViewRootImpl实现了ViewParent接口,作为整个控件树的根部,它是控件树正常运作的动力所在。ViewRootImpl提供了控件的测量、布局、绘制以及输入事件的配发处理的功能。 - 另一方面,它是
WindowManagerGlobal工作的实际实现者,因此它还需要负责与WMS交互通信以调整窗口的位置大小,以及对来自WMS的事件(如窗口尺寸改变等)做出相应的处理。
ViewRootImpl 的创建及其重要成员
ViewRootImpl 创建于 WindowManagerGlobal 的 addView() 方法中你那个,而调用 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();
......
}
-
- 从
WindowManagerGlobal中获取的一个IWindowSession的实例。它是ViewRootImpl和WMS进行通信的代理。
- 从
-
- 保持参数
mDisplay,在后面setView()调用中将会把窗口添加到这个Diaplay上。
- 保持参数
-
- 保持当前线程到
mThread,这个赋值操作体现了创建ViewRootImpl的线程如何成为UI线程。这个UI线程就是ActivityThread运行的线程。
- 保持当前线程到
-
mDirty用于收集窗口的无效区域。所谓无效区域是指由于数据或状态发生改变时而需要进行重绘的区域。距离说明,当应用程序修改了一个TextView的文字时,TextView会将自己的区域标记为无效区域,并通过view.invalidate()方法将这块区域添加到这里的mDirty中。当下次绘制时,TextView便可以将新的文字绘制在这块区域上。
-
mWinFrame描述了当前窗口的位置和尺寸。与WMS中的WindowState.mFrame保持一致。
-
- 创建一个
W类型的实例,W是IWindow.Stub的子类。即它将在WMS中作为新窗口的ID,并接收来自WMS的回调。
- 创建一个
-
- 创建
mAttachInfo。mAttachInfo是控件系统中很重要的对象。它存储了当前控件树所贴附的窗口的各种有用信息,并且会派发给控件树中的每一个控件。这些控件会将这个对象保存在自己的mAttachInfo变量中。mAttachInfo中所保存的信息有WindowSession、窗口的实例(即mWindow)、ViewRootImpl实例、窗口所属的Display、窗口的Surface以及窗口在屏幕上的位置等。所以,当需要在一个View中查询与当前窗口相关的信息时,非常值得在mAttachInfo中搜索一下。
- 创建
-
- 创建
FallbackEventHandler。这个类同PhoneWindowManager一样,定义在android.policy包中,其实现为PhoneFallbackEventHandler。FallbackEventHandler是一个处理未经任何人消费的输入事件的场所。
- 创建
-
- 创建一个依附于当前线程,即主线程的
Choreographer,用于通过VSYNC特性安排重绘行为。
- 创建一个依附于当前线程,即主线程的
在构造函数之外,还有另外两个重要的成员被直接初始化:
-
mHandler:类型为ViewRootHandler。一个依附于创建ViewRootImpl的线程,即主线程上的,用于将某些必须在主线程进行的操作调度主线程中执行。mHandler与mChoreographer同时存在看似有些重复,其实它们拥有明确不同的分工与意义。由于mChoreographer处理消息时具有VSYNC特性,因此它主要用于处理与重绘相关的操作。但是由于mChoreographer需要等待VSYNC的垂直同步事件来触发对下一条消息的处理,因此它处理消息的即时性稍逊与mHandler。
-
mSurface:类型为Surface。采用无参构造函数创建的一个Surface实例。mSurface此时是一个没有任何内容的空壳子,在WMS通过relayoutWindow()为其分配一块Surface之前尚不能使用。
-
mWinFrame、mPendingContentInset、mPendingVisibleInset:这几个成员存储了窗口布局相关的信息。其中mWinFrame、mPendingContentInset、mPendingVisibleInset与窗口在WMS中的Frame、ContentInsets、VisibleInsets是保持同步的。这是因为这三个成员不仅会作为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() 方法的两个参数传达:widthSpec、heightSpec。它们是被称为 MeasureSpec 的复合型整形变量,用于指导控件对自身进行测量。它有两个分量,结构如下:
其 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/height为WRAP_CONETENT时,对应的MeasureSpec参数会使用这个SPEC_MODE。
测量协商
预测量在 measureHierarchy() 方法中执行。在预测量过程中,若窗口的 width 或 height 被指定为 WRAP_CONTENT,表示这是一个悬浮窗口。这时为了不出现如下丑陋的窗口会进行测量协商。
显然,对于非悬浮窗口,即当 LayoutParams.width 被设置为 MATCH_PARENT 时,不存在协商过程,直接使用给定的 desiredWindowWidth/Height 进行测量即可。而对于悬浮窗口 measureHierarchy 可以最多连续进行两次让步。在最不利的情况下,在 ViewRootImpl 的一次遍历中,控件树需要进行三次测量,即控件树中的每一个 View.onMeasure() 会被连续调用三次之多。所以相对于 onLayout(),onMeasure() 方法对性能的影响比较大。如下:
performTraversals() 方法总结
本节通过将 performTraversals() 方法拆分为五个阶段进行详细介绍。阶段关系如下:
可见,前四个阶段以 layoutRequested 为执行条件,即在 遍历 之前调用了 requestLayout() 方法。这是由于前四个阶段的主要工作目的是确定控件的位置和尺寸。因此,仅当一个或多个控件有改变位置或尺寸的需求时(此时会调用 requestLayout())才有执行的必要。
即使 requestLayout() 未被调用过,绘制阶段也会被执行。因为很多时候需要在不改变控件尺寸和位置的的情况下进行重绘,例如某个控件改变了其文字颜色或背景色。与 requestLayout() 方法相对,这种情况发生在控件调用了 invalidate() 方法的时候。
注意:即使
requestLayout()没有被调用过,有可能由于LayoutParams、mView的可见性等与窗口有关的属性发生变化时,“遍历” 流程仍会进入第二阶段。由于这些属性与布局没有直接联系,因此上图并没有体现这一点。
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_PADDING 和 FLAG_PADDING_NOT_NULL 标签组成,FLAG_CLIP_TO_PADDING 标签在 ViewGroup 初始化时就添加到了 mGroupFlags 中。FLAG_PADDING_NOT_NULL 标签当为控件设置了 padding 属性时也会添加到 mGroupFlags 中,也就是说如果此 ViewGroup 设置了 padding 属性那么在 dispatchDraw() 方法中 clipToPadding 变量的值就是 true。此时 Canvas 会裁减掉 padding 代表的部分,后续子控件只能在裁剪后的区域内绘制。如果 clipToPadding 为 false ,则后续子控件就可以在父控件的完全区域绘制包括 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() 方法可以实现自定义的逻辑。
以软件方式绘制控件树的完整流程如下:
在整个绘制过程中,dispatchDraw() 是使得绘制工作得以在父子控件之间延续的纽带,draw(ViewGroup, Canvas, long) 是准备坐标系的场所,而 draw(Canvas) 则是实际绘制的地方。
上图中描绘的整个绘制流程中,各个控件都使用了同一个 Canvas,并且他们的内容通过这个 Canvas 直接绘制到了 Surface 之上,这一特点如下图:
在随后讨论硬件加速与绘图缓存时将会看到与之结构类似但又有所不同图。将他们对比将有助于深刻理解这几种不同的绘制方式之间的异同以及优缺点。