14.WMS工作原理
WMS是Android中图形用户接口的引擎,它管理着所有窗口。所谓的“管理”大致包括创建,删除窗口,以及将某个窗口设置为焦点窗口,焦点窗口是指当前正在和用户交互的窗口。
在WMS中,窗口是由两部分内容构成的,一部分是描述该窗口的类WindowState,另一部分是该窗口在屏幕上对应的界面Surface。
窗口管理服务端要解决的核心问题如下:
- 窗口如何布局
- 窗口尺寸受制于哪些因素
- 如何寻找焦点窗口
窗口切换时的动画策略
WMS的内部逻辑中,会进行三种常见的操作:assign layer,perform layout,place surface。
assign layer的语义是,为窗口分配层值。在WMS中,每个窗口都使用WindowState类来描述,而窗口要在界面上显示时,需要指定窗口的层值。从用户的视角来看,层值越大,其窗口越靠近用户,窗口之间的层叠正是按照层值进行的。
- perform layout的语义是计算窗口的大小,每个窗口对象都必须有一个大小,即窗口大小,perform layout将根据状态栏大小、输入法窗口的状态、窗口动画状态计算该窗口的大小。
- place surface的语义是,调整Surface对象的属性,并重新将其显示到屏幕上。由于assign layer和perform layout的执行结果影响的仅仅是WindowState中的参数,而能够显示到屏幕上的窗口都包含一个Surface对象,因此只有将以上执行结果中的窗口层值、大小设置到Surface对象中,屏幕上才能看出该窗口的变化。place surface的过程就是将这些值赋值给Surface对象,并告诉Surface Flinger服务重新显示这些Surface对象。
WMS接口结构是指WMS功能模块与其他功能模块之间的交互接口,其中主要包括与AMS模块及应用程序客户端的接口:
- 应用程序在Activity中添加、删除窗口。具体实现就是通过调用WindowManager类的addView()和removeView()函数完成,这会转而调用ViewRootImpl类的相关方法,然后通过IPC调用到WMS中的相关方法完成添加、删除过程。
- 当AMS通知ActivityThread销毁某个Activity时,ActivityThread会直接调用WindowManager中的removeView()方法删除窗口。
- AMS中直接调用WMS,这种调用一般都不是请求WMS创建或者删除窗口,而是告诉WMS一些其他信息。比如某个新的Activity就要启动了,从而WMS会保存一个该Activity记录的引用。
在WMS内部,则全权接管了输入消息的处理和屏幕的绘制。其中输入消息的处理是借助于InputManager类完成的,而绘制屏幕则是借助于SurfaceFlinger模块完成的。
每个窗口都会对应一个WindowState对象,因为窗口的本质就是由WindowState类描述的数据对象,WindowState类中记录作为一个窗口应该有的全部属性,比如窗口的大小、在屏幕上的层值,以及窗口动画过程的各种状态信息。WindowToken描述的是窗口对应的token的相关属性,每个窗口都会对应一个WindowToken对象,但是一个窗口的所有子窗口将对应同一个WindowToken对象,即多对一的关系。如果窗口是由Activity创建的,即该窗口对应一个Activity,那么该窗口同时对应一个AppWindowToken对象。
创建窗口的时机可分为两种,第一种是程序员主动调用WindowManager类的addView()方法;另一种是当用户启动一个新的Activity或者显示一个对话框、菜单栏的时候,在这种情况下,程序员并不直接调用addView()函数,但是这些类的内部同样会间接调用addView()函数。
当客户端调用WindowManager类的addView()方法后,该方法会创建一个新的ViewRootImpl对象,然后调用ViewRootImpl类的setView()方法,该方法中会通过IPC方式调用WMS类中内联类Session类的add()方法。Session类的add()方法又会间接调用WMS的addView()方法。addView()流程内部可粗略分为三个小过程,第一个过程是进行前置处理,即首先判断参数的合法性,以确保接下来的添加操作能够顺利进行;第二个过程是具体添加和窗口有关的数据;第三个过程是后置处理,即添加窗口会引起相关状态的变化,因此需要把这些变化反映到相关的数据中。
无论何种情况,WMS要销毁一个Activity相关的窗口时,都必须调用ActivityThread.ApplicationThread的scheduleDestroyActivity()。该函数是异步执行的,它会发送一个Handler消息,然后返回消息的处理函数是handleDestroyActivity,该函数内部调用了两个重要函数,一个是removeViewImediate(),另一个是cloaseAll(),前者用于删除Activity窗口,后者用于删除Activity相关的窗口,比如Activity的启动窗口、菜单窗口、对话框窗口等。 这两个函数又都会调用各自窗口对应的ViewRootImpl对象的die()函数,每个窗口都会对应于一个ViewRootImpl对象,该函数又会调用doDie(),doDie()又会调用dispatchDetachedFromWindow(),该函数内部调用sWindowSession.remove(),这是一个IPC调用,sWindowSession是客户端中的一个全局静态变量,每个客户端对应一个该对象。
Activity attach方法中会调用Window.setWindowManager,方法中会创建WindowManagerImpl。
添加输入法窗口和普通窗口的主要执行过程是一致的,唯一的区别就在于Policy,PhoneWindowManager这个Policy处理输入法时有两个特点:
- 只允许输入法窗口被添加一次。
- 认为输入法窗口下面不应该显示其他窗口的任何内容,所以,当计算窗口时,对于输入法后面的所有窗口的区域被限制在了输入法区域之外,这就导致了输入法显示或隐藏时,其后面窗口的大小会发生变化,仅此而已。
应用窗口能够根据输入法窗口的显示、隐藏而重新设置大小的原因并不是因为它是输入法窗口,而是因为它的出现导致WMS通过IPC回调到了ViewRootImpl.W类的resize()方法,因此,任何能够回调该resized()方法的行为都会导致应用窗口重新设置大小。
WMS的功能可归纳为两个,第一个是保持窗口的层次关系,以便SurfaceFlinger能够据此绘制屏幕;第二个是把窗口信息传递给InputManager对象,以便InputDispatcher能够把输入消息派发给和屏幕上显示一致的窗口。
WMS中的InputManager对象被封装到了一个内部类InputMonitor中,从InputManager的角度来看,InputDispatcher有两个特点:
- 可以暂停整个Dispatcher的消息派发,该情况下,所有的窗口就都得不到输入消息了。该暂停属性保存在InputDispatcher内的全局变量中。
- 也可以暂停对某个窗口消息派发,在该情况下,除被禁止的窗口外,其他窗口的消息派发都正常,该禁止属性保存在每一个窗口的内部。
基于以上两点,InputMonitor中提供了两个函数,分别是updateInputDispatchModeLw()和updateInputWindowLw(),前者用于设置InputDispatcher整体的消息暂停属性,后者用于设置某个窗口的禁止属性。
A窗口切换到B窗口:
当要启动B时,AMS会调用WMS的addAppToken添加一个token,该token对应的是新的Activity。然后再调用WMS的setAppStartingWindow()告诉WMS启动窗口的标题和图标,以便WMS能根据这两个信息创建一个启动窗口。WMS接收到这个命令后就开始去创建启动窗口了,创建的过程就是标准的添加窗口的过程。
与此同时,AMS还去启动B对应的进程,如果进程不存在,则运行一个ActivityThread实例。
在接下来的一段时间里,AMS处于空闲状态;WMS内部则开始创建启动窗口,并可能已经创建完毕了启动窗口,但暂时不能显示该启动窗口;而ActivityThread内部也忙碌地启动进程并使ActivityThread就绪。当ActivityThread就绪后,就会通过IPC调用AMS的attachApplication(),通知AMS自己已经就绪,可以运行任何指定的Activity了。当AMS收到这个通知后,一方面会调用WMS的setAppVisibility()使其开始显示启动窗口,并调用WMS的setFocusedApp()将新的AppWindowToken设为焦点窗口,然而此时由于真正的窗口还没有就绪,所以焦点窗口被调整为Null。另一方面则调用ActivityThread中内联类ApplicationThread的scheduleLaunchActivity(),请求其开始运行指定的Activity,这最终会调用执行到Activity类的onCreate()函数中。
在接下来的一段时间里,在WMS中,由于真正的Activity窗口还没有被创建,因此当前的焦点窗口被调整为Null,并且开始了启动动画。另一方面,在ActivityThread类中则开始运行Activity的onCreate(),该函数最终会调用到setContentView(),这会间接地创建一个真正的Activity窗口。
当ActivityThread内部执行到创建真正的Activity窗口时,会调用到WMS中的addWindow()函数,在该函数中,当添加完新窗口后,就会把焦点调整到新窗口中。此时,根据启动动画是否执行完毕,会有两种情况:如果动画还没有执行完毕,则暂时不显示Activity窗口,不过尽管看不到该窗口,但是该窗口却能接收输入消息,换句话说该窗口只是隐形;另一种情况,如果动画已经执行结束,则新窗口会立即显示到屏幕上并接收输入消息。
如果上面的动画还没有结束,则继续执行启动动画,直到动画结束。当动画结束时,WMS内部会发送一个Handler消息,名称为FINISHED_STARTING,该消息具体是在performShowLocked()函数中发出的,在该消息的处理代码中,会调用removeWindow()删除启动窗口。
当按Back键时,会调用到AMS的finishActivityLocked(),在AMS的finishActivityLocked()中,会命令B客户端终止当前正在运行的Activity,B接收到这个命令后就开始执行Activity的onPause()。另一方面,AMS会通知WMS暂停对B窗口的消息派发,这是通过调用WMS的pauseKeyDispatching()函数完成的。
接下来,对用户来讲,次数当前屏幕依然是B窗口,然而却不能接受消息输入,不过仅仅是B窗口,状态栏窗口则依然可以接收消息输入。在B所在的ActivityThread中,则正忙着暂停B,当然,这个过程一般很短暂,最长不超过5秒。至于WMS和AMS,则当前都处于空闲状态。
直到B完成了暂停的过程,它会通过IPC调用AMS的completePauseLocked(),AMS会通知WMS将B置为不可视,将A置为可视。而在WMS中将A置为可视时,会同时通知A的客户端说“窗口可以显示了”,这是通过IPC调用客户端中ViewRootImpl.W类中的dispatchAppVisibility()完成的。A客户端收到该调用后,会在ViewRootImpl内部执行一次performTraversals(),而该函数内部则又会回调到WMS中的relayoutWindow()。
在WMS内部,一方面通过IPC调用客户端的performTraversals(),另一方面则将焦点赋值给A。当WMS收到A客户端的relayoutWindow()调用时,就开始重新创建A对应的Surface,并在Surface创建完毕后将焦点窗口切换到该窗口,然后开始执行退出动画。从现在开始,用户可能还不能看见A窗口的全部区域,但是当用户输入消息时则能够被A窗口处理。
在动画的过程中,当然也可能是在动画结束后,AMS会调用WMS的removeWindowLocked()函数删除B窗口。那么,AMS中为什么会调用到WMS的removeWindowLocked()呢?这是发生在A内部准备好后,会通过IPC调用AMS的activityIdle(),于是AMS就会销毁那些已经finish的Activity,其中就包括B。这是通过IPC调用B进程ActivityThread中的handleDestroyActivity()完成的,该函数内部又会调用removeWindow()等相关的函数,这实际上就调用到了WMS中的removeWindow()。
当退出动画结束后,会执行WindowState类内部的finishExit(),这就会把被删除的窗口的Surface暂时保存到mDestroySurface列表中,并把被删除窗口本身凡在mPendingRemove列表中,从而使得在WMS中的下一次traversals中删除窗口对应的Surface对象即WindowState对象本身。
最后,当B完成destroy时,会通知AMS说“我已经完成了destroy”,这是通过IPC回调AMS的activityDestoryed()实现的。在AMS的activityDestroyed()函数中,则调用了WMS的removeAppToken,从而删除B在WMS中地赢得AppWindowToken对象。