• Android:窗口管理器WindowManager


    Android:窗口管理器WindowManager

    在这里插入图片描述

    导言

    本篇文章主要是对Android中与窗口(Window)有关的知识的介绍,主要涉及到的有:

    1. Window
    2. WindowManager
    3. WindowManagerService

    主要是为了更进一步地向下地深入Android屏幕渲染的知识(虽然窗口可能并算不上)。

    窗口(Window)

    Q:什么是窗口

    实际上Android上的窗口指的并不是具体的手机窗口而是一个抽象的概念,它本质上也是一个View,我会把窗口理解成一组有关联的View

    ActivityManagerActivityManagerService的关系类似,WindowManager中方法的实现也是通过远程调用WindowManagerService实现的:

    在这里插入图片描述

    窗口的属性

    窗口的类型

    Window的类型大体来分有三种,我们可以在源文件中找到具体的对应:
    在这里插入图片描述

      1. 应用程序窗口:最常见的,顶层应用的显示窗口
      1. 子窗口:需要依附在其他窗口的窗口
      1. 系统窗口: Toast,系统输入法窗口,系统错误窗口等

    另外,每种窗口还有其对应的TYPE值,这个值主要是用来确定窗口的显示层次的,应用程序窗口的TYPE值在1-99范围内,子窗口在1000-1999,系统窗口在2000-2999。至于这个TYPE值会如何影响显示层次呢?这里我们可以简单的将这个TYPE值看做是一个z轴的坐标值,也就是垂直于手机屏幕的距离,数值越大,其离手机屏幕就越远,那么显示的优先级也会越高。

    当然,实际的情况比这要复杂,会涉及到一些加权的计算,这里我们先简单这样理解即可。

    窗口的标志

    窗口的标志决定了窗口的一些响应特性,这里直接给出一些常用的flag理解一下:

    Flag描述
    FLAG_ALLOW_LOCK_WHILE_SCREEN_ON只要窗口可见,就允许在开启状态的屏幕上锁屏
    FLAG_NOT_FOCUSABLE窗口不能获得输入焦点,在设置该标志的同时也会将FLAG_NOT_TOUCH_MODAL设置
    FLAG_NOT_TOUCHABLE窗口不接受任何触摸事件
    FLAG_NOT_TOUCH_MODAL将该窗口区域之外的触摸事件传递给其他的Window,而自己只会处理窗口区域内的触摸事件
    FLAG_KEEP_SCREEN只要窗口可见,就会一直保持长亮
    FLAG_LAYOUT_NO_LIMITS允许窗口显示在手机屏幕之外

    Window的具体实现类PhoneWindow

    这个PhoneWindow我们应该在Activity的setContentView方法中有提及到,这里再简单回顾一下:

        public void setContentView(@LayoutRes int layoutResID) {
            initViewTreeOwners();
            getDelegate().setContentView(layoutResID);
        }
        //Activity.java中
        public void setContentView(View view, ViewGroup.LayoutParams params) {
            getWindow().setContentView(view, params);
            initWindowDecorActionBar();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    当我们调用Activity的setContentView方法时首先会根据后面传入的xml布局文件初始化整棵视图树,之后会获取到Activity自身对应的Window对象,也就是描述Activity该如何显示的一个View,之后再调用该Window的setContentView方法,那这个Window对象是在何处被初始化的呢?答案是在Activity的attach方法中,该方法是在ActivityThread中被调用的:

    final void attach(...) {
            attachBaseContext(context);
    
            mFragments.attachHost(null /*parent*/);
    
            mWindow = new PhoneWindow(this, window, activityConfigCallback);
            ......
            mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以清楚的看到此处将Activity对应的PhoneWindow对象实例创建了出来,并将这个对象与一个WindowManager对象绑定起来,所以说上面Activity调用的setContentView最终是由这个PhoneWindow对象实例来完成的,最终就会在PhoneWindow中安装一个DecorView,DecorView作为整个PhoneWindow中的第一个View(实际上的根View),并把xml中的内容填充进DecorView的内容部分。

    WindowManager(窗口管理者)

    WindowManager接口

    接下来我们从源码角度先分析一下WindowManager:

    public interface WindowManager extends ViewManager
    
    • 1

    可以看到WindowManager本质上是一个继承了ViewManager接口的一个接口,因为ViewManager比较简单,我们先来看ViewManager接口:

    public interface ViewManager
    {
        public void addView(View view, ViewGroup.LayoutParams params);
        public void updateViewLayout(View view, ViewGroup.LayoutParams params);
        public void removeView(View view);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    首先这个类有一段注释,大概是说:这个接口是让你在Activity中添加或者移除子View的
    实际上这三个方法也很直白,addView方法用于添加子View,updateViewLayout用于更新子View,而removeView方法用于移除子View。

    从WindowManager继承了ViewManager这个角度我们也可以看出来Window实际上就是View,WindowManager只不过是在ViewManager接口的基础上添加了对窗口管理的逻辑,包括Window的类型,显示层级等处理。额外的逻辑中根据Window添加了两个方法:

    1. public Display getDefaultDisplay() (该方法已经废弃,用Context.getDisplay()进行替代):得到WindowManager所管理的屏幕 (Display)
    2. public void removeViewImmediate(View view) (同步方法,立即移除一个View,会触发View.onDetachFromWindow回调)

    Window绑定WindowManager

    一开始给出的一个简单的示意图中我们已经明确了一点:Window是由WindowManager进行管理的,并且在上一段中我们知道Window是在ActivityThread调用的attach方法之中通过mWindow.setWindowManager方法来绑定的,这一小段之中我们就来稍微看一眼这个方法的逻辑:

        public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
                boolean hardwareAccelerated) {
            mAppToken = appToken;
            mAppName = appName;
            mHardwareAccelerated = hardwareAccelerated;
            if (wm == null) {
                wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
            }
            mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
        }
    
        public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
            return new WindowManagerImpl(mContext, parentWindow, mWindowContextToken);
        }
    
        private WindowManagerImpl(Context context, Window parentWindow,
                @Nullable IBinder windowContextToken) {
            mContext = context;
            mParentWindow = parentWindow;
            mWindowContextToken = windowContextToken;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    可以看到这个方法主要会涉及到三个方法间的跳转,第一个方法中首先会通过Binder通信获取到系统服务之一的WindowService,之后就会跳转到第二个方法中,创建并返回一个WindowManagerImpl的实例。然后第三个方法创建这个示例的时候实际上就是对传入的数据进行了一个简单的封装,就是将需要绑定的Window对象,上下文对象Context,以及可以与WindowService进行通信的IBinder对象进行了一个封装:
    在这里插入图片描述
    我觉得这样做的目的也很明显,这样一下WindowManagerImpl同时持有了需要被操作的Window提供操作服务的WindowService的通信手段,这样一来就可以借助WindowService来操作Window对象了:
    在这里插入图片描述
    最后,我们可以来看一看WindowManagerImpl的addView方法:

        public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            applyTokens(params);
            mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                    mContext.getUserId());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到WindowManagerImpl自身并不实现addView方法,而是将其委托给mGlobal实现,这个mGlobal实际上是一个WindowManagerGlobal对象,所有的WindowManagerImpl对象都是将其委托给WindowManagerGlobal对象实现的,而WindowManagerGlobal又是一个单例的对象,所以说实际上所有的WindowManagerImpl都是通过过一个对象来实现对View的操作的。

    另外提一嘴,这里WindowManagerImpl将实现分为了抽象和具体两个部分,用到了桥接模式

    //Global是单例的
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    //DCL单例
    public static WindowManagerGlobal getInstance() {
        synchronized (WindowManagerGlobal.class) {
            if (sDefaultWindowManager == null) {
                sDefaultWindowManager = new WindowManagerGlobal();
            }
            return sDefaultWindowManager;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    WindowManager关联类

    实际上在上面介绍Window的过程中我们已经差不多已经把关联类介绍过了,此处借用进阶解密中的一张图来总结:
    在这里插入图片描述

    ViewRootImpl–WindowManager与Window的中转站

    ViewRootImpl的职责

    ViewRootImpl顾名思义就是名义上的View视图树的根节点,它有着多种职责:

    1. View树的根并且管理整颗视图树
    2. 触发View的测量,布局和绘制
    3. 输入事件的中转站
    4. 管理Surface
    5. 负责与WMS进行通信

    关于ViewRootImpl与WMS的通信,具体是通过一个Session进行的,可以看以下这张图:
    在这里插入图片描述

    ViewRootImpl存储Window

    当我们需要将之前创建的PhoneWindow添加到屏幕上时,显然就需要调用到WindowManageraddView方法了,具体我们也知道是会委托到WindowManagerGlobal来执行相关的操作,我们直接跳进WindowManagerGlobal来看看相关的逻辑:

    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow, int userId) {
            ........一些错误检查 
            ViewRootImpl root;
            View panelParentView = null;
    		//上锁
            synchronized (mLock) {
    			//加载参数
    			//判断当前View是否重复添加
    			........
                IWindowSession windowlessSession = null;
    			........
                if (windowlessSession == null) {
                	//如果Session为空就新生成一个ViewRootImpl
                    root = new ViewRootImpl(view.getContext(), display);
                } else {
                	//如果Session为空就新生成一个ViewRootImpl,并且把Session传入
                    root = new ViewRootImpl(view.getContext(), display,
                            windowlessSession);
                }
    			//设置相关的布局参数
                view.setLayoutParams(wparams);
    			//维护三个列表 
    			//Views列表
                mViews.add(view);
                //ViewRootImpl列表
                mRoots.add(root);
                //布局参数列表
                mParams.add(wparams);
                try {
                    //调用ViewRootImpl的setView绑定Window
                    root.setView(view, wparams, panelParentView, userId);
                } catch (RuntimeException e) {
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                    throw e;
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    相关的重要注释已经在上面的代码处标注出来了,我们可以发现这个方法中动态地维护了WindowManagerGlobal中的三个列表:

    @UnsupportedAppUsage
    private final ArrayList<View> mViews = new ArrayList<View>();
    @UnsupportedAppUsage
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    @UnsupportedAppUsage
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以看到他们都带有@UnsupportedAppUsage说明是不支持其他非系统App调用的,第一个列表维护的是被添加的View,第二个列表维护的是生成的ViewRootImpl,第三个列表是Window的布局参数。

    而在这个addView的具体方法中,会先生成一个对应的ViewRootImpl对象作为整颗视图树的根节点,之后还会将被添加的Window和这个根节点绑定起来,这样根节点就可以管理这整颗视图树了。

    读到这里相信大家也知道我为什么称ViewRootImpl为WindowManager与Window之间的中转站了:ViewRootImpl作为根节点管理整个Window,当Window中有请求发出的时候第一时间给ViewRootImpl进行处理,然后ViewRootImpl再通过WindowManagerGlobal的Binder机制与WindowManagerService间接地进行通信。

    题外话:在子线程真的不能更新UI吗

    首先我们需要刷新UI的话首先也是需要通过ViewManager接口中的updateViewLayout方法发起的,在具体实现中是交给WindowManagerGlobal实现的:

        public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
            if (view == null) {
                throw new IllegalArgumentException("view must not be null");
            }
            if (!(params instanceof WindowManager.LayoutParams)) {
                throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
            }
    
            final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    		//1-------1
            view.setLayoutParams(wparams);
    
            synchronized (mLock) {
                int index = findViewLocked(view, true);
                ViewRootImpl root = mRoots.get(index);
                mParams.remove(index);
                mParams.add(index, wparams);
                root.setLayoutParams(wparams, false);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这段方法中最重要的就是注释一处的view.setLayoutParams(wparams)方法中,这个方法还会进行一次跳转,最终会执行到ViewRootImpl的scheduleTraversals方法中:

        void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看到在这里会通过Handler向ViewRoot的handler对象发送一个同步屏障和Runnable任务,这个任务的具体内容如下:

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    
    void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    
        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
    
        performTraversals();
    
        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    实际上就是执行performTraversals()方法,这个方法我们可很熟悉,就是开启三大流程的方法,而这个过程中一旦涉及到performLayout方法的执行就会进行一个线程的检查:

        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
            	//检查线程
                checkThread();
                mLayoutRequested = true;
                scheduleTraversals();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面出现的checkThread()方法就是导致我们平时无法在主线程更新UI的原因,具体逻辑如下:

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个方法是在ViewRootImpl中执行的,也就是说他检查的是ViewRootImp的mThread线程是否是当前的线程,至于这个mThread是在哪里被赋值的,实际上是在其构造函数中被赋值的;

    所以说,并不是只有主线程不能更新UI,而是只有创建ViewRootImpl实例的线程才能更新UI。一般情况下ViewRootImpl的创建都是在ActivityThread,也就是主线程中进行的,所以说才会说只有主线程能更新UI。

    那有没有别的方法可以让我们在子线程更新UI呢?实际上是有的,比如我们可以使用SurfaceView或者TextureView,这些特殊View的绘制过程与一般的View不同,并且他们可以单独持有一个Surface。

    我们也可以自己在代码中添加View,然后让添加View和更新View的操作放在一个线程里跑就好了。

  • 相关阅读:
    jwsManager服务接口实现类-jni实现
    谷粒商城 (二十七) --------- 商品服务 API 商品管理
    坦克车机器人操作学习总结开始篇
    基于Java+微信小程序实现《电子点餐系统》
    Spring 对 OAuth 的实现及前世今生
    JVM的故事——类文件结构
    猿创征文| NoSQL数据库简介
    大学生网页设计制作作业实例代码 (全网最全,建议收藏) HTML+CSS+JS
    Retrofit解密:接口请求是如何适配suspend协程?
    python 从一道作业题到制作一个图形界面的“诈金花”游戏
  • 原文地址:https://blog.csdn.net/Tai_Monster/article/details/134092026