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之前我们先看一张图:
接下来看一点代码,如下:
1 2 3 4 5 6 7 8 9 |
|
可以看见,ViewManager接口定义了一组规则,也就是add、update、remove的操作View接口。也就是说ViewManager是用来添加和移除activity中View的接口。继续往下看:
1 2 3 4 5 6 7 8 9 10 |
|
看见没有,WindowManager继承自ViewManager,然后自己还是一个接口,同时又定义了一个静态内部类LayoutParams(这个 类比较重要,后面会分析。提前透漏下,如果你在APP做过类似360助手屏幕的那个悬浮窗或者做过那种类似IOS的小白圆点,点击展开菜单功能,你或多或 少就能猜到这个类的重要性。)。WindowManager用来在应用与Window之间的接口、窗口顺序、消息等的管理。继续看下 ViewManager的另一个实现子类ViewGroup,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
这下理解上面那幅图了吧,所以说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方法源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
看见没有,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类的该方法源码),如下:
1 2 3 4 5 6 7 8 9 |
|
可以看见,Window的setWindowManager方法中通过WindowManagerImpl实例的createLocalWindowManager方法获取了WindowManager实例,如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
看见没有?这样就把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类的静态方法块,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
看见没有,我们都知道Java的静态代码块是类加载是执行一次的,也就相当于一个全局的,这样就相当于每个Application只有一个WindowManagerImpl(display)实例。
还记不记得《Android应用setContentView与LayoutInflater加载解析机制源码分析》一文2-6小节中说的,setContentView的实质显示是触发了Activity的resume状态,也就是触发了makeVisible方法,那我们再来看下这个方法,如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
特别注意,看见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方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
这里当前传入的view是mDecor,LayoutParams呢?可以看见是getWindow().getAttributes(),那我们进去看看Window类的这个属性,如下:
1 2 |
|
原来是WindowManager的静态内部类LayoutParams的默认构造函数:
1 2 3 4 5 |
|
看见没有,Activity窗体的WindowManager.LayoutParams类型是TYPE_APPLICATION的。
继续回到WindowManagerImpl的addView方法,分析可以看见WindowManagerImpl中有一个单例模式的 WindowManagerGlobal成员mGlobal,addView最终调运了WindowManagerGlobal的addView,源码如 下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
可以看见,在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的),如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
|
看见没有,从上面类可以看出,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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
这是运行结果:
Part2:App开发中弹出软键盘时下面的输入框被软件盘挡住问题的解决办法:
在Activity中的onCreate中setContentView之前写如下代码:
1 2 |
|
Part3:创建悬浮窗口(仿IPhone的小圆点或者魅族的小白点或者360手机卫士的小浮标),退出当前Activity依旧可见的一种实现方法:
省略了Activity的start与stop Service的按钮代码,直接给出了核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
|
如下是运行过程模拟,特别留意屏幕右下角的变化:
怎么样,通过最后这个例子你是不是就能体会到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的构造函数开始分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
可以看到,Dialog构造函数首先把外部传入的参数context对象赋值给了当前类的成员(我们的Dialog一般都是在Activity中启动的, 所以这个context一般是个Activity),然后调用 context.getSystemService(Context.WINDOW_SERVICE)获取WindowManager,这个 WindowManager是哪来的呢?先按照上面说的context一般是个Activity来看待,可以发现这句实质就是Activity的 getSystemService方法,我们看下源码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
看见没有,Dialog中的WindowManager成员实质和Activity里面是一样的,也就是共用了一个WindowManager。
回到Dialog的构造函数继续分析,在得到了WindowManager之后,程序又新建了一个Window对象(类型是PhoneWindow 类型,和Activity的Window新建过程类似);接着通过w.setCallback(this)设置Dialog为当前window的回调接 口,这样Dialog就能够接收事件处理了;接着把从Activity拿到的WindowManager对象关联到新创建的Window中。
至此Dialog的创建过程Window处理已经完毕,很简单,所以接下来我们继续看看Dialog的show与cancel方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
可以看见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,如下代码:
1 2 3 4 5 6 7 8 9 10 11 |
|
分析:使用了Activity为context,也即和Activity共用token,符合上面的分析,所以不会报错,正常执行。
实现在一个Activity中显示一个Dialog,如下代码:
1 2 3 4 5 6 7 8 9 10 11 |
|
分析:传入的是Application的Context,导致TYPE_APPLICATION类型Dialog的token为null,所以抛出如下异常,无法显示对话框。
1 2 3 4 5 |
|
实现在一个Service中显示一个Dialog,如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
分析:传入的Context是一个Service,类似上面传入ApplicationContext一样的后果,一样的原因,抛出如下异常:
1 2 3 4 5 |
|
至此通过我们平时使用最多的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的使用,我们选择最常用的方式来分析,如下先看其中常用的一种构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
可以看见,构造函数只是初始化了一些变量,看完构造函数继续看下PopWindow的展示函数,如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
可以看见,当我们想将PopWindow展示在anchor的下方向(Z轴是在anchor的上面)旁边时经理了上面三步,我们一步一步来分析,先看第一步,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
接着回到showAsDropDown方法看看第二步,如下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
可以看见preparePopup方法的作用就是判断设置View,如果有背景则会在传入的contentView外面包一层 PopupViewContainer(实质是一个重写了事件处理的FrameLayout)之后作为mPopupView,如果没有背景则直接用 contentView作为mPopupView。我们再来看下这里的PopupViewContainer类,如下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
可以看见,这个PopupViewContainer是一个PopWindow的内部私有类,它继承了FrameLayout,在其中重写了Key 和Touch事件的分发处理逻辑。同时查阅PopupView可以发现,PopupView类自身没有重写Key和Touch事件的处理,所以如果没有将 传入的View对象放入封装的ViewGroup中,则点击Back键或者PopWindow以外的区域PopWindow是不会消失的(其实PopWindow中没有向Activity及Dialog一样new新的Window,所以不会有新的callback设置,也就没法处理事件消费了)。
接着继续回到showAsDropDown方法看看第三步,如下源码:
1 2 3 4 5 6 7 8 |
|
可以看见,这里使用了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
1 2 3 4 5 6 |
|
再来看下依据aidl文件自动生成的ITestService.java文件吧,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
这就是自动生成的java文件,接下来我们看看服务端的Service源码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
现在服务端App的代码已经OK,我们来看下客户端的代码。客户端首先也要像上面的工程结构一样,把AIDL文件放好,接着在客户端使用远程服务端的Service代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
|
到此你对应用层通过AIDL使用远程Service的形式已经很熟悉了,至于实质的通信使用Binder的机制我们后面会写文章一步一步往下分析。到此的准备知识已经足够用来理解下面我们的源码分析了。
5-2 Toast窗口源码分析
我们常用的Toast窗口其实和前面分析的Activity、Dialog、PopWindow都是不同的,因为它和输入法、墙纸类似,都是系统窗口。
我们还是按照最常用的方式来分析源码吧。
我们先看下Toast的静态makeText方法吧,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
可以看见,这个方法构造了一个Toast,然后把要显示的文本放到这个View的TextView中,然后初始化相关属性后返回这个新的Toast对象。
当我们有了这个Toast对象之后,
可以通过show方法来显示出来,如下看下show方法源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
我们看看show方法中调运的getService方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
通过上面我们的基础脑补实例你也能看懂这个getService方法了吧。那接着我们来看mTN吧,好像mTN在Toast的构造函数里见过一眼,我们来看看,如下:
1 2 3 4 5 6 7 8 |
|
可以看见mTN确实是在构造函数中实例化的,那我们就来看看这个TN类,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
看见没有,TN是Toast内部的一个私有静态类,继承自ITransientNotification.Stub。你这时指定好奇 ITransientNotification.Stub是个啥玩意,对吧?其实你在上面的脑补实例中见过它的,他出现在服务端实现的Service中, 就是一个Binder对象,也就是对一个aidl文件的实现而已,我们看下这个ITransientNotification.aidl文件,如下:
1 2 3 4 5 6 7 |
|
看见没有,和我们上面的例子很类似吧。
再回到上面分析的show()方法中可以看到,我们的Toast是传给远程的NotificationManagerService管理的,为了 NotificationManagerService回到我们的应用程序(回调),我们需要告诉NotificationManagerService 我们当前程序的Binder引用是什么(也就是TN)。是不是觉得和上面例子有些不同,这里感觉Toast又充当客户端,又充当服务端的样子,实质就是一 个回调过程而已。
继续来看Toast中的show方法的service.enqueueToast(pkg, tn, mDuration);语句,service实质是远程的NotificationManagerService,所以enqueueToast方法就是 NotificationManagerService类的,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
继续看下该方法中调运的showNextToastLocked方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
继续先看下该方法中调运的scheduleTimeoutLocked方法,如下:
1 2 3 4 5 6 7 8 9 10 |
|
可以看见这里先回调了Toast的TN的show,下面timeout可能就是hide了。接着还在该类的mHandler处理了这条消息,然后调运了如下处理方法:
1 2 3 4 5 6 7 8 9 10 |
|
我们继续看cancelToastLocked方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
到此可以发现,Toast的远程管理NotificationManagerService类的处理实质是通过Handler发送延时消息显示取消 Toast的,而且在远程NotificationManagerService类中又远程回调了Toast的TN类实现的show与hide方法。
现在我们就回到Toast的TN类再看看这个show与hide方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
可以看见,这里实现aidl接口的方法实质是通过handler的post来执行的一个方法,而这个Handler仅仅只是new了一下,也就是 说,如果我们写APP时使用Toast在子线程中则需要自行准备Looper对象,只有主线程Activity创建时帮忙准备了Looper(关于 Handler与Looper如果整不明白请阅读《Android异步消息处理机制详解及源码分析》)。
那我们重点关注一下handleShow与handleHide方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
到此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的实质其实都是如下接口提供的方法操作:
1 2 3 4 5 6 |
|
整个应用各种窗口的显示都离不开这三个方法而已,只是token及type与Window是否共用的问题。
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0615/3044.html