Android 系统中的窗口是屏幕上的一块用于绘制各种UI元素并可以响应用户输入的一个矩形区域。窗口的概念是独自占有一个Surface实例的显示区域。例如,Dialog、Activity的界面、壁纸、状态栏以及Toast等都是窗口。
Activity 通过Surface来显示自己的过程:
即然每个窗口都有一块Surface供自己涂鸦,必然需要一个角色对所有窗口的Surface进行协调管理。
于是,WMS 应运而生。
WMS为所有窗口分配Surface,掌管Surface的显示顺序(Z-order)以及位置尺寸,控制窗口动画,并且还是输入系统的一个重要中转站。
一个窗口:
使用WMS 的接口创建并渲染一个动画窗口,直接了当地揭示WMS的客户端如何申请、渲染并注销自己的窗口。
三个文件:
□SampleWindow.java主程序源代码。
□Android.mk编译脚本。
□sw.sh启动器
[SampleWindow.java–>SampleWindow]
pacakage understanding.wms.samplewindow;
......
public calss SampleWindow{
pbulic static void main(Stirng[] args){
try{
// SampleWindow.Run()是这个程序的主入口
new SampleWindow().Run();
}catch(Exception e){
e.printStackTrace();
}
}
// IWindowSession 是客户端向WMS 请求窗口操作的中间代理,并且是进程唯一的 。
IWindowSession mSession = null;
// InputChanner 是窗口接收用户输入事件的管道。
InputChanner mInputChannel = new InputChannel();
//下面三个Rect保存了窗口的布局结果。其中mFrame 表示了窗口在屏幕上的位置与尺寸
Rect mInsets = newRect();
Rect mFrame = new Rect();
Rect mVisisbleInsets = new Rect();
Configuration mConfig = new Configuration();
// 窗口的Surface,在此Surface 上进行的绘制都将在此窗口上显示出来
Surface mSurface = new Surface();
// 用于在窗口上进行绘图的画刷
Paint mPaint = new Paint();
// 添加窗口所需的令牌
IBinder mToken = new Binder()
// 一个窗口对象
MyWindow mWindow = new MyWindow();
// WindowManager.LayoutParams 定义窗口的布局属性,包括位置、尺寸以及窗口类型等
LayoutParams mLp = new LayoutParams();
Choreographer mChoreographer = null;
// InputHandler 用于从InputChannel 接收按键事件并做出响应
InputHandler mInputHandler = null;
boolean mContinueAnime = true;
public void Run() throws Exception{
Looper.prepare();
//获取WMS 服务
IWindowManager wms = IWindowManager.Stub.asInterface(
ServiceManager.getService(Context.WINDOW_SERVICE));
// 通过WindowManagerGlobal 获取进程唯一的IWindowSession 实例。将用于向WMS
// 发送请求
mSession = WindowManagerGlobal.getWindowSession(Looper.myLooper());
// 获取屏幕分辨率
IDisplayManager dm = IDisplayManager.Stub.asInterface(
ServiceManager.getService(Context.DISPALY_SERVICE));
DisplayInfo di = dm.getDisplayInfo(Display.DEFAULT_DISPLAY);
Point scrnSize = new Point(di.appWidth, di.appHeight);
//初始化WindowManager.LayoutParams
initLayoutParams(scrnSize);
// 将新窗口添加到WMS
installWindow(wms);
// 初始化Choreographer的实例,此实例为线程唯一。这个类的用法与Handler类似
// 不过它总是在VSYC 同步时回调,所以比Handler更适合做动画的循环器
mChoreographer = Choreographer.getInstance();
// 开始处理第一帧的动画
scheduleNextFrame();
// 当前线程陷入消息循环,直到Looper.quit()
Looper.loop();
// 标记不要继续绘制动画帧
mContinueAnime = false;
// 卸载当前window
uninstallWindow(wms);
}
public void initLayoutParams(Point screenSize){
//标记即将安装的窗口类型为SYSTEM_ALERT ,这将使得窗口的Z-Order顺序比较靠前
mLp.type = LayoutParams.TYPE_SYSTEM_ALERT;
mLp.setTitle("SampleWindow");
// 设定窗口的左上角坐标以及高度和宽度
mLp.gravity = Gravity.LEFT | Gravity.TOP;
mLp.x = screenSize.x / 4;
mLp.y = screenSize.y / 4;
mLp.width = screenSize.x / 2;
mLp.height = screenSize.y / 2;
// 和输入事件相关的Flag,希望当输入事件发生在此窗口之外时,其他窗口也可以接收输入事件
mLp.flags = mLp.flags | WindowManager.LayoutParams.FLAG_NOT_TOUVH_MODAL;
}
public void installWindow(IWindowManager wms) throws Exception{
// 首先向WMS 声明一个Token ,任何一个Window都需要隶属于一个特定类型的Token
wms.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
// 设置窗口所隶属的Token
mLp.token = mToken;
// 通过IWindowSessin 将窗口安装进WMS,注意,此时仅仅是安装到WMS,本例的Window目前仍然没有有效的Surface
// 不过,经过这个调用后,mInputChannel 已经可以用来接收输入事件了
mSession.add(mWindow, 0 , mLp, View.VISIBLE, mInsets, mInputChannel);
// 通过IWindowSession 要求WMS 对本窗口进行重新布局,经过这个操作后,WMS将会为窗口创建一块用于绘制的Surface并保存在参数mSurface中。同时,这个Surface被WMS放置在LayoutParams所指定的位置上。
mSeesion.relayout(mWindow,0,mLp,mLp.width,mLp.height, View.VISIBLE,0,mFrame,mInsets,mVissibleInsets,mConfig,mSurface);
if(!mSurface.isValid()){
throw new RuntimeException("Failed creating Surface.");
}
// 基于WMS返回的InputChannel 创建一个Handler,用于监听输入事件
// mInputHandler 一旦被创建,就已经在监听输入事件了
mInputHandler = new InputHandler(mInputChannel,Looper.myLooper());
}
public void uninstallWindow(IWindowManager wms) throws Exception{
// 从WMS 处卸载窗口
mSession.remove(mWindow);
// 从WMS 处移除之前添加的Token
wms.removeWindowToken(mToken);
}
public void scheduleNextFrame(){
// 要求显示系统刷新下一帧时回调mFrameRender, 注意,只回调一次
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION,mFrameRender,null);
}
// 这个Runnable 对象用于在窗口上描绘一帧
public Runnable mFrameRender = new Runnable(){
@Override
public void run(){
try{
// 获取当期时间戳
long time = mChoreographer.getFrameTime() % 1000;
// 绘图
if(mSurface.isValid()){
Canvas canvas = mSurafce.lockCanvas(null);
canvas.drawColor(Color.DKGRAY);
canvas.drawRect(2*mLp.width * time / 1000 - mLp.width, 0 ,2*mLp.width * time / 1000, mLp.height, mPaint);
mSurface.unlockCanvasAndPost(canvas);
mSession.finishDrawing(mWindow);
}
if(mContinueAnime)
scheduleNextFrame();
} catch (Exception e){
e.printStackTrace();
}
}
};
// 定义一个类继承InputEventReceiver,用于在其onInputEvent() 函数中接收窗口的输入事件
class InputHandler extends InputEventReceiver{
Looper mLooper = null;
public InputHandler(InputChannel inputChannel,Looper looper){
super(inputChannel,looper);
mLooper = looper;
}
}
@Override
public void onInputEvent(InputEvent event){
if (event instanceof MotionEvent){
MotionEvent me = (MotionEvent)event;
if(me.getAction() == MotionEvent.ACTION_UP){
// 退出程序
mLooper.quit();
}
}
super.onInputEvent(event);
}
}
// 实现一个继承自IWindow.Stub 的类MyWindow
class MyWindow extends IWindow.Stub{
// 保持默认的实现即可
}
}
[Android.mk]
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE := samplewindow
include $(BUILD_JAVA_LIBRARY)
将这两个文件放在$TOP/frameworks/base/cmds/samplewindow/下,然后用make或mm命令进行编译。最终生成的结果是samplewindow.jar,文件位置在out/target//system/framework/下。将该文件通过adb push到手机的/system/framework/下。
然而,samplewindow.jar不是一个可执行程序,故需借助Android的app_process工具来加载并执行。笔者编写了一个脚本作为启动器:
[sw.sh]
base =/system
export CLASSPATH = $base/framework/samplewindow.jar
exec app_process $base/bin understanding.wms.samplewindow.SampleWindow "&@"
app_process其实就是大名鼎鼎的zygote。不过,只有使用–zygote参数启动时它才会改名为zygote[插图],否则就像java-jar命令一样,运行指定类的main静态函数。
总结在客户端创建一个窗口的步骤:
窗口的绘制过程:
通过Surface.lock() 函数获取可以在其上作画的Canvas实例。
使用Canvas 实例进行作画。
通过Surface.unlockCanvasAndPost()函数提交绘制结果。
在SampleWindow例子中,有一个名为mWindow(类型为IWindow)的变量。读者可能会理所当然地认为它就是窗口了。其实这种认识并不完全正确。IWindow继承自Binder,并且其Bn端位于应用程序一侧(在例子中IWindow的实现类MyWindow就继承自IWindow.Stub),于是其在WMS一侧只能作为一个回调,以及起到窗口Id的作用。
窗口的本质: 是进行绘制所使用的画布: Surface
当一块Surface显示在屏幕上时,就是用户所看到的窗口了。客户端向WMS添加一个窗口的过程,其实就是WMS为其分配一块Surface的过程,一块块Surface在WMS的管理之下有序地排布在屏幕上。Andorid 才得以呈现出多姿多彩的界面。 (SurfaceManagerService)
根据对Surface的操作类型可以将Android 的显示系统分为三个层次:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kAUmaZcb-1630897249842)(C:\Users\10303437\AppData\Roaming\Typora\typora-user-images\image-20210902143906855.png)]
第一个层次是UI框架层,其工作为在Surface上绘制UI元素以及响应输入事件。
第二个层次为WMS,其主要工作是管理Surface的分配、层级顺序等。
第三个层次为SurfaceFlinger ,负责将多个Surface混合并输出。
和其他的系统服务一样,WMS 启动位于SystemService.java中ServerThread类的run()函数内。
[SystemServer.java–>ServerThread.run()]
public void run()
{
......
WindowManagerService wm = null;
......
try{
......
// 1. 创建WMS实例
/* 通过WindowManagerService的静态函数main()创建WindowManagerService的实例。
注意main()函数的两个参数wmHandler和uiHandler.这两个Handler分别运行于由ServerThread所创建的两个名为“WindowManager”和“UI”的两个HandlerThread中*/
wm = WindowManagerService.main(context,power,display,inputManager,uiHandler,wmHandler,
factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL)
}
}
WMS 的 创建分为三个阶段:
接下来看看WMS 的main()函数的实现:
[WindowManagerService.java–>WindowManagerSrevice.main()]
public static WindowManagerService main(final Context context,
final PowerManagerService pm,final DisplayManagerService dm,
final InputManagerService im,
final Handler uiHandler, final Handler wmHandler,
final boolean haveInputMethods, final boolean showBootMsgs,
final WindowManagerService[] holder = new WindowManagerServcie[1];
// 通过由SystemServer 为WMS 创建的Handler新建一个WindowManagerService对象
// 此Handler运行在一个名为WindowManager的HandlerThread中
// 这个函数将会在Handler所在的线程中执行传入的Runable 对象,同时阻塞调用线程的执行,
// 直到Runnable 对象的run()函数执行完毕
wmHandler.runWithScissors(new Runnable()){
@Override
public void run(){
holder[0] = new WindowManagerService(context, pm, dm, im,
uiHandler, haveInputMethods,
showBootMsgs, onlyCore);
}
} ,0);
return holder[0];
}
接下来看看其构造函数,看一下WMS定义哪些组件。
[WindowManagerService.java -->WindowManagerService.WindowManagerService()]
private WindowManagerService(Context context, PowerManagerService pm, DisplayManagerService displayManager, InputManagerService inputManager,
Handler uiHandler,
boolean haveInputMethods, boolwan showBootMsgs, boolean onlyCore)
......
mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
mDisplayManager.registerDisplayListener(this,null);
Display[] dispalys = mDisplayManager.getDisplays();
/*初始化DispalyContent列表。DisplayContent 是Android 为支持多屏幕输入所引入的一个概念。
*一个DisplayContent 指代一块屏幕,屏幕可以是手机自身的屏幕,也可以是基于WI-FIDisplay 技术的虚拟屏幕
*/
for(Display display : displays){
createDisplayContentLocked(display);
}
.....
/* 保存InputManagerService。输入事件最终要分发给具有焦点的窗口,而WMS是窗口管理者,所以WMS是输入系统中的重要一环*/
mInputManager = inputManager;
// 这个看起来其貌不扬的mAnimator ,事实上具有非常重要的作用,它管理着所有窗口的动画
mAnimator = new WindowAnimator(this, context , mPolicy);
// 在“UI” 线程中将对另一个重要成员mPolicy,也就是WindowManagerPolucy 进行初始化
initPolicy(uiHandler);
// 将自己加入到Watchdog中
Watchdog.getInstance().addMonitor(this);
......
}
第 2 步,displayReady() 函数的调用主要是初始化显示尺寸的信息。在其完成后,WMS会要求ActivityManagerService进行第一次Configuration更新。
第 3 步, 在systemReady() 函数中,WMS 本身将不会再进行任何操作,直接调用mPolicy的systemReady()函数。
mInputManager , InputManagerService(输入系统服务)的实例。用于管理每个窗口的输入事件通道(InputChannel)以及向通道上派发事件。
mChoreographer,Choreographer的实例,在SampleWindow的例子中已经见过了。Choreographer的意思是编舞指导。它拥有从显示子系统获取VSYNC同步事件的能力,从而可以在合适的时机通知渲染动作,避免在渲染的过程中因为发生屏幕重绘而导致的画面撕裂。从这个意义上讲,Choreographer的确是指导Android翩翩起舞的大师。WMS使用Choreographer负责驱动所有的窗口动画、屏幕旋转动画、墙纸动画的渲染。
mAnimator,WindowAnimator的实例。它是所有窗口动画的总管(窗口动画是一个WindowStateAnimator对象)。在Choreographer的驱动下,逐个渲染所有的动画。
mPolicy,WindowPolicyManager的一个实现。目前它只有PhoneWindowManager一个实现类。mPolicy定义了很多窗口相关的策略,可以说是WMS的首席顾问!每当WMS要做什么事情的时候,都需要向这个顾问请教应当如何做。例如,告诉WMS某一个类型的Window的ZOrder的值是多少,帮助WMS矫正不合理的窗口属性,会为WMS监听屏幕旋转的状态,还会预处理一些系统按键事件(例如HOME、BACK键等的默认行为就是在这里实现的),等等。所以,mPolicy可谓是WMS中最重要的一个成员了。
mDisplayContents,一个DisplayContent类型的列表。Android 4.2支持基于Wi-Fi Display的多屏幕输出,而一个DisplayContent描述了一块可以绘制窗口的屏幕。每个DisplayContent都用一个整型变量作为其ID,其中手机默认屏幕的ID由Display.DEFAULT_DISPLAY常量指定。DisplayContent的管理是由DisplayManagerService完成的,在本章不会去探讨DisplayContent的实现细节,而是关注DisplayContent对窗口管理与布局的影响。
下面的几个成员的初始化并没有出现在构造函数中,不过它们的重要性一点也不亚于上面几个。
mTokenMap,一个HashMap,保存了所有的显示令牌(类型为WindowToken),用于窗口管理。在SampleWindow例子中曾经提到过,一个窗口必须隶属于某一个显示令牌。在那个例子中所添加的令牌就被放进了这个HashMap中。从这个成员中还衍生出几个辅助的显示令牌的子集,例如mAppTokens保存了所有属于Activity的显示令牌(WindowToken的子类AppWindowToken),mExitingTokens则保存了正在退出过程中的显示令牌等。其中mAppTokens列表是有序的,它与AMS中的mHistory列表的顺序保持一致,反映了系统中Activity的顺序。
mWindowMap,也是一个HashMap,保存了所有窗口的状态信息(类型为WindowState),用于窗口管理。在SampleWindow例子中,使用IWindowSession.add()所添加的窗口的状态将会被保存在mWindowMap中。与mTokenMap一样,mWindowMap一样有衍生出的子集。例如mPendingRemove保存了那些退出动画播放完成并即将被移除的窗口,mLosingFocus则保存了那些失去了输入焦点的窗口。在DisplayContent中,也有一个windows列表,这个列表存储了显示在此Display-Content中的窗口,并且它是有序的。窗口在这个列表中的位置决定了其最终显示时的Z序。
mSessions,一个List,元素类型为Session。Session其实是SampleWindow例子中的IWindowSession的Bn端。也就是说,mSessions这个列表保存了当前所有想向WMS寻求窗口管理服务的客户端。注意Session是进程唯一的。
mRotation,只是一个int型变量。它保存了当前手机的旋转状态。
WMS定义的成员一定不止这些,但是它们是WMS每一种功能最核心的变量。读者在这里可以先对它们有一个感性认识。在本章后续的内容里将会详细分析它们在WMS的各种工作中所发挥的核心作用
WindowToken、WindowState以及DisplayContent。并且在函数开始处对窗口类型的检查判断也初步揭示了它们之间的关系:除子窗口外,添加任何一个窗口都必须指明其所属的WindowToken;窗口在WMS中通过一个WindowState实例进行管理和保管。同时必须在窗口中指明其所属的DisplayContent,以便确定窗口将被显示到哪一个屏幕上。
####4.1 理解WindowToken
WindowToken 的意义
WindowToken 将属于同一个应用组件(Activity、InputMethod、Wallpaper、Dream)的窗口组织在一起。在WMS对窗口的管理过程中,用WindowToken指代一个应用组件。例如在进行窗口Z-Order排序时,属于同一个WindowToken的窗口会被安排在一起。
WindowToken 具有令牌的作用,是对应用组件的行为规范管理的一个手段。WindowToken由应用组件或其管理者负责向WMS声明并持有。应用组件在需要新的窗口时,必须提供WindowToken以表明自己的身份,并且窗口的类型必须与所持有的WindowToken类型一致。
从前面的代码可以看到,在创建系统类型的窗口时不需要提供一个有效的Token,WMS会隐式地为其声明一个WindowToken,看起来谁都可以添加一个系统级的窗口。难道Android为了内部使用方便而置安全于不顾吗?非也,addWindow()函数一开始的mPolicy.checkAddPermission()的目的就是如此。它要求客户端必须拥有SYSTEM_ALERT_WINDOW或INTERNAL_SYSTEM_WINDOW权限才能创建系统类型的窗口。
向WMS声明WindowToken
在SampleWindow应用中,使用wms.addWindowToken()函数声明mToken作为它的令牌,所以在添加窗口时,通过设置lp.token为mToken向WMS出示,从而获得WMS添加窗口的许可。这说明,只要是一个Binder对象(随便一个),都可以作为Token向WMS进行声明。对WMS的客户端来说,Token仅仅是一个Binder对象而已。
[WindowManagerService.java–>WindowManagerService.addWindowToken()]
@Override
public void addWindowToken(IBinder token, int type){
// 需要声明Token的调用者拥有MANAGE_APP_TOKENS 的权限
if(!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS,"addWindowToken()")){
throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
}
synchronized(mWindowMap){
.....
// 注意其构造函数的参数与addWindow()中不同,最后一个参数为true,表明这个人Token是显
// 示声明的
wtoken = new WindowToken(this, token,type,true);
mTokenMap.put(token,wtoken);
......
}
}
ken的调用者拥有MANAGE_APP_TOKENS 的权限
if(!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS,“addWindowToken()”)){
throw new SecurityException(“Requires MANAGE_APP_TOKENS permission”);
}
synchronized(mWindowMap){
…
// 注意其构造函数的参数与addWindow()中不同,最后一个参数为true,表明这个人Token是显
// 示声明的
wtoken = new WindowToken(this, token,type,true);
mTokenMap.put(token,wtoken);
…
}
}