Android:窗口管理器WindowManager

news2024/9/20 22:55:25

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的操作放在一个线程里跑就好了。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vlc打开网络流(如rtmp),并查看媒体信息

打开vlc 选择媒体&#xff0c;打开网络串流 输入rtmp地址&#xff0c;点击播放 选择工具-编解码信息 可以查看节目的编码信息什么的

口袋参谋:如何快速挑选宝贝核心关键词?三种方法,简单有效!

做电商&#xff0c;一定要会选择关键词&#xff01;这是我近十年做电商的经验之谈。 不管是标题还是直通车推广&#xff0c;都需要选择与产品相关度高、符合买家搜索习惯的关键词&#xff0c;只有这样&#xff0c;才能吸引更多自然流量&#xff01;有了流量加持&#xff0c;还…

C#学习相关系列之多线程(七)---Task的相关属性用法

一、Task和Thread的区别 任务是架构在线程之上的,任务最终的执行还是要给到线程去执行的。任务和线程之间不是一对一的关系&#xff0c;任务更像线程池&#xff0c;任务相比线程池有很小的开销和精确的控制。&#xff08;总的来说Task的用法更为先进&#xff0c;在多线程的时候…

人工智能基础_机器学习008_使用正规方程_损失函数进行计算_一元一次和二元一次方程演示_sklearn线性回归演示---人工智能工作笔记0048

自然界很多都是正态分布的,身高,年龄,体重...但是财富不是. 然后我们来看一下这个y = wx+b 线性回归方程. 然后我们用上面的代码演示. 可以看到首先import numpy as np 导入numby 数据计算库 import matplotlib.pyplot as plt 然后导入图形画的库 然后: X = np.linspace(0,…

【计算机毕设经典案例】基于微信小程序的图书管理系统

前言&#xff1a;我是IT源码社&#xff0c;从事计算机开发行业数年&#xff0c;专注Java领域&#xff0c;专业提供程序设计开发、源码分享、技术指导讲解、定制和毕业设计服务 &#x1f449;IT源码社-SpringBoot优质案例推荐&#x1f448; &#x1f449;IT源码社-小程序优质案例…

数组与链表算法-数组与多项式

目录 数组与链表算法-数组与多项式 多项式数组表达式 C代码 数组与链表算法-数组与多项式 多项式是数学中相当重要的表达方式&#xff0c;如果使用计算机来处理多项式的各种相关运算&#xff0c;那么通常使用数组或链表来存储多项式。 多项式数组表达式 假如一个多项&…

网络安全(黑客技术)—小白自学

目录 一、自学网络安全学习的误区和陷阱 二、学习网络安全的一些前期准备 三、网络安全学习路线 四、学习资料的推荐 想自学网络安全&#xff08;黑客技术&#xff09;首先你得了解什么是网络安全&#xff01;什么是黑客&#xff01; 网络安全可以基于攻击和防御视角来分类&am…

PostgreSQL 基础知识

执行环境&#xff1a; psql 1. 创建一个表格 CREATE TABLE customers ( customer_id serial PRIMARY KEY,firstname VARCHAR(100) NOT NULL,lastname VARCHAR(100) NOT NULL,username VARCHAR(50) UNIQUE NOT NULL,password VARCHAR(50) NOT NULL,email VARCHAR(255) UNIQUE …

Go学习第十三章——Gin(请求与响应)

Go web框架——Gin&#xff08;请求与响应&#xff09; 1 响应1.1 String1.2 JSON&#xff08;*&#xff09;1.3 HTML&#xff08;*&#xff09;1.4 XML1.5 文件&#xff08;*&#xff09; 2 请求2.1 请求参数查询参数 (Query)动态参数 (Param)表单参数 (PostForm)原始参数 (Ge…

[导弹打飞机H5动画制作]飞机路线的随机起飞及自爆模拟

参考代码&#xff1a; this.btnOff.addEventListener("click", off.bind(this)); this.btnBomb.addEventListener("click", bomb.bind(this)); var _this this; var pb null; function off() {if (pb null) {pb new lib.sb1link();pb.x 600;pb.y 30…

Spring Cloud:三【详细】

目录 Http客户端Feign Feign的使用 Feign自定义配置 第一种方式 第二种方式 Feign的优化 Feign最佳实践方式 实现一 实现二 Http客户端Feign RestTemplate缺点是&#xff0c;url不统一&#xff0c;编写困难&#xff0c;可读性差&#xff0c;参数复杂难以维护。 这时…

【计算机网络笔记】DNS报文格式

DNS 提供域名到主机IP地址的映射  域名服务的三大要素&#xff1a;  域&#xff08;Domain&#xff09;和域名(Domain name)&#xff1a; 域指由地 理位置或业务类型而联系在一起的一组计算机构 成。  主机&#xff1a;由域名来标识。域名是由字符和&#xff08;或&a…

刚刚:腾讯云3年轻量2核2G4M服务器优惠价格366元三年

腾讯云3年轻量2核2G4M服务器&#xff0c;2023双十一优惠价格366元三年&#xff0c;自带4M公网带宽&#xff0c;下载速度可达512KB/秒&#xff0c;300GB月流量&#xff0c;50GB SSD盘系统盘&#xff0c;腾讯云百科txybk.com分享腾讯云轻量2核2G4M服务器性能、优惠活动、购买条件…

雷电_安卓模拟器安装burpsuit_CA证书

雷电_安卓模拟器安装burpsuit_CA证书 文章目录 雷电_安卓模拟器安装burpsuit_CA证书雷电模拟器官网&#xff1a;https://www.ldmnq.com 安装burpsuit证书1 打开雷电模拟器右上角的winfi图标 -->点击齿轮2 修改网络3 选择高级 —》手动4 查看真实机IP的地址&#xff0c;选择虚…

超分辨率重建——SESR网络训练并推理测试(详细图文教程)

最近学了一个超轻量化的超分辨率重建网络SESR&#xff0c;效果还不错。 目录 一、 源码包二、 数据集的准备2.1 官网下载2.2 网盘下载 三、 训练环境配置四、训练4.1 修改配置参数4.2 导入数据集4.3 2倍超分网络训练4.3.1 训练SESR-M5网络4.3.2 训练SESR-M5网络4.3.3 训练SESR…

Kitex踩坑 [Error] KITEX: processing request error,i/o timeout

报错问题 2023/010/28 17:20:10.250768 default_server_handler.go:234: [Error] KITEX: processing request error, remoteService, remoteAddr127.0.0.1:65425, errordefault codec read failed: read tcp 127.0.0.1:8888->127.0.0.1:65425: i/o timeout 分析原因 Hert…

【刷题笔记10.28】Leetcode:使括号有效的最少添加

Leetcode&#xff1a;使括号有效的最少添加 上代码 方法一&#xff1a;使用Deque 双向队列 /*** 方法一&#xff1a;使用Deque 双向队列* param s* return*/public int minAddToMakeValid(String s) {//1、将括号字符串转换为char[] 字符数组char[] data s.toCharArray();//2…

SDK 资源

目录 资源的使用 带资源的.exe文件的编译方式 向窗口发送消息 菜单 加载菜单 菜单消息 图标 光标 快捷键 字符串 资源的使用 在VS2019中&#xff0c;点击视图下的其他窗口&#xff0c;资源视图&#xff0c;就可以看到本项目的所有资源 鼠标右键添加-资源&#xff0c…

数据特征工程 | 主成分分析(Python)

特征抽取(feature extraction)和特征选择(feature selection)不一样,特征抽取是从原特征集中推导出有用的信息构成新的特征集。特征选择是从原特征集中选择一部分子集作为训练特征。 特征抽取将数据集从一个特征空间投影到了一个更低维度的特征空间。 主成分分析(princ…

C++设计模式_16_Adapter 适配器

Adapter 适配器也是属于“接口隔离”模式&#xff0c;也是间接思想在某一个层面的应用。 文章目录 1. 动机( Motivation)2. 模式定义3. 结构 (Structure)4. Adapter 适配器的代码实现5. 要点总结6. 其他参考 1. 动机( Motivation) 在软件系统中&#xff0c;由于应用环境的变化…