您的位置:首页 > 移动开发 > Android开发

Android应用Activity、Dialog、PopWindow、Toast窗口添加机制及源码分析

2015-07-17 15:59 736 查看

1 背景

之所以写这一篇博客的原因是因为之前有写过一篇《Android应用setContentView与LayoutInflater加载解析机制源码分析》, 然后有人在文章下面评论和微博私信中问我关于Android应用Activity、Dialog、PopWindow加载显示机制是咋回事,所以我就写一 篇文章来分析分析吧(本文以Android5.1.1 (API 22)源码为基础分析),以便大家在应用层开发时不再迷糊。

PS一句:不仅有人微博私信我这个问题,还有人问博客插图这些是用啥画的,这里告诉大家。就是我,快来猛戳我

还记得之前《Android应用setContentView与LayoutInflater加载解析机制源码分析》这篇文章的最后分析结果吗?就是如下这幅图:



在那篇文章里我们当时重点是Activity的View加载解析xml机制分析,当时说到了Window的东西,但只是皮毛的分析了Activity相关的一些逻辑。(PS:看到这不清楚上面啥意思的建议先移步到《Android应用setContentView与LayoutInflater加载解析机制源码分析》,完事再回头继续看这篇文章。)当时给大家承诺过我们要从应用控件一点一点往下慢慢深入分析,所以现在开始深入,但是本篇的深入也只是仅限Window相关的东东,之后文章还会继续慢慢深入。

2 浅析Window与WindowManager相关关系及源码

通过上面那幅图可以很直观的看见,Android屏幕显示的就是Window和各种View,Activity在其中的作用主要是管理生命周期、建 立窗口等。也就是说Window相关的东西对于Android屏幕来说是至关重要的(虽然前面分析Activity的setContentView等原理 时说过一点Window,但那只是皮毛。),所以有必要在分析Android应用Activity、Dialog、PopWindow加载显示机制前再看 看Window相关的一些东西。

2-1 Window与WindowManager基础关系

在分析Window与WindowManager之前我们先看一张图:



接下来看一点代码,如下:

可以看见,ViewManager接口定义了一组规则,也就是add、update、remove的操作View接口。也就是说ViewManager是用来添加和移除activity中View的接口。继续往下看:

看见没有,WindowManager继承自ViewManager,然后自己还是一个接口,同时又定义了一个静态内部类LayoutParams(这个 类比较重要,后面会分析。提前透漏下,如果你在APP做过类似360助手屏幕的那个悬浮窗或者做过那种类似IOS的小白圆点,点击展开菜单功能,你或多或 少就能猜到这个类的重要性。)。WindowManager用来在应用与Window之间的接口、窗口顺序、消息等的管理。继续看下 ViewManager的另一个实现子类ViewGroup,如下:

这下理解上面那幅图了吧,所以说View通过ViewGroup的addView方法添加到ViewGroup中,而ViewGroup层层嵌套到最顶级都会显示在在一个窗口Window中(正如上面背景介绍中《Android应用setContentView与LayoutInflater加载解析机制源码分析》的示意图一样),其中每个View都有一个ViewParent类型的父节点mParent,最顶上的节点也是一个viewGroup,也即前面文章分析的Window的内部类DecorView(从《Android应用setContentView与LayoutInflater加载解析机制源码分析》的总结部分或者《Android应用层View绘制流程与源码分析》的5-1小节都可以验证这个结论)对象。同时通过上面背景中那幅图可以看出来,对于一个Activity只有一个DecorView(ViewRoot),也只有一个Window。

2-2 Activity窗口添加流程拓展

前面文章说过,ActivityThread类的performLaunchActivity方法中调运了activity.attach(…)方法进行初始化。如下是Activity的attach方法源码:

看见没有,Activity类中的attach方法又创建了Window类型的新成员变量mWindow(PhoneWindow实现类)与 Activity相关联,接着在Activity类的attach方法最后又通过mWindow.setWindowManager(…)方法创建了与 Window相关联的WindowManager对象,最后又通过mWindow.getWindowManager()将Window的 WindowManager成员变量赋值给Activity的WindowManager成员变量mWindowManager。

接下来我们看下上面代码中的mWindow.setWindowManager(…)方法源码(PhoneWindow没有重写抽象Window的setWindowManager方法,所以直接看Window类的该方法源码),如下:

可以看见,Window的setWindowManager方法中通过WindowManagerImpl实例的createLocalWindowManager方法获取了WindowManager实例,如下:

看见没有?这样就把Activity的Window与WindowManager关联起来了。Activity类的Window类型成员变量mWindow及WindowManager类型成员变量mWindowManager就是这么来的。

回过头继续看上面刚刚贴的Activity的attach方法代码,看见mWindow.setWindowManager方法传递的第一个参数 没?有人会想(WindowManager)context.getSystemService(Context.WINDOW_SERVICE)这行代 码是什么意思,现在告诉你。

《Android应用Context详解及源码解析》一 文中第三部分曾经说过ActivityThread中创建了Acitivty(执行attach等方法)等东东,在创建这个Activity之前得到了 Context的实例。记不记得当时说Context的实现类就是ContextImpl吗?下面我们看下ContextImpl类的静态方法块,如下:

看见没有,我们都知道Java的静态代码块是类加载是执行一次的,也就相当于一个全局的,这样就相当于每个Application只有一个WindowManagerImpl(display)实例。

还记不记得《Android应用setContentView与LayoutInflater加载解析机制源码分析》一文2-6小节中说的,setContentView的实质显示是触发了Activity的resume状态,也就是触发了makeVisible方法,那我们再来看下这个方法,如下:

特别注意,看见makeVisible方法的wm变量没,这个变量就是Window类中通过调运WindowManagerImpl的 createLocalWindowManager创建的实例,也就是说每一个Activity都会新创建这么一个WindowManager实例来显示 Activity的界面的,有点和上面分析的ContextImpl中static块创建的WindowManager不太一样的地方就在于 Context的WindowManager对每个APP来说是一个全局单例的,而Activity的WindowManager是每个Activity 都会新创建一个的(其实你从上面分析的两个实例化WindowManagerImpl的构造函数参数传递就可以看出来,Activity中Window的 WindowManager成员在构造实例化时传入给WindowManagerImpl中mParentWindow成员的是当前Window对象,而 ContextImpl的static块中单例实例化WindowManagerImpl时传入给WindowManagerImpl中 mParentWindow成员的是null值)。

继续看makeVisible中调运的WindowManagerImpl的addView方法如下:

这里当前传入的view是mDecor,LayoutParams呢?可以看见是getWindow().getAttributes(),那我们进去看看Window类的这个属性,如下:

原来是WindowManager的静态内部类LayoutParams的默认构造函数:

看见没有,Activity窗体的WindowManager.LayoutParams类型是TYPE_APPLICATION的。

继续回到WindowManagerImpl的addView方法,分析可以看见WindowManagerImpl中有一个单例模式的 WindowManagerGlobal成员mGlobal,addView最终调运了WindowManagerGlobal的addView,源码如 下:

可以看见,在addView方法中会利用LayoutParams获得Window的属性,然后为每个Window创建ViewRootImpl, 最后通过ViewRootImpl的setView方法通过mSession向WindowManagerService发送添加窗口请求把窗口添加到 WindowManager中,并且由WindowManager来管理窗口的view、事件、消息收集处理等(ViewRootImpl的这一添加过程 后面会写文章分析,这里先记住这个概念即可)。

至此我们对上面背景中那幅图,也就是《Android应用setContentView与LayoutInflater加载解析机制源码分析》这篇文章总结部分的那幅图又进行了更深入的一点分析,其目的也就是为了下面分析Android应用Dialog、PopWindow、Toast加载显示机制做铺垫准备。

2-3 继续顺藤摸瓜WindowManager.LayoutParams类的源码

上面2-1分析Window与WindowManager基础关系时提到了WindowManager有一个静态内部类LayoutParams, 它继承于ViewGroup.LayoutParams,用于向WindowManager描述Window的管理策略。现在我们来看下这个类(PS:在 AD上也可以看见,自备梯子点我看AD的),如下:

看见没有,从上面类可以看出,Android窗口类型主要分成了三大类:

应用程序窗口。一般应用程序的窗口,比如我们应用程序的Activity的窗口。

子窗口。一般在Activity里面的窗口,比如对话框等。

系统窗口。系统的窗口,比如输入法,Toast,墙纸等。

同时还可以看见,WindowManager.LayoutParams里面窗口的type类型值定义是一个递增保留的连续增大数值,从注释可以看 出来其实就是窗口的Z-ORDER序列(值越大显示的位置越在上面,你需要将屏幕想成三维坐标模式)。创建不同类型的窗口需要设置不同的type值,譬如 上面拓展Activity窗口加载时分析的makeVisible方法中的Window默认属性的type=TYPE_APPLICATION。

既然说这个类很重要,那总得感性的体验一下重要性吧,所以我们先来看几个实例。

2-4 通过上面WindowManager.LayoutParams分析引出的应用层开发常用经典实例

有了上面分析相信你一定觉得WindowManager.LayoutParams还是蛮熟悉的,不信我们来看下。

Part1:开发APP时设置Activity全屏常亮的一种办法(设置Activity也就是Activity的Window):

这是运行结果:



Part2:App开发中弹出软键盘时下面的输入框被软件盘挡住问题的解决办法:

在Activity中的onCreate中setContentView之前写如下代码:

Part3:创建悬浮窗口(仿IPhone的小圆点或者魅族的小白点或者360手机卫士的小浮标),退出当前Activity依旧可见的一种实现方法:

省略了Activity的start与stop Service的按钮代码,直接给出了核心代码如下:

如下是运行过程模拟,特别留意屏幕右下角的变化:



怎么样,通过最后这个例子你是不是就能体会到WindowManager.LayoutParams的Z-ORDER序列类型,值越大显示的位置越在上面。

2-5 总结Activity的窗口添加机制

有了上面这么多分析和前几篇的分析,我们对Activity的窗口加载再次深入分析总结如下:



可以看见Context的WindowManager对每个APP来说是一个全局单例的,而Activity的WindowManager是每个 Activity都会新创建一个的(其实你从上面分析的两个实例化WindowManagerImpl的构造函数参数传递就可以看出来,Activity 中Window的WindowManager成员在构造实例化时传入给WindowManagerImpl中mParentWindow成员的是当前 Window对象,而ContextImpl的static块中单例实例化WindowManagerImpl时传入给 WindowManagerImpl中mParentWindow成员的是null值),所以上面模拟苹果浮动小图标使用了Application的 WindowManager而不是Activity的,原因就在于这里;使用Activity的WindowManager时当Activity结束时 WindowManager就无效了,所以使用Activity的getSysytemService(WINDOW_SERVICE)获取的是 Local的WindowManager。同时可以看出来Activity中的WindowManager.LayoutParams的type为 TYPE_APPLICATION。

好了,上面也说了不少了,有了上面这些知识点以后我们就来开始分析Android应用Activity、Dialog、PopWindow窗口显示机制。

3 Android应用Dialog窗口添加显示机制源码

3-1 Dialog窗口源码分析

写过APP都知道,Dialog是一系列XXXDialog的基类,我们可以new任意Dialog或者通过Activity提供的 onCreateDialog(……)、onPrepareDialog(……)和showDialog(……)等方法来管理我们的Dialog,但是究 其实质都是来源于Dialog基类,所以我们对于各种XXXDialog来说只用分析Dialog的窗口加载就可以了。

如下从Dialog的构造函数开始分析:

可以看到,Dialog构造函数首先把外部传入的参数context对象赋值给了当前类的成员(我们的Dialog一般都是在Activity中启动的, 所以这个context一般是个Activity),然后调用 context.getSystemService(Context.WINDOW_SERVICE)获取WindowManager,这个 WindowManager是哪来的呢?先按照上面说的context一般是个Activity来看待,可以发现这句实质就是Activity的 getSystemService方法,我们看下源码,如下:

看见没有,Dialog中的WindowManager成员实质和Activity里面是一样的,也就是共用了一个WindowManager。

回到Dialog的构造函数继续分析,在得到了WindowManager之后,程序又新建了一个Window对象(类型是PhoneWindow 类型,和Activity的Window新建过程类似);接着通过w.setCallback(this)设置Dialog为当前window的回调接 口,这样Dialog就能够接收事件处理了;接着把从Activity拿到的WindowManager对象关联到新创建的Window中。

至此Dialog的创建过程Window处理已经完毕,很简单,所以接下来我们继续看看Dialog的show与cancel方法,如下:

可以看见Dialog的新Window与Activity的Window的type同样都为TYPE_APPLICATION,上面介绍 WindowManager.LayoutParams时TYPE_APPLICATION的注释明确说过,普通应用程序窗口 TYPE_APPLICATION的token必须设置为Activity的token来指定窗口属于谁。所以可以看见,既然Dialog和 Activity共享同一个WindowManager(也就是上面分析的WindowManagerImpl),而WindowManagerImpl 里面有个Window类型的mParentWindow变量,这个变量在Activity的attach中创建WindowManagerImpl时传入 的为当前Activity的Window,而当前Activity的Window里面的mAppToken值又为当前Activity的token,所以 Activity与Dialog共享了同一个mAppToken值,只是Dialog和Activity的Window对象不同。

3-2 Dialog窗口加载总结

通过上面分析Dialog的窗口加载原理,我们总结如下图:



从图中可以看出,Activity和Dialog共用了一个Token对象,Dialog必须依赖于Activity而显示(通过别的 context搞完之后token都为null,最终会在ViewRootImpl的setView方法中加载时因为token为null抛出异常),所 以Dialog的Context传入参数一般是一个存在的Activity,如果Dialog弹出来之前Activity已经被销毁了,则这个 Dialog在弹出的时候就会抛出异常,因为token不可用了。在Dialog的构造函数中我们关联了新Window的callback事件监听处理, 所以当Dialog显示时Activity无法消费当前的事件。

到此Dialog的窗口加载机制就分析完毕了,接下来我们说说应用开发中常见的一个诡异问题。

3-3 从Dialog窗口加载分析引出的应用开发问题

有了上面的分析我们接下来看下平时开发App初学者容易犯的几个错误。

实现在一个Activity中显示一个Dialog,如下代码:

分析:使用了Activity为context,也即和Activity共用token,符合上面的分析,所以不会报错,正常执行。

实现在一个Activity中显示一个Dialog,如下代码:

分析:传入的是Application的Context,导致TYPE_APPLICATION类型Dialog的token为null,所以抛出如下异常,无法显示对话框。

实现在一个Service中显示一个Dialog,如下代码:

分析:传入的Context是一个Service,类似上面传入ApplicationContext一样的后果,一样的原因,抛出如下异常:

至此通过我们平时使用最多的Dialog也验证了Dialog成功显示的必要条件,同时也让大家避免了再次使用Dialog不当出现异常的情况,或者出现类似异常后知道真实的背后原因是什么的问题。

可以看见,Dialog的实质无非也是使用WindowManager的addView、updateViewLayout、removeView进行一些操作展示。

4 Android应用PopWindow窗口添加显示机制源码

PopWindow实质就是弹出式菜单,它与Dialag不同的地方是不会使依赖的Activity组件失去焦点(PopupWindow弹出后可 以继续与依赖的Activity进行交互),Dialog却不能这样。同时PopupWindow与Dialog另一个不同点是PopupWindow是 一个阻塞的对话框,如果你直接在Activity的onCreate等方法中显示它则会报错,所以PopupWindow必须在某个事件中显示地或者是开 启一个新线程去调用。

说这么多还是直接看代码吧。

4-1 PopWindow窗口源码分析

依据PopWindow的使用,我们选择最常用的方式来分析,如下先看其中常用的一种构造函数:

可以看见,构造函数只是初始化了一些变量,看完构造函数继续看下PopWindow的展示函数,如下:

可以看见,当我们想将PopWindow展示在anchor的下方向(Z轴是在anchor的上面)旁边时经理了上面三步,我们一步一步来分析,先看第一步,源码如下:

接着回到showAsDropDown方法看看第二步,如下源码:

可以看见preparePopup方法的作用就是判断设置View,如果有背景则会在传入的contentView外面包一层 PopupViewContainer(实质是一个重写了事件处理的FrameLayout)之后作为mPopupView,如果没有背景则直接用 contentView作为mPopupView。我们再来看下这里的PopupViewContainer类,如下源码:

可以看见,这个PopupViewContainer是一个PopWindow的内部私有类,它继承了FrameLayout,在其中重写了Key 和Touch事件的分发处理逻辑。同时查阅PopupView可以发现,PopupView类自身没有重写Key和Touch事件的处理,所以如果没有将 传入的View对象放入封装的ViewGroup中,则点击Back键或者PopWindow以外的区域PopWindow是不会消失的(其实PopWindow中没有向Activity及Dialog一样new新的Window,所以不会有新的callback设置,也就没法处理事件消费了)。

接着继续回到showAsDropDown方法看看第三步,如下源码:

可以看见,这里使用了Activity的WindowManager将我们的PopWindow进行了显示。

到此可以发现,PopWindow的实质无非也是使用WindowManager的addView、updateViewLayout、 removeView进行一些操作展示。与Dialog不同的地方是没有新new Window而已(也就没法设置callback,无法消费事件,也就是前面说的PopupWindow弹出后可以继续与依赖的Activity进行交互 的原因)。

到此PopWindw的窗口加载显示机制就分析完毕了,接下来进行总结与应用开发技巧提示。

4-2 PopWindow窗口源码分析总结及应用开发技巧提示

通过上面分析可以发现总结如下图:



可以看见,PopWindow完全使用了Activity的Window与WindowManager,相对来说比较简单容易记理解。

再来看一个开发技巧:

如果设置了PopupWindow的background,则点击Back键或者点击PopupWindow以外的区域时PopupWindow就 会dismiss;如果不设置PopupWindow的background,则点击Back键或者点击PopupWindow以外的区域 PopupWindow不会消失。

5 Android应用Toast窗口添加显示机制源码

5-1 基础知识准备

在开始分析这几个窗口之前需要脑补一点东东,我们从应用层开发来直观脑补,这样下面分析源码时就不蛋疼了。如下是一个我们写的两个应用实现 Service跨进程调用服务ADIL的例子,客户端调运远程Service的start与stop方法控制远程Service的操作。

Android系统中的应用程序都运行在各自的进程中,进程之间是无法直接交换数据的,但是Android为开发者提供了AIDL跨进程调用Service的功能。其实AIDL就相当于双方约定的一个规则而已。

先看下在Android Studio中AIDL开发的工程目录结构,如下:



由于AIDL文件中不能出现访问修饰符(如public),同时AIDL文件在两个项目中要完全一致而且只支持基本类型,所以我们定义的AIDL文件如下:

ITestService.aidl

再来看下依据aidl文件自动生成的ITestService.java文件吧,如下:

这就是自动生成的java文件,接下来我们看看服务端的Service源码,如下:

现在服务端App的代码已经OK,我们来看下客户端的代码。客户端首先也要像上面的工程结构一样,把AIDL文件放好,接着在客户端使用远程服务端的Service代码如下:

到此你对应用层通过AIDL使用远程Service的形式已经很熟悉了,至于实质的通信使用Binder的机制我们后面会写文章一步一步往下分析。到此的准备知识已经足够用来理解下面我们的源码分析了。

5-2 Toast窗口源码分析

我们常用的Toast窗口其实和前面分析的Activity、Dialog、PopWindow都是不同的,因为它和输入法、墙纸类似,都是系统窗口。

我们还是按照最常用的方式来分析源码吧。

我们先看下Toast的静态makeText方法吧,如下:

可以看见,这个方法构造了一个Toast,然后把要显示的文本放到这个View的TextView中,然后初始化相关属性后返回这个新的Toast对象。

当我们有了这个Toast对象之后,

可以通过show方法来显示出来,如下看下show方法源码:

我们看看show方法中调运的getService方法,如下:

通过上面我们的基础脑补实例你也能看懂这个getService方法了吧。那接着我们来看mTN吧,好像mTN在Toast的构造函数里见过一眼,我们来看看,如下:

可以看见mTN确实是在构造函数中实例化的,那我们就来看看这个TN类,如下:

看见没有,TN是Toast内部的一个私有静态类,继承自ITransientNotification.Stub。你这时指定好奇 ITransientNotification.Stub是个啥玩意,对吧?其实你在上面的脑补实例中见过它的,他出现在服务端实现的Service中, 就是一个Binder对象,也就是对一个aidl文件的实现而已,我们看下这个ITransientNotification.aidl文件,如下:

看见没有,和我们上面的例子很类似吧。

再回到上面分析的show()方法中可以看到,我们的Toast是传给远程的NotificationManagerService管理的,为了 NotificationManagerService回到我们的应用程序(回调),我们需要告诉NotificationManagerService 我们当前程序的Binder引用是什么(也就是TN)。是不是觉得和上面例子有些不同,这里感觉Toast又充当客户端,又充当服务端的样子,实质就是一 个回调过程而已。

继续来看Toast中的show方法的service.enqueueToast(pkg, tn, mDuration);语句,service实质是远程的NotificationManagerService,所以enqueueToast方法就是 NotificationManagerService类的,如下:

继续看下该方法中调运的showNextToastLocked方法,如下:

继续先看下该方法中调运的scheduleTimeoutLocked方法,如下:

可以看见这里先回调了Toast的TN的show,下面timeout可能就是hide了。接着还在该类的mHandler处理了这条消息,然后调运了如下处理方法:

我们继续看cancelToastLocked方法,如下:

到此可以发现,Toast的远程管理NotificationManagerService类的处理实质是通过Handler发送延时消息显示取消 Toast的,而且在远程NotificationManagerService类中又远程回调了Toast的TN类实现的show与hide方法。

现在我们就回到Toast的TN类再看看这个show与hide方法,如下:

可以看见,这里实现aidl接口的方法实质是通过handler的post来执行的一个方法,而这个Handler仅仅只是new了一下,也就是 说,如果我们写APP时使用Toast在子线程中则需要自行准备Looper对象,只有主线程Activity创建时帮忙准备了Looper(关于 Handler与Looper如果整不明白请阅读《Android异步消息处理机制详解及源码分析》)。

那我们重点关注一下handleShow与handleHide方法,如下:

到此Toast的窗口添加原理就分析完毕了,接下来我们进行总结。

5-3 Toast窗口源码分析总结及应用开发技巧

经过上面的分析我们总结如下:



通过上面分析及上图直观描述可以发现,之所以Toast的显示交由远程的NotificationManagerService管理是因为 Toast是每个应用程序都会弹出的,而且位置和UI风格都差不多,所以如果我们不统一管理就会出现覆盖叠加现象,同时导致不好控制,所以Google把 Toast设计成为了系统级的窗口类型,由NotificationManagerService统一队列管理。

在我们开发应用程序时使用Toast注意事项:

通过分析TN类的handler可以发现,如果想在非UI线程使用Toast需要自行声明Looper,否则运行会抛出Looper相关的异常;UI线程不需要,因为系统已经帮忙声明。

在使用Toast时context参数尽量使用getApplicationContext(),可以有效的防止静态引用导致的内存泄漏。

有时候我们会发现Toast弹出过多就会延迟显示,因为上面源码分析可以看见Toast.makeText是一个静态工厂方法,每次调用这 个方法都会产生一个新的Toast对象,当我们在这个新new的对象上调用show方法就会使这个对象加入到 NotificationManagerService管理的mToastQueue消息显示队列里排队等候显示;所以如果我们不每次都产生一个新的 Toast对象(使用单例来处理)就不需要排队,也就能及时更新了。

6 Android应用Activity、Dialog、PopWindow、Toast窗口显示机制总结

可以看见上面无论Acitivty、Dialog、PopWindow、Toast的实质其实都是如下接口提供的方法操作:

整个应用各种窗口的显示都离不开这三个方法而已,只是token及type与Window是否共用的问题。

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0615/3044.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: