控件系统原理的剖析

由于平时做项目经常涉及到自定义view,而控件系统又是相当复杂的一个部分,应项目组要求,做一期控件系统原理的讲解报告,于是我从framework层的WindowManager开始剖析控件系统的原理,这里暂不涉及WindowManagerService和SurfaceFlinger;剖析工作由空余时间完成,历时几个月,目前控件系统的整个流程已经理解通畅,由于自身水平原因,理解深入度可能比较浅显;

项目包括:

1.WindowManager的实现原理

2.ViewRootImpl的工作原理

3.控件树的测量,布局与绘制;

4.输入事件在控件树中的派发工作

5.属性动画的工作原理

6.PhoneWindow的工作原理

7.Acitivity以及Dialog的工作原理

8.自定义view的常用思想总结

9.view滑动的常用思想总结

10.布局优化等等

 

1.创建窗口的方法

获得WindowManager实例;

生成WindowManager.LayoutParams,描述窗口的类型和位置;

WindowManager.addView()将窗口添加到系统;

窗口等级过高,需要声明权限;

 

2.WindowManagerGlobal的出现

WindowManager和ViewGroup一样,继承于 ViewManager,提供了添加,删除以及更新窗口的api,可以看做WMS在客户端的代理;WindowManager可以被当成一个窗口容器,它主要管理每个窗口的位置和大小,其实具体位置和大小的计算由WindowManagerService去实现;

WindowManager-->WindowManagerImpl-->WindowManagerGlobal

WindowManagerImpl,它没有实际的逻辑,它的工作都交由WindowManagerGlobal来处理,但是它保存了两个重要的只读成员,即窗口所要显示的屏幕和将会作为哪个窗口的子窗口;

WindowGlobal是一个单例,一个进程只有一个WindowManagerGlobal,所有WindowManagerImpl都是这个进程唯一的WindowManagerGlobal实例的代理,它是WindowManager的最终实现者,维护了当前进程中所有已经添加到系统中的窗口的信息;

 

3.WindowManagerGlobal.addView,updateViewLayout和removeView

如果当前窗口有父窗口的话,需要父窗口根据自己的情况对当前窗口的布局参数进行一些修改;

检查view是否已经存在于WindowManager,如果存在则抛出异常,WindowManager不允许同一个view被添加两次;

创建一个ViewRootImpl对象,并且调用它的setView方法,将作为窗口的控件设置给ViewRootImpl,这个动作将导致ViewRootImpl向WMS添加新的窗口,申请surface以及托管控件在Surface上的重绘动作,这才是真正意义上完成了窗口的添加操作,即把控件交给ViewRootImpl进行托管;

updateViewLayout--->root.setLayoutParams

所以,ViewRootImpl的生命从setView开始,到die结束

removeView--->root.die

4.ViewRootImpl的工作原理

ViewRootImpl实现了ViewParent接口,是一个控件树的根,它负责与WMS通信,负责管理Surface,负责触发控件的测量和布局,负责触发控件的绘制,同时也是输入事件的中转站;

(1)ViewRootImpl构造函数:

它初始化了很多重要的变量,如:

IWindowSession mWindowSession:从WindowManagerGlobal中获取一个IWindowSession实例,它是ViewRootImpl与WMS进行通信的代理;

Display mDisplay:保存参数display,用于将窗口添加到这个display上;

Thread mThread:保存当前线程到mThread,这就是UI线程的由来,以后操作view时,会判断发送请求的Thread是否与这个mThread相同;

Rect   mDirty:收集窗口中的无效区域,即数据或者状态发生改变时而需要进行重绘的区域

Rect mWinFrame:描述了当前窗口的位置和尺寸;

W mWindow:W是IWindow.Stub的子类,它将在WMS中作为新窗口的id,并接收来自来自WMS的回调;

AttachInfo mAttachInfo:mAttachInfo存储了当前控件树所贴附的窗口的各种有用信息,并且会派发给控件树中的每一个控件,这些控件会将这个mAttachInfo保存在自己的mAttachInfo变量中;所以,当需要在view中查询与当前窗口相关的信息时,可以在它的mAttachInfo中会搜索;

Choreographer mChoreographer:创建一个依附于当前线程即主线程的Choreographer,用于通过VSYNC特性安排重绘行为;

mWidth/mHeight:记录在窗口在ViewRootImpl中的尺寸,当窗口在WMS中被重新布局而导致尺寸发生变化时,此时mWinFrame会与mWidth和mHeight发生差异,此时ViewRootImpl即可得知需要对控件树进行重新布局以适应新的窗口变化。在布局完成后,mWidth/mHeight会被赋值为mWinFrame中的所保存的宽和高,二者重新统一;

ViewRootHandler mHandler:将发生在其他线程中的事件安排在主线程执行,即WMS的回调在这里处理;

(2)setView()方法:

在窗口添加之前,先通过requestLayout在主线程上安排一次遍历,所谓遍历,是指ViewRootImpl中的核心方法performTraversals();

requestLayout的遍历,它先是发送了一个阻断信息到MessageQueue,阻止hanlder处理信息,为什么要这样做,因为在遍历的时候,handler可能会处理来自于WMS的回调,而这个回调可能会导致一些属性发生改变,如窗口的尺寸等等,这样会使遍历现场受到破坏,所以它需要在遍历的时候阻断,当遍历完成后在取消阻断;

而且遍历操作用的是mChoreographer;

mWindowSession.addToDisplay添加窗口;

执行顺序是:添加窗口-->遍历-->WMS回调;

初始化InputChannel,InputChannel是窗口接收来自InputDispatcher的输入事件的管道。注意判断条件,否则导致此窗口无法接收任何输入事件;

如果mInputChannel不为空,则创建WindowInputEventReceiver mInputEventReceiver,用于接收输入事件;

ViewRootImpl将作为参数view的parent。所以,viewRootImpl可以从控件树中的任何一个控件开始,通过回溯getParent()的方法得到,view.assignParent(this);

构造函数主要进行成员的初始化,setView()则是创建窗口,建立输入事件接收机制的场所,同时,触发第一次遍历操作的信息已经发送给主线程,在随后的第一次遍历完成之后,ViewRootImpl将会完成对控件树的第一次测量,布局,并从WMS获取窗口的Surface以进行控件树的初次绘制工作;

5.perfromTraversals()

ViewRootImpl中接收的各种变化,都会引发perfromTraversals()方法的调用;

该方法主要有一下几个阶段:

预测量阶段:对控件树进行第一次测量,测量结果可以通过mView.getMeasureWidth/Height()来获得,在此阶段中将会计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸;

布局窗口阶段:根据预测量的结果,通过IWindowSeesion.relayout()方法向WMS请求调整窗口的尺寸以及属性,这将引发WMS对窗口的重新布局,并将返回结果返回给ViewRootImpl;

最终测量阶段:预测量的结果是控件树所期望的窗口尺寸,但由于在WMS中影响窗口布局的因素很多,WMS不一定会将窗口准确的布局为控件树所期望的尺寸,所以控件树不得不接受WMS的布局结果,此时perfromTraversals()将以窗口的实际尺寸对控件树进行测量;

布局控件树阶段:完成最终测量之后便可以布局控件树,测量确定的是控件的尺寸,而布局则是确定控件的位置

绘制阶段:确定控件的位置和尺寸后,便可以对控件树进行绘制;

(1)预测量,也是一个完整的测量过程,它与最终测量的区别仅在于区别不同;实际的测量过程在view以及它的子类的onMeasure()执行,并且其测量结果需要受限于来自于父控件的指示。这个指示就是onMeasure()中的两个参数,widthSpec和heightSpec,他们被称为MeasureSpec的复合整形变量,用于指导控件对自身进行测量;

MeasureSpec的理解:

MeasureSpec有两个分量,1--30是SPEC_SIZE;30--32是SPEC_MODE;

1到30是父控件给出的建议尺寸,建议尺寸对测量结果的影响依SPEC_MODE的不同而不同;

SPEC_MODE的取值取决于此控件的LayoutParams.width/height的设置,有如下取值:

MeasureSpec.UNSPECIFIED(0):控件在进行测量时,可以无视SPEC_SIZE的值;

MeasureSpec.EXACTLY(1):子控件必须为SPEC_SIZE所指定的尺寸;当控件的LayoutParams.width/height为以确定值时,对应的SPEC_MODE便是这个参数;

MeasureSpec.AT_MOST(2):表示子控件可以是它期望的尺寸,但不可大于SPEC_SIZE的大小;

 

measureHierarchy测量前的准备工作:

第一次遍历时:

此时窗口刚刚被添加到WMS,此时窗口尚未进行relayout,mWinFrame没有存储有效的窗口尺寸;

第一次遍历的测量,采用了应用可以使用的最大的尺寸;

由于这是第一次进行遍历,控件树即将第一次被显示在窗口上,因此接下来的代码填充了mAttachInfo字段;

host.dispatchAttachedToWindow:每一个位于控件树中的控件都会回调onAttachToWindow()方法;

非第一次遍历时:

采用窗口的最新尺寸作为SPEC_SIZE的候选;

如果窗口的最新尺寸与ViewRootImpl中的尺寸不同,说明WMS单方面改变了窗口的尺寸;

mFullRedrawNeeded = true--->需要进行完整的重绘以适应新的窗口尺寸;

mLayoutRequested = true;--->需要对控件树进行重新布局

windowSizeMayChange = true;-->控件树有可能拒绝新的窗口尺寸,此时就需要在窗口布局阶段尝试设置新的窗口尺寸;

getRunQueue().executeActions(mAttachInfo.mHandler)--->执行位于RunQueue中回调,RunQueue是进程唯一的,在执行多线程任务时,开发者可通过View.post或者View.postDelayed方法讲一个Runnable对象发送到主线程中去执行;这,两个方法的原理,是将Runnable对象发送到ViewRootIImpl的mHandler;

第一次测量时,使用应用可用的最大尺寸进行测量;

当窗口为悬浮窗口时,使用应用可用的最大尺寸进行测量;

其他情况下,使用窗口的最新尺寸进行测量;

measureHierarchy():

在测量之前需要考虑如何将窗口布局尽可能的布局优雅,这里只针对LayoutParams为WRAP_CONTENT的悬浮窗口而言;

采用了与空间树进行协商的办法:先使用measureHierarchy期望的宽度进行测量,然后通过测量结果来检查控件树是否能够在此限制下满足其充分显示内容的要求;倘若无法满足,measureHierarchy进行让步,放宽对宽度的限制,然后再次进行测量,再做检查,倘若仍不满足则再度进行让步;

第一次协商:宽度限制存放在baseSize中,第一次测量,使用performMeasure,获取控件树的测量结果,如果控件对这个结果不满意,会在返回值中添加MEASURED_STATE_TOO_SMALL,第二次协商,上述测量结果表示控件树认为measureHierarchy给出的宽度太小,在此适当的放宽对宽度的限制,第二次测量,如果不满足,不再进行让步,放弃所有限制,第三次测量,这次不再检查控件树是否满意,因为就算不满意,也没有多余的宽度给它了,如果测量结果与ViewRootImpl中保存的结果不相等,随后可能有必要进行窗口尺寸的调整,即windowSizeMayChange =true ;

 

测量原理:performMeasure()方法的调用;

performMeasure()-->mView.measure:

仅当给予的MeasureSpec发生变化,或要求强制重新布局时,才会进行测量。

所谓强制重新布局,是指当一个子view的内容发生变化时,需要进行重新测量和布局,在这种情况下,这个子控件的的父控件的MeasureSpec是与上次一样的,从而导致父控件的measure()方法无法得到执行,进而导致子控件无法重新测量其尺寸和布局,因此当子控件的内容发生变化时,调用requestlayout方法,该方法会依次调用沿途父控件的requestLayout方法,这个方法会在mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使这些父控件的measure方法得以执行,继而使这个子控件有机会进行重新测量和布局;

去除PFLAG_MEASURED_DIMENSION_SET标记,PFLAG_MEASURED_DIMENSION_SET用于检查控件在onMeasure中是否调用过setMeasureDimension存储测量结果;

对本控件进行测量,每个子view都需要重载这个这个方法以便正确的对自身进行测量;

检查onMeasure是否调用了setMeasureDimension(),setMeasureDimension会将PFLAG_MEASURED_DIMENSION_SET标记重新加入mPrivateFlags中;

添加PFLAG_LAYOUT_REQUIRED标签,这一操作会被之后的布局操作放行;

 

测量结果的获取:

getMeasuredWidthAndState:得到的是 一个测量状态与结果的复合型变量,0到30位表示了测量结果的尺寸,而31到32位则表示了控件树对测量结果是否满意,则使用view.MEASURED_STATE_TOO_SMALL|measuredSize作为参数传递给setMeasureDimension以告知父控件对MeasureSpec进行可能的调整;

onMeasure算法的实现原则:

控件在测量时,  控件需要将它的padding尺寸计算在内,因为padding是其尺寸的一部分;

ViewGroup在进行测量时,需要将子控件的margin尺寸计算在内,因为子控件的margin是父控件尺寸的一部分;

ViewGroup为其子控件准备MeasureSPEC时,SPEC_MODE取决于子控件的LayoutParams.width/height的取值;SPEC_SIZE,理解为ViewGroup对子控件的限制;

ViewGroup在测量子控件时,必须调用子控件的measure()方法,而不能直接调用其onMeasure方法,直接调用onMeasure的最坏结果是,子控件的PFLAG_LAYOUT_REQUIRED无法加入到mPrivateFlags中,导致子控件无法布局;

测量控件树,实则是测量控件树的根控件;

 

调整窗口尺寸大小有两个必要条件:

layoutRequested 为 true,即调用了requestlayout(),performTraversals()还有可能是控件树需要被重绘时被调用,此时窗口尺寸不需要变化;

windowSizeMayChange 为 true,WMS单方面改变了窗口尺寸或者当前窗口为悬浮窗口,其控件树的测量结果将决定窗口的新尺寸;

还有两个可选条件:

测量结果与ViewRootImpl中所保存的当前尺寸有差异;

悬浮窗口的测量结果与当前窗口的最新尺寸有差异;

 

(2)窗口布局:

以requestWindow()为核心;

requestWindow()调用的是mWindowSession.relayout();

输入:窗口的LayoutParams,预测量时的结果以及mView的可见性;

输出:mWinFrame,mPendingContentInsets/VisibleInsets,mSurface;

mPendingConfiguration:WMS给予当前窗口的配置;

对比布局结果,检查是否insets是否发生变化;

如果mContentInsets发生变化,启动过渡动画避免内容发生突兀的抖动,将最新的ContentInsets保存到mContentInsets中;要求mView及其子控件,适应这一ContentInsets,View.fitSystemWindow()将会把ContentInsets作为其padding属性保存下来;如果visisble发生变化,将其保存到mAttachInfo中,mVisibleInsets不影响测试和布局,仅仅影响绘制时的偏移;

 

布局控件树:

控件的实际位置与尺寸由view的mLeft,mTop,mRight,mBottom来表示,这些是相对于父控件的左上角的距离;

倘若需要获取控件在窗口坐标系中的位置,可以使用View. getLocationInWindow() 方法,相应,也可以通过View. getLoca- tionOnScreen() 方法 获取 控 件 在 屏幕 坐标 系 下 的 位置。

布局控件树主要做了两件事情:

进行控件树布局;

设置窗口的透明区域;

控件树布局调用了performLayout()方法

 

 

 

 

 

学会的东西:

requestLayout和invilate的区别

View.post的调用

onAttachToWindow()的调用时机;

getParent();

要在主线程操作view的原理;

可以在onTouch事件中操作View的原理;

onMeasure()中的参数以及MeasureSPEC的解释;

是否每一次都需要重新测量,是否每一次都需要重新布局;

onmeasure中需要强制调用setMeasuredDimension,如何会报异常

 

 

转载于:https://my.oschina.net/u/3491256/blog/910957

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值