多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
ViewRootImpl在其创建过程中通过requestLayout()向主线程发送了一条触发“遍历”操作的消息,“遍历”操作是指performTraversals()方法。它的性质与WMS中的performLayoutAndPlaceSurfacesLocked()类似,是一个包罗万象的方法。ViewRootImpl中接收到的各种变化,如来自WMS的窗口属性变化,来自控件树的尺寸变化、重绘请求等都引发performTraversals()的调用,并在其中完成处理。View类及其子类中的onMeasure()、onLayout()以及onDraw()等回调也都是在performTraversals()的执行过程中直接或间接地引发。也正是如此,一次次的performTraversals()调用驱动着控件树有条不紊地工作着,一旦此方法无法正常执行,整个控件树都将处于僵死状态。因此,performTraversals()函数可谓是ViewRootImpl的心跳。 由于布局的相关工作是此方法中最主要的内容,为了简化分析,并突出此方法的工作流程,本节将以布局的相关工作为主线进行探讨。待完成了这部分内容的分析之后,庞大的performTraversals()方法将不再那么难以驯服,读者便可以轻易地学习其他的工作了。 #### 1.performTraversals()的工作阶段 performTraversals()是Android 源码中最庞大的方法之一,因此在正式探讨它的实现之前最好先将其划分为以下几个工作阶段作为指导。 - 预测量阶段。这是进入performTraversals()方法后的第一个阶段,它会对控件树进行第一次测量。测量结果可以通过mView. getMeasuredWidth()/Height()获得。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次得到回调。 - 布局窗口阶段。根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。 - 最终测量阶段。预测量的结果是控件树所期望的窗口尺寸。然而由于在WMS中影响窗口布局的因素很多(参考第4章),WMS不一定会将窗口准确地布局为控件树所要求的尺寸,而迫于WMS作为系统服务的强势地位,控件树不得不接受WMS的布局结果。因此在这一阶段,performTraversals()将以窗口的实际尺寸对控件进行最终测量。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次被回调。 - 布局控件树阶段。完成最终测量之后便可以对控件树进行布局了。测量确定的是控件的尺寸,而布局则是确定控件的位置。在这个阶段中,View及其子类的onLayout()方法将会被回调。 - 绘制阶段。这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对控件树进行绘制了。在这个阶段中,View及其子类的onDraw()方法将会被回调。 说明 很多文章都倾向于将performTraversals()的工作划分为测量、布局与绘制三个阶段。然而笔者认为如此划分隐藏了WMS在这个过程中的地位,并且没能体现出控件树对窗口尺寸的期望、WMS对窗口尺寸做最终的确定,最后以WMS给出的结果为准再次进行测量的协商过程。而这个协商过程充分体现了ViewRootImpl作为WMS与控件树的中间人的角色。 接下来将结合代码,对上述五个阶段进行深入的分析。 #### 2.预测量与测量原理 本节将探讨performTraversals()将以何种方式对控件树进行预测量,同时,本节也会对控件的测量过程与原理进行介绍。 ##### 预测量参数的候选 预测量也是一次完整的测量过程,它与最终测量的区别仅在于参数不同而已。实际的测量工作在View或其子类的onMeasure()方法中完成,并且其测量结果需要受限于来自其父控件的指示。这个指示由onMeasure()方法的两个参数进行传达:widthSpec与heightSpec。它们是被称为MeasureSpec的复合整型变量,用于指导控件对自身进行测量。它有两个分量,结构如图6-5所示。 :-: ![](http://img.blog.csdn.net/20150814133603687?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) 图 6 - 5 MeasureSpec的结构 其1到30位给出了父控件建议尺寸。建议尺寸对测量结果的影响依不同的SPEC\_MODE的不同而不同。SPEC\_MODE的取值取决于此控件的LayoutParams.width/height的设置,可以是如下三种值之一。 - MeasureSpec.UNSPECIFIED (0):表示控件在进行测量时,可以无视SPEC\_SIZE的值。控件可以是它所期望的任意尺寸。 - MeasureSpec.EXACTLY (1):表示子控件必须为SPEC\_SIZE所制定的尺寸。当控件的LayoutParams.width/height为一确定值,或者是MATCH\_PARENT时,对应的MeasureSpec参数会使用这个SPEC\_MODE。 - MeasureSpec.AT\_MOST (2):表示子控件可以是它所期望的尺寸,但是不得大于SPEC\_SIZE。当控件的LayoutParams.width/height为WRAP\_CONTENT时,对应的MeasureSpec参数会使用这个SPEC\_MODE。 Android提供了一个MeasureSpec类用于组合两个分量成为一个MeasureSpec,或者从MeasureSpec中分离任何一个分量。 那么ViewRootImpl会如何为控件树的根mView准备其MeasureSpec呢? 参考如下代码,注意desiredWindowWidth/Height的取值,它们将是SPEC\_SIZE分量的候选。另外,这段代码分析中也解释了与测量无关,但是比较重要的代码段。 **ViewRootImpl.java-->ViewRootImpl.performTraversals()** ``` private void performTraversals() { // 将mView保存在局部变量host中,以此提高对mView的访问效率 finalView host = mView; ...... // 声明本阶段的主角,这两个变量将是mView的SPEC_SIZE分量的候选 intdesiredWindowWidth; intdesiredWindowHeight; ....... Rectframe = mWinFrame; // 如上一节所述,mWinFrame表示了窗口的最新尺寸 if(mFirst) { /*mFirst表示了这是第一次遍历,此时窗口刚刚被添加到WMS,此时窗口尚未进行relayout,因此 mWinFrame中没有存储有效地窗口尺寸 */ if(lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) { ......// 为状态栏设置desiredWindowWidth/Height,其取值是屏幕尺寸 }else { //① 第一次“遍历”的测量,采用了应用可以使用的最大尺寸作为SPEC_SIZE的候选 DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; } /* 由于这是第一次进行“遍历”,控件树即将第一次被显示在窗口上,因此接下来的代码填充了 mAttachInfo中的一些字段,然后通过mView发起了dispatchAttachedToWindow()的调用 之后每一个位于控件树中的控件都会回调onAttachedToWindow() */ ...... } else { // ② 在非第一次遍历的情况下,会采用窗口的最新尺寸作为SPEC_SIZE的候选 desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); /* 如果窗口的最新尺寸与ViewRootImpl中的现有尺寸不同,说明WMS侧单方面改变了窗口的尺寸 这将产生如下三个结果 */ if(desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { // 需要进行完整的重绘以适应新的窗口尺寸 mFullRedrawNeeded = true; // 需要对控件树进行重新布局 mLayoutRequested = true; /* 控件树有可能拒绝接受新的窗口尺寸,比如在随后的预测量中给出了不同于窗口尺寸的测量结果 产生这种情况时,就需要在窗口布局阶段尝试设置新的窗口尺寸 */ windowSizeMayChange = true; } } ...... /* 执行位于RunQueue中的回调。RunQueue是ViewRootImpl的一个静态成员,即是说它是进程唯一 的,并且可以在进程的任何位置访问RunQueue。在进行多线程任务时,开发者可以通过调用View.post() 或View.postDelayed()方法将一个Runnable对象发送到主线程执行。这两个方法的原理是将 Runnable对象发送到ViewRootImpl的mHandler去。当控件已经加入到控件树时,可以通过 AttachInfo轻易获取这个Handler。而当控件没有位于控件树中时,则没有mAttachInfo可用,此时 执行View.post()/PostDelay()方法,Runnable将会被添加到这个RunQueue队列中。 在这里,ViewRootImpl将会把RunQueue中的Runnable发送到mHandler中,进而得到执行。所以 无论控件是否显示在控件树中,View.post()/postDelay()方法都是可用的,除非当前进程中没有任何 处于活动状态的ViewRootImpl */ getRunQueue().executeActions(attachInfo.mHandler); booleanlayoutRequested = mLayoutRequested && !mStopped; /* 仅当layoutRequested为true时才进行预测量。 layoutRequested为true表示在进行“遍历”之前requestLayout()方法被调用过。 requestLayout()方法用于要求ViewRootImpl进行一次“遍历”并对控件树重新进行测量与布局 */ if(layoutRequested) { final Resources res = mView.getContext().getResources(); if(mFirst) { ......// 确定控件树是否需要进入TouchMode,本章将在6.5.1节介绍 TouchMode }else { /*检查WMS是否单方面改变了ContentInsets与VisibleInsets。注意对二者的处理的差异, ContentInsets描述了控件在布局时必须预留的空间,这样会影响控件树的布局,因此将 insetsChanged标记为true,以此作为是否进行控件布局的条件之一。而VisibleInsets则 描述了被遮挡的空间,ViewRootImpl在进行绘制时,需要调整绘制位置以保证关键控件或区域, 如正在进行输入的TextView等不被遮挡,这样VisibleInsets的变化并不会导致重新布局, 所以这里仅仅是将VisibleInsets保存到mAttachInfo中,以便绘制时使用 */ if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) { insetsChanged = true; } if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) { mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets); } /*当窗口的width或height被指定为WRAP_CONTENT时,表示这是一个悬浮窗口。 此时会对desiredWindowWidth/Height进行调整。在前面的代码中,这两个值被设置 被设置为窗口的当前尺寸。而根据MeasureSpec的要求,测量结果不得大于SPEC_SIZE。 然而,如果这个悬浮窗口需要更大的尺寸以完整显示其内容时,例如为AlertDialog设置了 一个更长的消息内容,如此取值将导致无法得到足够大的测量结果,从而导致内容无法完整显示。 因此,对于此等类型的窗口,ViewRootImpl会调整desiredWindowWidth/Height为此应用 可以使用的最大尺寸 */ if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { // 悬浮窗口的尺寸取决于测量结果。因此有可能需要向WMS申请改变窗口的尺寸。 windowSizeMayChange = true; if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) { // } else { // ③ 设置悬浮窗口SPEC_SIZE的候选为应用可以使用的最大尺寸 DisplayMetrics packageMetrics = res.getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; } } } // **④ 进行预测量。**通过measureHierarchy()方法以desiredWindowWidth/Height进行测量 windowSizeMayChange |=measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight); } // 其他阶段的处理 ...... } ``` 由此可知,预测量时的SPEC\_SIZE按照如下原则进行取值: - 第一次“遍历”时,使用应用可用的最大尺寸作为SPEC\_SIZE的候选。 - 此窗口是一个悬浮窗口,即LayoutParams.width/height其中之一被指定为WRAP\_CONTENT时,使用应用可用的最大尺寸作为SPEC\_SIZE的候选。 - 在其他情况下,使用窗口最新尺寸作为SPEC\_SIZE的候选。 最后,通过measureHierarchy()方法进行测量。 ##### 测量协商 measureHierarchy()用于测量整个控件树。传入的参数desiredWindowWidth与desiredWindowHeight在前述代码中根据不同的情况作了精心的挑选。控件树本可以按照这两个参数完成测量,但是measureHierarchy()有自己的考量,即如何将窗口布局地尽可能地优雅。 这是针对将LayoutParams.width设置为了WRAP\_CONTENT的悬浮窗口而言。如前文所述,在设置为WRAP\_CONTENT时,指定的desiredWindowWidth是应用可用的最大宽度,如此可能会产生如图6-6左图所示的丑陋布局。这种情况较容易发生在AlertDialog中,当AlertDialog需要显示一条比较长的消息时,由于给予的宽度足够大,因此它有可能将这条消息以一行显示,并使得其窗口充满了整个屏幕宽度,在横屏模式下这种布局尤为丑陋。 倘若能够对可用宽度进行适当的限制,迫使AlertDialog将消息换行显示,则产生的布局结果将会优雅得多,如图6-6右图所示。但是,倘若不分清红皂白地对宽度进行限制,当控件树真正需要足够的横向空间时,会导致内容无法显示完全,或者无法达到最佳的显示效果。例如当一个悬浮窗口希望尽可能大地显示一张照片时就会出现这样的情况。 :-: ![](http://img.blog.csdn.net/20150814133648296?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) 图 6 - 6 丑陋的布局与优雅的布局 那么measureHierarchy()如何解决这个问呢?它采取了与控件树进行协商的办法,即先使用measureHierarchy()所期望的宽度限制尝试对控件树进行测量,然后通过测量结果来检查控件树是否能够在此限制下满足其充分显示内容的要求。倘若没能满足,则measureHierarchy()进行让步,放宽对宽度的限制,然后再次进行测量,再做检查。倘若仍不能满足则再度进行让步。 参考代码如下: **ViewRootImpl.java-->ViewRootImpl.measureHierarchy()** ``` private boolean measureHierarchy(final View host,final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { intchildWidthMeasureSpec; // 合成后的用于描述宽度的MeasureSpec intchildHeightMeasureSpec; // 合成后的用于描述高度的MeasureSpec booleanwindowSizeMayChange = false; // 表示测量结果是否可能导致窗口的尺寸发生变化 booleangoodMeasure = false; // goodMeasure表示了测量是否能满足控件树充分显示内容的要求 // 测量协商仅发生在LayoutParams.width被指定为WRAP_CONTENT的情况下 if(lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { /* **① 第一次协商。**measureHierarchy()使用它最期望的宽度限制进行测量。这一宽度限制定义为 一个系统资源。可以在frameworks/base/core/res/res/values/config.xml找到它的定义 */ res.getValue(com.android.internal.R.dimen.config_prefDialogWidth,mTmpValue, true); intbaseSize = 0; // 宽度限制被存放在baseSize中 if(mTmpValue.type == TypedValue.TYPE_DIMENSION) { baseSize = (int)mTmpValue.getDimension(packageMetrics); } if(baseSize != 0 && desiredWindowWidth > baseSize) { // 使用getRootMeasureSpec()函数组合SPEC_MODE与SPEC_SIZE为一个MeasureSpec childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height); //**②第一次测量。**由performMeasure()方法完成 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); /* 控件树的测量结果可以通过mView的getmeasuredWidthAndState()方法获取。如果 控件树对这个测量结果不满意,则会在返回值中添加MEASURED_STATE_TOO_SMALL位 */ if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) ==0) { goodMeasure = true; // 控件树对测量结果满意,测量完成 } else { // **③ 第二次协商。**上次测量结果表明控件树认为measureHierarchy()给予的宽度太小, 在此适当地放宽对宽度的限制,使用最大宽度与期望宽度的中间值作为宽度限制 */ baseSize = (baseSize+desiredWindowWidth)/2; childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); // **④ 第二次测量** performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 再次检查控件树是否满足此次测量 if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = true; // 控件树对测量结果满意,测量完成 } } } } if(!goodMeasure) { /* **⑤ 最终测量。**当控件树对上述两次协商的结果都不满意时,measureHierarchy()放弃所有限制 做最终测量。这一次将不再检查控件树是否满意了,因为即便其不满意,measurehierarchy()也没 有更多的空间供其使用了 */ childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); /* 最后,如果测量结果与ViewRootImpl中当前的窗口尺寸不一致,则表明随后可能有必要进行窗口 尺寸的调整 */ if(mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } } // 返回窗口尺寸是否可能需要发生变化 returnwindowSizeMayChange; } ``` 显然,对于非悬浮窗口,即当LayoutParams.width被设置为MATCH\_PARENT时,不存在协商过程,直接使用给定的desiredWindowWidth/Height进行测量即可。而对于悬浮窗口,measureHierarchy()可以连续进行两次让步。因而在最不利的情况下,在ViewRootImpl的一次“遍历”中,控件树需要进行三次测量,即控件树中的每一个View.onMeasure()会被连续调用三次之多,如图6-7所示。所以相对于onLayout(),onMeasure()方法的对性能的影响比较大。 :-: ![](http://img.blog.csdn.net/20150814133700983?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) 图 6 - 7 协商测量的三次尝试 接下来通过performMeasure()看控件树如何进行测量。 ##### 测量原理 performMeasure()方法的实现非常简单,它直接调用mView.measure()方法,将measureHierarchy()给予的widthSpec与heightSpec交给mView。 看下View.measure()方法的实现: **View.java-->View.measure()** ``` public final void measure(int widthMeasureSpec,int heightMeasureSpec) { /* 仅当给予的MeasureSpec发生变化,或要求强制重新布局时,才会进行测量。 所谓强制重新布局,是指当控件树中的一个子控件的内容发生变化时,需要进行重新的测量和布局的情况 在这种情况下,这个子控件的父控件(以及其父控件的父控件)所提供的MeasureSpec必定与上次测量 时的值相同,因而导致从ViewRootImpl到这个控件的路径上的父控件的measure()方法无法得到执行 进而导致子控件无法重新测量其尺寸或布局。因此,当子控件因内容发生变化时,从子控件沿着控件树回溯 到ViewRootImpl,并依次调用沿途父控件的requestLayout()方法,在这个方法中,会在 mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使得这些父控件的measure()方法得以顺利 执行,进而这个子控件有机会进行重新测量与布局。这便是强制重新布局的意义 */ if ((mPrivateFlags& PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { /* **① 准备工作。**从mPrivateFlags中将PFLAG_MEASURED_DIMENSION_SET标记去除。 PFLAG_MEASURED_DIMENSION_SET标记用于检查控件在onMeasure()方法中是否通过 调用setMeasuredDimension()将测量结果存储下来 */ mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; ...... /* **② 对本控件进行测量** 每个View子类都需要重载这个方法以便正确地对自身进行测量。 View类的onMeasure()方法仅仅根据背景Drawable或style中设置的最小尺寸作为 测量结果*/ onMeasure(widthMeasureSpec, heightMeasureSpec); /* ③ 检查onMeasure()的实现是否调用了setMeasuredDimension() setMeasuredDimension()会将PFLAG_MEASURED_DIMENSION_SET标记重新加入 mPrivateFlags中。之所以做这样的检查,是由于onMeasure()的实现可能由开发者完成, 而在Android看来,开发者是不可信的 */ if((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) !=PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException(......); } // ④ 将PFLAG_LAYOUT_REQUIRED标记加入mPrivateFlags。这一操作会对随后的布局操作放行 mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } // 记录父控件给予的MeasureSpec,用以检查之后的测量操作是否有必要进行 mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; } ``` 从这段代码可以看出,View.measure()方法没有实现任何测量算法,它的作用在于引发onMeasure()的调用,并对onMeasure()行为的正确性进行检查。另外,在控件系统看来,一旦控件执行了测量操作,那么随后必须进行布局操作,因此在完成测量之后,将PFLAG\_LAYOUT\_REQUIRED标记加入mPrivateFlags,以便View.layout()方法可以顺利进行。 onMeasure()的结果通过setMeasuredDimension()方法尽行保存。setMeasuredDimension()方法的实现如下: **View.java-->View.setMeasuredDimension()** ``` protected final void setMeasuredDimension(intmeasuredWidth, int measuredHeight) { /* ① 测量结果被分别保存在成员变量mMeasuredWidth与mMeasuredHeight中 mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; // ② 向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此证明onMeasure()保存了测量结果 mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; } ``` 其实现再简单不过。存储测量结果的两个变量可以通过getMeasuredWidthAndState()与getMeasuredHeightAndState()两个方法获得,就像ViewRootImpl.measureHierarchy()中所做的一样。此方法虽然简单,但需要注意,与MeasureSpec类似,测量结果不仅仅是一个尺寸,而是一个测量状态与尺寸的复合整、变量。其0至30位表示了测量结果的尺寸,而31、32位则表示了控件对测量结果是否满意,即父控件给予的MeasureSpec是否可以使得控件完整地显示其内容。当控件对测量结果满意时,直接将尺寸传递给setMeasuredDimension()即可,注意要保证31、32位为0。倘若对测量结果不满意,则使用View.MEASURED\_STATE\_TOO\_SMALL | measuredSize 作为参数传递给setMeasuredDimension()以告知父控件对MeasureSpec进行可能的调整。 既然明白了onMeasure()的调用如何发起,以及它如何将测量结果告知父控件,那么onMeasure()方法应当如何实现的呢?对于非ViewGroup的控件来说其实现相对简单,只要按照MeasureSpec的原则如实计算其所需的尺寸即可。而对于ViewGroup类型的控件来说情况则复杂得多,因为它不仅拥有自身需要显示的内容(如背景),它的子控件也是其需要测量的内容。因此它不仅需要计算自身显示内容所需的尺寸,还有考虑其一系列子控件的测量结果。为此它必须为每一个子控件准备MeasureSpec,并调用每一个子控件的measure()函数。 由于各种控件所实现的效果形形色色,开发者还可以根据需求自行开发新的控件,因此onMeasure()中的测量算法也会变化万千。不从Android系统实现的角度仍能得到如下的onMeasure()算法的一些实现原则: - 控件在进行测量时,控件需要将它的Padding尺寸计算在内,因为Padding是其尺寸的一部分。 - ViewGroup在进行测量时,需要将子控件的Margin尺寸计算在内。因为子控件的Margin尺寸是父控件尺寸的一部分。 - ViewGroup为子控件准备MeasureSpec时,SPEC\_MODE应取决于子控件的LayoutParams.width/height的取值。取值为MATCH\_PARENT或一个确定的尺寸时应为EXACTLY,WRAP\_CONTENT时应为AT\_MOST。至于SPEC\_SIZE,应理解为ViewGroup对子控件尺寸的限制,即ViewGroup按照其实现意图所允许子控件获得的最大尺寸。并且需要扣除子控件的Margin尺寸。 - 虽然说测量的目的在于确定尺寸,与位置无关。但是子控件的位置是ViewGroup进行测量时必须要首先考虑的。因为子控件的位置即决定了子控件可用的剩余尺寸,也决定了父控件的尺寸(当父控件的LayoutParams.width/height为WRAP\_CONTENT时)。 - 在测量结果中添加MEASURED\_STATE\_TOO\_SMALL需要做到实事求是。当一个方向上的空间不足以显示其内容时应考虑利用另一个方向上的空间,例如对文字进行换行处理,因为添加这个标记有可能导致父控件对其进行重新测量从而降低效率。 - 当子控件的测量结果中包含MEASURED\_STATE\_TOO\_SMALL标记时,只要有可能,父控件就应当调整给予子控件的MeasureSpec,并进行重新测量。倘若没有调整的余地,父控件也应当将MEASURED\_STATE\_TOO\_SMALL加入到自己的测量结果中,让它的父控件尝试进行调整。 - ViewGroup在测量子控件时必须调用子控件的measure()方法,而不能直接调用其onMeasure()方法。直接调用onMeasure()方法的最严重后果是子控件的PFLAG\_LAYOUT\_REQUIRED标识无法加入到mPrivateFlag中,从而导致子控件无法进行布局。 综上所述,测量控件树的实质是测量控件树的根控件。完成控件树的测量之后,ViewRootImpl便得知了控件树对窗口尺寸的需求。 ##### 确定是否需要改变窗口尺寸 接下来回到performTraversals()方法。在ViewRootImpl.measureHierarchy()执行完毕之后,ViewRootImpl了解了控件树所需的空间。于是便可确定是否需要改变窗口窗口尺寸以便满足控件树的空间要求。前述的代码中多处设置windowSizeMayChange变量为true。windowSizeMayChange仅表示有可能需要改变窗口尺寸。而接下来的这段代码则用来确定窗口是否需要改变尺寸。 **ViewRootImpl.java-->ViewRootImp.performTraversals()** ``` private void performTraversals() { ......// 测量控件树的代码 /* 标记mLayoutRequested为false。因此在此之后的代码中,倘若控件树中任何一个控件执行了 requestLayout(),都会重新进行一次“遍历” */ if (layoutRequested) { mLayoutRequested = false; } // 确定窗口是否确实需要进行尺寸的改变 booleanwindowShouldResize = 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)); } ``` 确定窗口尺寸是否确实需要改变的条件看起来比较复杂,这里进行一下总结,先介绍必要条件: - layoutRequested为true,即ViewRootImpl.requestLayout()方法被调用过。View中也有requestLayout()方法。当控件内容发生变化从而需要调整其尺寸时,会调用其自身的requestLayout(),并且此方法会沿着控件树向根部回溯,最终调用到ViewRootImp.requestLayout(),从而引发一次performTraversals()调用。之所以这是一个必要条件,是因为performTraversals()还有可能因为控件需要重绘时被调用。当控件仅需要重绘而不需要重新布局时(例如背景色或前景色发生变化时),会通过invalidate()方法回溯到ViewRootImpl,此时不会通过performTraversals()触发performTraversals()调用,而是通过scheduleTraversals()进行触发。在这种情况下layoutRequested为false,即表示窗口尺寸不需发生变化。 - windowSizeMayChange为true,如前文所讨论的,这意味着WMS单方面改变了窗口尺寸而控件树的测量结果与这一尺寸有差异,或当前窗口为悬浮窗口,其控件树的测量结果将决定窗口的新尺寸。 在满足上述两个条件的情况下,以下两个条件满足其一: - 测量结果与ViewRootImpl中所保存的当前尺寸有差异。 - 悬浮窗口的测量结果与窗口的最新尺寸有差异。 注意ViewRootImpl对是否需要调整窗口尺寸的判断是非常小心的。第4章介绍WMS的布局子系统时曾经介绍过,调整窗口尺寸所必须调用的performLayoutAndPlaceSurfacesLocked()函数会导致WMS对系统中的所有窗口新型重新布局,而且会引发至少一个动画帧渲染,其计算开销相当之大。因此ViewRootImpl仅在必要时才会惊动WMS。 至此,预测量阶段完成了。 ##### 总结 这一阶段的工作内容是为了给后续阶段做参数的准备并且其中最重要的工作是对控件树的预测量,至此ViewRootImpl得知了控件树对窗口尺寸的要求。另外,这一阶段还准备了后续阶段所需的其他参数: - viewVisibilityChanged。即View的可见性是否发生了变化。由于mView是窗口的内容,因此mView的可见性即是窗口的可见性。当这一属性发生变化时,需要通过通过WMS改变窗口的可见性。 - LayoutParams。预测量阶段需要收集应用到LayoutParams的改动,这些改动一方面来自于WindowManager.updateViewLayout(),而另一方面则来自于控件树。以SystemUIVisibility为例,View.setSystemUIVisibility()所修改的设置需要反映到LayoutParams中,而这些设置确却保存在控件自己的成员变量里。在预测量阶段会通过ViewRootImpl.collectViewAttributes()方法遍历控件树中的所有控件以收集这些设置,然后更新LayoutParams。