50 Dialog-Android中有子窗口吗

作者: 凤邪摩羯 | 来源:发表于2021-07-06 08:56 被阅读0次

    之前看文章,经常看到一些分析 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对象。

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

    图片

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

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

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

    WindowToken还有一个我们或多或少都听说过的子类:ActivityRecord,他就是Activity在系统服务进程对应的对象。关于ActivityRecord相关的类,在之前的回有介绍过,不过那个是基于SDK API 28分析的,现在API 30已经没有了TaskRecord这个类了。。。但大致结构没变,感兴趣的同学也可以看下。

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

    是这样的:

    WindowManagerGlobal在调用ViewRootImpl的setView方法(把我们通过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,感兴趣的同学可以看下,都有文档注释的。

    WindowManagerGlobal在addView方法中,会检查我们传进来的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移除掉,顺便抛一个异常。

    相关文章

      网友评论

        本文标题:50 Dialog-Android中有子窗口吗

        本文链接:https://www.haomeiwen.com/subject/blqqultx.html