Android 中 Window 的管理

一、理解 Android 的 Window

Window 表示一个窗口的概念,是一个抽象的概念,每一个 Window 都对应一个 View 和一个 ViewRootImpl,Window 和 View 通过 ViewRootImpl 来建立联系,因此 Window 并不是实际存在的,它是以 View 的形式存在。

Android 中的每个窗口 View 都有一个对应的 Window,例如 Activity、Dialog,在他们初始化的时候就会为其创建对应的 PhoneWindow 并赋值到其内部的一个引用

window 的层级

WindowLayoutParams.setType 设置

每个 window 都有其对应的层级,应用 window 在 1-99,子 window 在 1000-1999,系统 window 在 2000-2999 ,层级高的会覆盖层级低的

子 window 必须依赖于父 window 存在,例如 Dialog 必须在 Activity 中弹出,Dialog 中的 window 为子 window ,Activity 中的 window 为父 window

显示系统级别的 window 需要权限 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

WindowLayoutparams 的 flags

FLAG_NOT_FOUCSABLE window 不需要获取焦点,也不需要接收各种输入事件,回同时启用 FLAG_NOT_TOUCH_MODAL

FLAG_NOT_TOUCH_MODAL 系统会将当前 Window 区域外的单击事件传递给底层的 Window,在当前 Window 区域内的事件则自己处理

FLAG_SHOW_WHEN_LOCKED 开启此模式让 window 显示在锁屏界面上

二、理解 Android 中的 WindowManager

Android 中对 Window 的管理都是通过 WindowManager 来完成的,创建 PhoneWindow 之后还会为该 Window 对象设置 WindowManager ,WindowManager 是一个接口继承 ViewManager 接口,从这里也能看出对 Window 的操作其实就是对 View 的操作,WindowManager 的实现类是 WindowMangerImpl ,WindowMangerImpl 通过 new 创建。

三、Window 与 WindowManagerImpl 的关联

通过 ContextImpl 的 getSystemService 可以得到 WindowManagerImpl 实例,同一 ContextImpl 得到的是同一个 WindowManagerImpl对象,得到 WindowMangerImpl 之后,调用 Window 的 setWindowManager 方法建立 Window 与 WindowManagerImpl 之间的联系。

setWindowManager 中主要完成在 WindowManagerImpl 实例的基础上重新创建一个与当前 Window 绑定的 WindowManagerImpl,并为 Window 中的属性 mWindowManager 赋值

也就是说在 Java 层上 Window 与 WindowManager 建立了第一步联系,并将 Activity、Dialog 等中的 WindowManager 赋值为新的 WindowManagerImpl 对象。

注意:这里是使用单例的 WindowManagerImpl ,结合不同的 Window ,最后构建了与 Window 有关联的非单例的 WindowManagerImpl 对象

四、对 Window 的操作

1. 添加操作 WindowManagerImpl.addView,注意,是添加一个新的 Window ,不是对一个 Window 中的 view 做操作

Android 中每显示一个窗口,其实就是将 View 显示到屏幕的过程,如果我们自定义一个要显示的布局,拿到 View 对象,这时候只要调用 WindowManagerImpl 对象的 addView 方法就行了,通过 ContextImpl 的 getSystemService 可以得到 WindowManagerImpl 实例

WindowManagerImpl 对象,在 WindowManagerImpl 中存在一个单例存在的 WindowManagerGlobal 对象,在 WindowManagerImpl 的各个方法中,将任务的执行过程传递到了 WindowManagerGlobal 中,在传递过程中除了将 View、LayoutParams 传递,还将 WindowManagerImpl 中关联的 window 对象也一起传递

WindowManagerGlobal 的 addView 方法

WindowMAnagerGlobal 中判断 view、LayoutParams 等参数合法性,创建 ViewRootImpl ,将 ViewRootImpl、View、LayoutParams 添加到 WindowMAnagerGlobal 中对应的 ArayyList 集合中,再调用 ViewRootImpl 的 setView 方法

ViewRootImpl

继承自 Handler 类,是作为 native 层和 Java 层 View 系统通信的桥梁

ViewRootImpl 创建时保存了创建其的线程的引用,开发过程中更新 View 时会判断当前线程是否是创建 ViewRootImpl 的线程,如果不是会抛出异常。

一般都是在主线程中创建 ViewRootImpl ,所以在子线程更新 UI 会抛出异常,是因为 ViewRootImpl 是 UI 线程中创建的,并不是因为只有 UI 线程才可以更新 UI

在 Activity 的 onResume 之前如果在子线程中修改 UI 是不会抛出异常的,因为在 onResume 之后才创建 ViewRootImpl,这时更新 UI 需要经过 ViewRootImpl 来更新,在 onResume 之前 Activity 的屏幕并没有显示,修改 UI 操作只是会修改 layout 中的 UI,并不会调用 ViewRootImpl 的方法显示到屏幕上。

所以得出结论,只有 UI 显示到屏幕上之后,在更新 UI 时就会判断线程是否为创建 UI 的线程,如果不匹配则抛出异常,在 UI 没有显示到屏幕上时更新 UI 是不会进行线程判断的

ViewRootImpl 的 setView 方法:

  1. setView 方法中会首先调用 requestLayout() 方法,在这里进行线程判断,如果线程匹配则调用 scheduleTraversals() 完成 View 的测量、布局、绘制过程

  2. setView 中接下来远程调用 IWindowSession 对象中的 addToDisplay 方法,将 window 等信息传递到 NMS 中调用 NMS 的 addWindow 方法完成最后window 在屏幕上的展示。

IWindowSession WindowManagerService

这里是将 View 显示到屏幕上的关键,是将 View 从应用进程传递到系统进程然后完成显示的地方。 ViewRootImpl 中的 IWindowSession 对象是通过 WindowManagerGlobal 的静态方法 getWindowSession() 得到的,该方法中首先通过 ServiceManager 通过 Binder 进行 IPC 得到系统服务 WindowsManagerService 在应用进程的远程代理,然后通过 AIDL 通信的方式调用 WMS 的 openSession() 方法得到 IWindowSession 接口的实现类 Session 类远程代理对象,Session 类也是一个 Binder 所以可以跨进程传递

在 Session 的远程代理的 addToDisplay 方法中通过 AIDL 调用 Session 的 addToDisplay 方法将 window 信息传递到系统进程,然后调用 AMS 的 addWindow 方法,AMS 最后将 Window 中的 View 显示到屏幕上

2. 删除操作,是删除一个屏幕上已有的 Window

WindowManagerImpl.removeView 操作中,先通过 findViewLocked 查找要删除的 View,再通过 View 找到对应的 Window 的 ViewRootImpl ,将 View、LayoutParams、ViewRootImpl 从相对应的 ArrayList 中删除,再通过 IPC 调用 Session 的 remove 其中调用 WMS 的 removeWindow 方法,在屏幕上移除该 Window 对应的 View

3. 更新操作

WindowManagerImpl.updateViewLayout ,为 view 设置新的 LayoutParams ,通过 findViewLocked 找到对应 ViewRootImpl,删除 LayoutParams 集合中旧的 LayoutParams,在集合原位置加入 新的 LayoutParams,调用 ViewRootImpl 的 setLayoutParams 完成 View 的重新测量,布局,绘制,最后通过 IPC 调用 Session 再调用 WMS 完成 window 的更新。

4. 添加 Window 代码

自定义的 Window 在创建过程中并没有主动的创建 Window,而是在显示的时候由系统维护,这里也体现了 Window 是一个抽象的概念,最终需要处理的还是 View

private void addWindow() {
    TextView view = new TextView(this);
    view.setText("Text");
    view.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.i("renxl", "onClick");
        }
    });
    view.setBackgroundColor(Color.RED); // 要显示的 View 可以是新创建的,也可以是 LayoutInflater 从 xml 布局中获取的

    WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams(
            WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0,
            PixelFormat.TRANSPARENT); // 必须是 WindowManager.LayoutParams
    mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; // 三种 flag 从中选一
    mLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST; // type 表示优先级
    mLayoutParams.gravity = Gravity.START | Gravity.TOP; 
    mLayoutParams.x = 100; // 在屏幕上 X 轴位置
    mLayoutParams.y = 300; // 在屏幕上 Y 轴位置

    WindowManager manager = (WindowManager) getSystemService(WINDOW_SERVICE);
    manager.addView(view, mLayoutParams); // 将 View 添加到界面上
}

**注意,如果是系统级别的 Window 也就是优先级超过 1999 的,需要声明权限**
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

五、用户触摸屏幕事件处理流程

WMS 将事件 IPC 传递到 Window,Window 中调用其内部的 CallBack 对象也就是 Activity 或者 Dialog 对象或者的方法。最终将事件传递到 View,再通过 ViewRootImpl 将响应以后的 View 和对应 window IPC 提交到 WMS 完成响应后的展示。

六、常见 Window 的创建

1. Activity 的 Window 创建过程

Activity 启动流程中,Activity 的 attach 方法中为 window 赋值为一个新的 PhoneWindow ,setContentView 将 layout 传递到 PhoneWindow 中,PhoneWindow 中通过 LayoutInflater 的 inflate 方法加载布局 View,并添加到 PhoneWindow 内部的 DecorView 中,在 onResume 的时候,调用 WindowManager 的 addView 方法将 Window 中的 DecorView 通过 WMS 显示到屏幕上

Activity 在创建 Window 的时候,实现了 Window 的 Callback 接口中的方法,在 Window 收到触摸时,则会回调 Callback 中的方法将事件传递到 Activity 中,Activity 中会调对应 PhoneWindow 中的分发方法,PhoneWindow 中会调用 DecordView 中的方法, 最终将事件传递到 View 中。

猜测事件由 WMS 传递到 Window 再到 Activity 再到 Window 这样多一层 Activity 的原因是,开发者可以在 Activity 中处理事件,不一定非要传递到 View

2. Dialog 的 Window 创建过程

同 Activity,实例化 Dialog 对象时创建 PhoneWindow ,show 方法调用时通过 AIDL 调用 WMS 的 addView 方法将 View 添加到屏幕

3. Toast 的 Window 创建过程

Toast 在创建过程中并没有主动的创建 Window,而是在显示的时候由系统维护 Toast 的 window,这里也体现了 Window 是一个抽象的概念,最终需要处理的还是 View

Toast 的工作工程需要 TN NMS WMS 三个部分协同完成,IN 也是一个 Binder,NMS 中调用 TN 是远程访问,TN 调用 WMS 也是远程调用

NMS 即 NotificationManagerService

Toast 的工作过程分为两步

  1. 第一步是当前线程中要先生成一个 Binder 类型的 TN 的对象,远程调用 NMS 中的 enquneue 方法将 TN 传到 NMS 中,NMS 远程调用 TN 的 show 方法,TN 中 show 方法运行在当前应用的 Binder 线程池中,通过 Handler 的 post 系列方法将进程切换到主线程,主线程再通过 WindowManager 来调用 WMS 中的方法完成 show 过程

  2. NMS 中调用了 TN 的 show 方法之后,会通过自己内部的 Handler 延时发送一个时间为 Toast 展示时间的消息,NMS 中的 Handler 收到消息之后,再调用 TN 的 hide 方法(远程调用过程),TN 中的 hide 方法又会通过 WindowManager 远程调用 WMS 中的 hide 方法,将 Toast 隐藏。完成整个过程。

七、总结

  1. 屏幕展示的每一个 window,都需要 window 和 View 两个相互结合,屏幕中可以有多个 Window。以下所说的 View 都是一个 Window 中包含的根 View

  2. window 的创建以及对 View 的添加,删除、更新是由 WindowManager 来实现的,而 WindowManager 中对 window 的操作通过 每个 window 对应的 ViewRootImpl 中通过 IPC 远程请求 IWindowSession 中的方法再调用 WMS 的对应方法将对当前 window 操作的实现到屏幕上。

  3. 每一个 Window 都对应一个 ViewRootImpl ,window 通过对应的 ViewRootImpl 来完成对 view 的管理

  4. 在屏幕有用户交互的时候,WMS 又会将事件传递到相应界面的 Window,Window 会调用当前界面的对应的 CallBack 来处理事件

  5. WindowManager 是接口,实现类是 WindowManagerImpl,WindowManagerImpl 中又通过 WindowMAnagerGlobal 来完成操作。典型的桥接模式

添加 Window 显示不出来问题

由于国内对于 ROM 的定制,多种机型会默认禁止应用对悬浮窗的创建,所以如果是没有显示,检查是否关闭了应用的权限。

  1. 安卓 6.0 添加了对权限的开关设置,悬浮窗权限默认是关闭的

  2. 一些国内定制的 Rom 6.0 之前就可以设置权限的开关,悬浮窗权限默认关闭

问题解决

mLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;

将 type 设置为 TYPE_TOAST , 源码中对 TYPE_TOAST 是没有任何限制的。

在国内定制的 Rom 上,只有少数机型会在设置 TYPE_TOAST 的时候,View 的监听事件不能获取,显示都是可以的。

推荐阅读更多精彩内容