3.3 Android窗口
3.3.1 概述
Android系统中,窗口管理系统是基于C/S模式的,客户端(App)请求创建窗口和使用窗口,服务端(WMS)管理所有窗口,包括创建、删除窗口,以及将某个窗口设置为焦点窗口(当前正在和用户交互的窗口)。
Android Gui系统
我们先简单来介绍一下Android的GUI系统,它包含以下部分内容:
- 应用框架系统 — AMS
- 窗口和图形系统 — WMS
- 显示渲染系统 — SurfaceFlinger
- 用户输入系统 — InputManager
它们之间的关系,如下图所示:
简单来讲,AMS负责Activity的启动,以及生命周期的管理。一个Activity对应一个应用窗口,WMS负责管理这个窗口(创建、删除等)以及事件分发。View系统管理每个窗口中的具体布局,最终这个View系统中最顶端的根View(即DecorView)会被作为窗口,添加到WMS中。WMS管理着所有这些添加的窗口,负责管理这些窗口的层次,显示位置等内容。每个窗口都有一块自己的Surface,SurfaceFlinger负责把这些Surface显示到屏幕上。
窗口的概念
看AndroidSDK文档的描述:Window是一个抽象基类,用于控制顶层窗口的外观和行为,如绘制背景、标题栏和按键处理等。View是一个基本的UI单元,占据屏幕的一块矩形区域,用于绘制显示UI,接收处理事件等。
而窗口的概念,从不同的角度来看,其含义是不一样的,有时候是指Window,有时候是指View。我们知道,WMS管理所有的窗口,这里的窗口其实是一个View(DecorView),而不是Window。WMS负责管理这些View的Z-order,显示区域,以及把消息派发到对应的View。
3.3.2 窗口的类型
Framework中定义了三种类型的窗口:应用窗口,子窗口,系统窗口。
应用窗口
Activity对应的窗口就是应用窗口,其默认的窗口类型是TYPE_BASE_APPLICATION。而Dialog的窗口类型是TYPE_APPLICATION,而很多Dialog的子类,修改了窗口类型,如ContextMenu,本质是用Dialog来实现的,但是在添加窗口前,修改了type类型,赋值为TYPE_APPLICATION_ATTACHED_DIALOG。从这个我们可以看到,WMS并没有把应用窗口与子窗口区分得那么清楚。
与应用窗口相关的窗口表示类是PhoneWindow,PhoneWindow继承自Window,其本身只是一个窗口封装类,其核心是成员mDecorView,mDecorView是一个顶层的View,窗口的添加就是通过调用WindowManager.addView()把该View添加到WMS。
子窗口
子窗口是指该窗口必须要有一个父窗口,父窗口可以是一个应用类型窗口,也可以是其他类型的窗口。例如前面手Q界面中,点击右上角的按钮显示一个PopupWindow,它就是一个子窗口,其类型一般TYPE_APPLICATION_PANEL。既然称为子窗口,其与父窗口的关系是比较容易理解的:B是A的子窗口,A不可见时,B也会不可见的。
系统窗口
一般来讲,系统窗口应该由系统来创建的,例如发生异常,ANR时的提示框,状态栏,屏保等。但是,Framework还是定义了一些可以被应用所创建的系统窗口,如TYPE_TOAST,TYPE_INPUT_METHOD和TYPE_WALLPAPER等等。系统窗口的添加也是直接调用WindowManager.addView()将目标View添加到WMS。
3.3.3 窗口的创建、显示与移除
这里以应用窗口为例简单介绍以下窗口的创建,显示和移除过程。
窗口的创建
我们知道每个Activity对应一个PhoneWindow,当我们调用setContentView时,其实最终结果是把我们的View树作为子View添加到PhoneWindow的DecorView中,而这个DecorView会在ActivityThread的handleResumeActivity方法中,通过WindowManager.addView()方法添加到WMS中去的。
AMS在接收到启动Activity请求时,首先生成一个token作为该Activity的唯一标识,然后在WMS中添加一个AppWindowToken,其封装了Activity的token。接着AMS启动应用客户端进程并把token传递到该进程,在客户端进程里完成Activity的初始化。在Activity的attach()方法中,Activity完成PhoneWindow的创建,并且把token传递给PhoneWindow。在Activity调用WindowManager.addView()时,在WindowManager内部会把token和该View关联,真正向WMS申请创建窗口的时候,再把token传递给WMS。WMS接收到创建窗口的请求的时候,通过mTokenMap查询对应该token的AppWindowToken,如果为空则抛出异常,否则创建一个WindowState并完成初始化工作和其他数据结构的调整工作。在这个过程中,token贯穿了服务端的AMS、WMS和客户端的Activity、Window。
应用请求创建窗口时,和应用直接交互的是WindowManager对象,其负责管理一个应用的所有本地窗口。当应用调用addView()创建窗口时,WindowManager会生成一个ViewRoot对象与之相对应,并且把相应的参数LayoutParams保存起来。addView()的执行流程如下:
- 检查所添加的窗口是否已经添加过,不允许重复添加;
- 如果所添加窗口为子窗口类型,找到其父窗口,并保存在内部变量中;
- 创建一个新的ViewRoot,并保存对应的View(DecorView)和LayoutParams;
- 调用ViewRoot的setView()方法,完成添加工作。
ViewRoot本质上是一个Handler,并且实现了ViewParent接口。ViewRoot的主要功能是:
- 负责分发消息事件,如Key、Motion事件等;
- 负责和WMS的交互,分发WMS的交互命令;
- 作为DecorView的parent,对DecorView进行measure、layout和draw等操作;
在addView()的第3、4步完成之后,之后和WMS的交互工作就由ViewRoot负责。而ViewRoot和WMS之间的双向对话,主要是通过以下两个数据结构进行的:IWindowSession,IWindow。其中IWindowSession负责ViewRoot到WMS的请求,IWindow则用于WMS回调ViewRoot。在ViewRoot对象内部,存在着一个IWindowSession的静态成员和一个IWindow的非静态成员,所以一个进程里只有一个IWindowSession对象,但是可以有多个IWindow对象。参见下图:
到此为止,整个窗口管理系统整体架构可表示如下:
窗口显示
从Client端调用WindowManager的addView()方法到WMS完成WindowState的初始化,在这个过程中,主要是完成了窗口数据结构的创建以及Client端的窗口和Server端的窗口建立起连接关系:WMS能够对Client端的窗口进行操作,同时WMS也能够接收Client端窗口的请求,对WindowState进行相应的调整。
一个Window想要显示在屏幕上,必须申请一个显示缓存(Surface),这个显示缓存的管理和维护是在底层图形模块实现的。WindowState申请到Surface对象之后,会将此Surface对象的相关数据拷贝到Client端的ViewRoot中,ViewRoot中也维护了一个Surface对象,这两个对象是指向同一块显示缓存。ViewRoot有了这块显示缓存的引用之后,即可以通过lockCanvas来获取绘画画布,绘制完毕之后通过unlockAndPostCanvas来将绘制内容刷新到显示缓存中。也就是说,Client端窗口和Server端窗口共用一个Surface,Client负责绘制Surface的内容,Server负责控制Surface在屏幕上的大小位置等。
ViewRoot通过IWindowSession的relayout方法来向WMS发送请求命令,包括窗口的显示和隐藏,窗口的布局信息如位置大小,同时还会接收WMS的处理结果。WMS会根据屏幕大小和Client请求的布局参数来决定窗口最终的布局信息,同时也会根据Client请求的显示隐藏命令来返回一个有效的或者无效的Surface对象。通常一个窗口的显示过程为:
- Client请求显示窗口,并且传递布局参数;
- WMS根据布局参数,申请一个Surface对象并返回给Client;
- Client对Surface进行绘画操作,完成后告诉WMS;
- WMS将Surface显示在屏幕上,并且进行层级等相应调整;
于是一个窗口从添加到显示可用以下时序图表示:
一个横跨Activity、View、Window、WMS以及Surface的整体概念如下如所示:
窗口的移除
窗口的移除是通过调用WindowManager.removeView()来完成的,具体流程如下:
ViewRootImpl在收到要删除窗口的命令后,会执行以下操作:
- 判断是否可以立即删除窗口,否则会等下次UI操作时执行;
- 确认需要删除窗口时,会执行doDie方法,通过dispatchDetachedFromWindow通知View树,窗口要被删除了;
- dispatchDetachedFromWindow执行以下操作:通过dispatchDetachedFromWindow,通知View树,窗口已经移除了;把窗口对应的HardRender、Surface给释放了;通过mWindowSession,通知WMS,窗口要移除了,WMS会把跟这个窗口相关的WindowState,以及WindowToken给移除,同时更新其它窗口的显示;通知Choreographer,这个窗口不需要显示了,跟这个窗口相关的一些UI刷新操作,可以取消了。
- 当根View收到dispatchDetachedFromWindow调用后,会遍历View树中的每一个View,把这个通知传递下来。
3.3.4 总结
这里我们总结一下,Android中窗口的相关内容:
- 在Window System中,分为两部分的内容,一部分是运行在SystemServer进程中的WMS及相关类,另一部分是运行在应用进程的WindowManager,ViewRoot等相关类。WMS用WindowState来描述一个窗口,而应用进程用DecorView、ViewRoot以及WindowManager.LayoutParams等来描述一个窗口的相关内容。
- 对于WMS来讲,窗口对应一个View对象,而不是Window对象。通过WindowManager.addView()方法添加一个窗口,通过removeView方法移除一个窗口,通过updateViewLayout()方法更新一个窗口的属性。
- Android把窗口分为三种类型:应用窗口,子窗口以及系统窗口。不同类型的窗口,在执行添加窗口操作时,对于WindowManager.LayoutParams中的参数token具有不同的要求。应用窗口,LayoutParams中的token,必须是某个有效的Activity的mToken。而子窗口,LayoutParams中的token,必须是父窗口的ViewRootImpl中的W对象。
- 在调用WindowManager.addView()之前,如果没有给token赋值,则会走默认的token赋值逻辑:如果mParentWindow不为空,则会调用其adjustLayoutParamsForSubWindow方法,在该方法中,如果当前要添加的窗口是应用窗口,则会把当前PhoneWindow的mToken赋值给token。如果是子窗口,则会把当前PhoneWindow对应的DecorView的mAttachInfo中的mWindowToken赋值给token。而View中的mAttachIno来自ViewRootImpl的mAttachInfo。因此这个token本质就是父窗口的ViewRootImpl中的W类对象。