查看原文
其他

一个困惑很久的问题,Android中有子窗口吗?

陈小缘 鸿洋 2021-10-12

稳住,今天是周末。


这是一篇我在 wanandroid上的问答转化而来,小缘可以说把Android 的窗口机制从源码角度说的非常透彻了,非常值得学习一波。


注意 wanandroid 上已经有 130+的问题了,绝对是深入学习Android的宝库,也非常感谢大家的回答,尤其感谢小缘,同时希望问答栏目可以持续的够久,产出足够的内容。


问题:


之前看文章,经常看到一些分析 Dialog、PopupWindow的文章,有些文章分析如下:


Dialog有自己独立的Window,而PopupWindow没有,所以PopupWindow可以称之为子窗口,而 Dialog不是。


问题来了:


1. 这种说法合理吗?

2. 在Android中,有没有子窗口的概念呢?如果有到底应该以什么为标准呢?




小缘:


"Dialog有自己独立的Window,而PopupWindow没有,所以PopupWindow可以称之为子窗口,而 Dialog不是。" 这种说法合理吗?


emmmm,从源码的角度来看,确实是这样定义的。。。


但PopupWindow在系统服务进程那边,还是会有一个对应的WindowState对象的,不能说没有Window,这个我们等下细说。


先来看一张图:

图中列出的都是一些耳熟能详的类:


WindowManagerGlobal里持有同一进程内所有ViewRootImpl的引用。


每个ViewRootImpl都对应着一个Window,当然了这里说的Window并不是指PhoneWindow继承的那个类,指的是AIDL接口IWindow的实现类,在ViewRootImpl中以IBinder的形式存在。


除此之外,ViewRootImpl中还有一个很重要的成员变量mWindowAttributes,它其实是WindowManager的静态内部类LayoutParams,现在列出了其中两个属性,第一个type,就是Window的类型,注意!判断一个ViewRootImpl是否 "子窗口" ,就是根据这个属性来判断的。第二个属性 token,可以理解成ViewRootImpl所属容器的token(这个等下也会介绍到)。


如果你经常接触WindowManager,或者做过一些悬浮窗相关的需求,你会知道在调用WindowManager的addView方法时,需要传入一个WindowManager.LayoutParams对象,这个LayoutParams会在addView方法中保存到新创建的ViewRootImpl对象实例里面,同时这个新创建的ViewRootImpl实例也会被add到WindowManagerGlobal的mRoots中。


在Activity启动时,onResume方法回调之后,ActivityThread就会做一次这样的事,即调用WindowManager的addView方法,把PhoneWindow的DecorView添加进去。添加完之后,我们所看到的界面,它的结构大概就是这样的:

刚刚说过,ViewRootImpl.mWindowAttributes.token,保存的是ViewRootImpl所属容器的token,现在能理解了吧?Activity的主Window的容器就是Activity,所以这里它会持有Activity.mToken的引用。


嗯,如果在Activity中show一个Dialog,它的结构会是怎样的呢:




没错,Dialog看上去是跟Main Window同级别的存在,因为它们的爸爸都是Activity。从源码的角度来看,是因为Dialog在show方法被调用时,它往WindowManager的addView方法传的LayoutParams,type是没有修改过的,默认是TYPE_APPLICATION,官方把这个type定义为 "a normal application window"。


可能有同学已经想到了,既然把PopupWindow看作是子窗口,那它内部在向WindowManager addView的时候,肯定是修改过LayoutParams.type的。


是的,PopupWindow所对应的type是TYPE_APPLICATION_PANEL,它就是子窗口的TYPE。源码上的文档注释是这样说的:"These windows appear on top of their attached window"。


既然子窗口是依附在别的窗口上,那对应ViewRootImpl所属容器的token ,就不是Activity的token了,而是:



而是它依附的ViewRootImpl里面的mWindow属性!


开头说了,ViewRootImpl.mWindow是以IBinder的形式存在,所以能直接赋值给LayoutParams.token


这里提一下,Activity提供的OptionsMenu,也是通过PopupWindow来实现的,所以OptionsMenu也是显示在新的窗口上的。


好啦,应用进程这边大概就说这些,但还没完,还有系统服务进程没说呢!


刚刚一开始讲到,无论是不是子窗口,在系统服务进程那边,都会有一个对应的WindowState对象。


emmmm,还是先来熟悉一下相关类的结构吧:

这边有个叫ConfigurationContainer的类,里面有三个抽象方法:获取子元素总数量、获取子元素对象、获取父容器对象。


到了他的实现类WindowContainer,就多了一个叫mChildren的List,很明显它就是用来储存子元素的。


现在列出了2个WindowContainer的子类,一个是WindowToken,另一个叫WindowState,这两个类都指定了泛型类型为WindowState,也就是说,它从WindowContainer中继承的mChildren装的都应该是WindowState的对象了。


WindowToken还有一个我们或多或少都听说过的子类:ActivityRecord,他就是Activity在系统服务进程对应的对象。关于ActivityRecord相关的类,在之前的回答:进一步了解ActivityRecord、TaskRecord、ActivityStack (https://www.wanandroid.com/wenda/show/12574?fid=7590#msg_id1406)也有介绍过,不过那个是基于SDK API 28分析的,现在API 30已经没有了TaskRecord这个类了。。。但大致结构没变,感兴趣的同学也可以看下。


那么,这些类是怎么跟应用进程那边的ViewRootImpl关联起来的呢?


是这样的:


WindowManagerGlobal在调用ViewRootImplsetView方法(把我们通过WindowManager.addView传进去的View对象交给ViewRootImpl管理)时,最终会调用到WMS的addWindow方法,在addWindow里面,会先找到对应的ActivityRecord,然后根据LayoutParams的type判断是否Child Window,如果不是,则直接添加到ActivityRecord.mChildren中,如果是Child Window的话,会找到Child Window的Parent(跟前面应用进程那边的结构对应,Child Window的Parent还是WindowState对象),然后添加到WindowState.mChildren里面。


Activity还没有添加任何额外的Window时,它对应的ActivityRecord结构是这样的:


mChildren里就只有一个WindowState对象。


当Activity中show了一个Dialog的时候,它是这样的:



还有第三种,添加了一个type为SUB_WINDOW的:



太多线条可能会有点眼花,其实也就是上面所说的,子窗口对应的WindowState对象,添加到了它所在Window的WindowState对象的mChildren里面而已。


可以看出,系统服务进程这边的WindowState的结构,跟应用进程的ViewRootImpl结构都是一一对应的。


现在来回答题目中的问题:


Android中的子窗口应该以什么为标准来判定呢?


应该以WindowManager.LayoutParams里面的type来判定。SUB_WINDOW的type值在1000~1999之间,一般我们直接用WindowManager.LayoutParams中声明好的几个静态属性就够了,比如PopupWindow的type是TYPE_APPLICATION_PANEL (1000),OptionsMenu用的是TYPE_APPLICATION_SUB_PANEL (1002)TYPE_APPLICATION_ATTACHED_DIALOG (1003) 等等,WindowManager.LayoutParams里还有很多其他TYPE,感兴趣的同学可以看下,都有文档注释的。


WindowManagerGlobaladdView方法中,会检查我们传进来的LayoutParams.type,如果type值在1000~1999之间的话,就会把ParentWindow(这个ParentWindow存在对应的WindowManager里面,如果你用来获取WindowManager实例的Context是Activity的话(Activity重写了getSystemService),它的ParentWindow就是Activity的主Window)的token赋值给LayoutParams,也就是在前面的图片中看到的效果(LayoutParams.token = ViewRootImpl.mWindow)。


噢,对了,应该会有同学有这个疑问:


既然Dialog不算子窗口,那为什么在Activity Destroy的时候,如果还有Dialog未dismiss的话,会抛出一个WindowLeaked异常?它是怎么检测出来的?


其实这不关是不是子窗口的事,因为你Dialog所对应的ViewRootImpl.mWindowAttributes的token是Activity的Token,在Activity Destroy时,会遍历WindowManagerGlobal.mRoots,将所有持有Activity.mToken引用的ViewRootImpl移除掉,顺便抛一个异常。


最后,周末起床时间不固定,醒来就放留言。




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读


开启B站少女心,探究APP换肤
两年Android研发,大厂面试问什么?
引入Jetpack架构后,你的App会发生哪些变化?


点击 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存