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

251 篇文章 3 订阅
76 篇文章 5 订阅

这是一篇我在 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在调用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移除掉,顺便抛一个异常。

 

转自:https://mp.weixin.qq.com/s/AguM2NYfflEKin6Nr75XPQ

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
计算自然语言困惑度通常需要使用语言模型来进行。语言模型是一种能够预测下一个词或字符的概率分布的模型,困惑度则是用于评估语言模型预测性能的指标。 在文语境下,常用的语言模型包括基于N-gram的模型、基于神经网络的模型等。其,基于神经网络的语言模型在近年来的发展表现出了极高的性能,并且已经成为了NLP领域的主流。 一个简单的计算困惑度的方法是,使用一个预训练好的文语言模型(例如BERT、GPT等),输入字符串列表的每个字符串,得到每个字符串的困惑度,最后将所有字符串的困惑度求和并求平均。这个方法的实现可以使用Python的HuggingFace库,具体代码如下: ```python from transformers import AutoTokenizer, AutoModelForCausalLM # 加载预训练的文GPT语言模型 tokenizer = AutoTokenizer.from_pretrained("uer/gpt2-chinese-cluecorpussmall") model = AutoModelForCausalLM.from_pretrained("uer/gpt2-chinese-cluecorpussmall") # 定义计算困惑度的函数 def perplexity(text_list): total_loss = 0 total_len = 0 for text in text_list: # 将文本转换为模型输入的格式 input_ids = tokenizer.encode(text, return_tensors="pt") # 计算模型的损失值 loss = model(input_ids, labels=input_ids).loss # 计算困惑度 total_loss += loss.item() total_len += len(input_ids[0]) perplexity = pow(2, total_loss / total_len) return perplexity # 示例使用 text_list = ["这是一句文文本,包含了一些标点符号。", "This is an English sentence that contains some punctuation."] print(perplexity(text_list)) ``` 需要注意的是,困惑度的值越小表示模型的表现越好,因此在计算平均困惑度时需要将各个字符串的困惑度相加并求平均,而不是直接求和。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值