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();
    }

当我们调用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);
    }

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

WindowManager(窗口管理者)

WindowManager接口

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

public interface WindowManager extends ViewManager

可以看到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);
}

首先这个类有一段注释,大概是说:这个接口是让你在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;
    }

可以看到这个方法主要会涉及到三个方法间的跳转,第一个方法中首先会通过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());
    }

可以看到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;
    }
}

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;
            }
        }
    }

相关的重要注释已经在上面的代码处标注出来了,我们可以发现这个方法中动态地维护了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>();

可以看到他们都带有@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);
        }
    }

这段方法中最重要的就是注释一处的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();
        }
    }

可以看到在这里会通过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;
    }
 }
}

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

    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
        	//检查线程
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

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

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/109237.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

复习Animate和木疙瘩学习笔记-动画制作的回家之路

这个融媒体H5制作平台功能比较完善&#xff1a;包含了Flash(现在叫Animate)传统H5网页制作 720全景视频制作发布网页&#xff01; 主要功能&#xff1a;素材导入、2D动画制作、常见交互添加、发布生成链接二维码 基本就是一个制作H5为主&#xff0c;但是里面的动画可以依赖4种…

力扣第738题 单调递增的数字 c++ 暴力超时 贪心优化

题目 738. 单调递增的数字 中等 相关标签 贪心 数学 当且仅当每个相邻位数上的数字 x 和 y 满足 x < y 时&#xff0c;我们称这个整数是单调递增的。 给定一个整数 n &#xff0c;返回 小于或等于 n 的最大数字&#xff0c;且数字呈 单调递增 。 示例 1: 输入: n 1…

【DevChat】智能编程助手 - 使用评测

写在前面&#xff1a;博主是一只经过实战开发历练后投身培训事业的“小山猪”&#xff0c;昵称取自动画片《狮子王》中的“彭彭”&#xff0c;总是以乐观、积极的心态对待周边的事物。本人的技术路线从Java全栈工程师一路奔向大数据开发、数据挖掘领域&#xff0c;如今终有小成…

操作系统运行机制

文章目录 操作系统运行机制特权指令VS非特权指令内核态VS用户态中断和异常内中断(异常)外中断中断机制基本原理中断处理过程 系统调用系统调用和库函数的区别为什系统调用时必须的&#xff1f;什么功能需要用到系统调用系统调用的过程小结 操作系统内核 操作系统运行机制 特权…

Java 四种引用类型

文章目录 前言一、整体架构二、强引用&#xff08;Reference&#xff09;三、软引用&#xff08;SoftReference&#xff09;四、弱引用&#xff08;WeakReference&#xff09;五、虚引用&#xff08;PhantomReference&#xff09;六、引用队列&#xff08;ReferenceQueue&#…

React Hooks 实战案例

文章目录 一、React Hooks 简介二、React Hooks 的基本用法1. 使用 useState 创建状态2. 使用 useEffect 添加副作用 三、React Hooks 的常见问题1. 循环引用问题2. 副作用问题 四、React Hooks 实战案例1. 使用 useReducer 和 Redux&#xff1a;2. 使用 useContext&#xff1a…

如何使用drawio画流程图以及导入导出

画一个基本的流程图 你可以在线使用drawio, 或者drawon创建很多不同类型的图表。 如何使用编辑器&#xff0c;让我们以一个最基本的流程图开始。 流程图&#xff0c;就是让你可视化的描述一个过程或者系统。 图形和很少部分的文字表达就可以让读者很快的理解他们需要什么。 创…

07、SpringCloud -- jmeter 压测

目录 jmeter 入门jmeter 安装测试步骤测试数据模拟多用户操作1、创建http请求2、添加http cookie 管理器3、并发获取当前登录用户数据的效果4、添加多个用户模拟并发请求5、访问方法6、jmeter添加 CSV Data Set Config7、高并发执行访问的效果8、总结流程高并发秒杀压测jmeter …

Python 日期和时间处理教程:datetime 模块的使用

Python 中的日期不是独立的数据类型&#xff0c;但我们可以导入一个名为 datetime 的模块来使用日期作为日期对象。 示例&#xff1a;导入 datetime 模块并显示当前日期&#xff1a; import datetimex datetime.datetime.now() print(x)日期输出 当我们执行上面示例中的代码…

springboot web项目中 Set-Cookie 失败 办法

1. 背景 目前有个项目 线上环境 使用spring session管理的登录 项目中有两个接口 一个用来登录的 登录成功后会设置cookie 后续请求就会使用该cookie &#xff08;cookie的键值就是session Id 和 登录后的信息 例如菜单&#xff0c;权限等&#xff09; 一个用来检查是否登录…

LeetCode热题100 旋转图像

题目描述 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9…

2022年06月 Python(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python等级考试&#xff08;1~6级&#xff09;全部真题・点这里 一、单选题&#xff08;共25题&#xff0c;每题2分&#xff0c;共50分&#xff09; 第1题 运行下列程序&#xff0c;输出的结果是&#xff1f;&#xff08; &#xff09; tup1 (苏炳添, 谷爱凌, 北京冬奥会, …

VSCode编写Unity代码自动补全配置

1.下载并安装.NET 7.0&#xff08;C#插件需要&#xff09;和.NET Framework 4.7.1&#xff08;Unity需要&#xff09; .NET 7.0下载链接&#xff1a;https://dotnet.microsoft.com/en-us/download .NET Framework 4.7.1下载链接&#xff1a;https://dotnet.microsoft.com/en-…

cmd基本命令

一、cmd黑框是什么 cmd 是 Windows 命令提示符&#xff08;cmd.exe&#xff09;是 Windows NT 及以后的 Windows 系统下的一个用于运行 Windows 控制面板程序或某些 DOS 程序的shell程序&#xff1b;或在 Windows CE 下只用于运行控制面板程序的外壳程序。 二、打开步骤 wind…

H5游戏源码分享-命悬一线

H5游戏源码分享-命悬一线 在合适的时机跳下绳子&#xff0c;能安全站到木桩上&#xff0c;就通过。 游戏源码 <!DOCTYPE html> <html> <head><meta http-equiv"Content-Type" content"text/html; charsetutf-8" /><meta name&…

多线程---阻塞队列+生产者消费者模型

文章目录 阻塞队列自己实现一个阻塞队列&#xff08;三步&#xff09;标准库中的阻塞队列使用阻塞队列的优势 生产者消费者模型 阻塞队列 队列&#xff08;Queue&#xff09;是我们熟悉的一个数据结构&#xff0c;它是“先进先出”的。但是并不是所有的队列都是“先进先出”的…

RocketMq源码分析(八)--消息消费流程

文章目录 一、消息消费实现二、消息消费过程1、消息拉取2、消息消费1&#xff09;提交消费请求2&#xff09;消费消息 一、消息消费实现 消息消费有2种实现&#xff0c;分别为&#xff1a;并发消费实现&#xff08;ConsumeMessageConcurrentlyService&#xff09;和顺序消费实现…

pre-existing shared memory block

发生原因: 1.服务器cpu、内存进行扩容 2.非正常关闭,导致任在占用共享内存段 解决方案: 根据shmid进行关闭 ipcs -mipcrm -m xxx

Kotlin协程核心理解

一、协程是什么&#xff1f; 1.1 基本概念的理解 我们知道JVM中的线程的实现是依赖其运行的操作系统决定的&#xff0c;JVM只是在上层进行了API的封装&#xff0c;包含常见的有线程的启动方法&#xff0c;状态的管理&#xff0c;比如&#xff1a;Java中抽象出了6种状态&#x…

windows8080端口占用

查看端口占用 netstat -ano | findstr “8080”查看占用进程 tasklist | findstr “4664”关闭占用进程 taskkill /f /t /im httpd.exe