Toast概述
Toast的作用
不需要和用户交互的提示框。 更多参见官网:https://developer.android.com/guide/topics/ui/notifiers/toasts.html
Toast的简单使用
自定义Toast
布局文件中根元素为LinearLayout,垂直放入一个ImageView和一个TextView。代码就不贴了。
高级自定义Toast
产品狗的需求:点击一个Button,网络请求失败的情况下使用Toast的方式提醒用户。 程序猿:ok~大笔一挥。
测试:你这程序写的有问题。每次点击就弹出了气泡,连续点击20次,居然花了一分多钟才显示完。改! 程序猿:系统自带的就这样。爱要不要。 测试:那我用单元测试模拟点击50次之后,它就不显示了,这个怎么说。 程序猿:… 这个时候,高级自定义Toast就要出场了~ activity_main.xml—->上下两个按钮,略。 MainActivity.Java
SingleToast.java
那么有的同学会问了:你这样不就是加了个单例吗,好像也没有什么区别。区别大了。仅仅一个单例,既实现了产品狗的需求,又不会有单元测试快速点击50次的之后不显示的问题。为什么?Read The Fucking Source Code。
Toast源码解析
这里以Toast.makeText().show为例,一步步追寻这个过程中源码所做的工作。自定义Toast相当于自己做了makeText()方法的工作,道理是一样一样的,这里就不再分别讲述了~
源码位置:frameworks/base/core/java/Android/widght/Toast.java Toast#makeText()
这里填充的布局transient_notification.xml位于frameworks/base/core/res/res/layout/transient_notification.xml。加分项,对于XML布局文件解析不太了解的同学可以看下这篇博客。
可以发现,里面只有一个TextView,平日设置的文本内容就是在这里展示。接下来只有一个show()方法,似乎我们的源码解析到这里就快结束了。不,这只是个开始
这里有三个问题。
- 通过getService()怎么就获得一个INotificationManager对象?
- TN类是个什么鬼?
- 方法最后只有一个service.enqueueToast(),显示和隐藏在哪里?
Toast的精华就在这三个问题里,接下来的内容全部围绕上述三个问题,尤其是第三个。已经全部了解的同学可以去看别的博客了~
1. 通过getService()怎么就获得一个INotificationManager对象?
对Binder机制了解的同学看见XXX.Stub.asInterface肯定会很熟悉,这不就是AIDL中获取client嘛!确实是这样。
tips: 本着追本溯源的精神,先看下ServiceManager.getService("notification")。在上上上上篇博客SystemServer启动流程源码解析中startOtherServices()涉及到NotificationManagerService的启动,代码如下,这里不再赘述。
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 24px 0px; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 14px; padding: 16px; overflow: auto; line-height: 1.45; background-color: rgb(247, 247, 247); border-radius: 3px; word-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; tab-size: 2; hyphens: none; color: rgb(51, 51, 51); letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px;">mSystemServiceManager.startService(NotificationManagerService.class);</pre>
Toast中AIDL对应文件的位置。
源码位置:frameworks/base/core/java/android/app/INotificationManager.aidl
Server端:NotificationManagerService.java 源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
篇幅有限,这里不可能将AIDL文件完整的叙述一遍,不了解的同学可以理解为:经过进程间通信(AIDL方式),最后调用NotificationManagerService#enqueueToast()。具体可以看下这篇博客。
2. TN类是个什么鬼?
在Toast#makeText()中第一行就获取了一个Toast对象
源码位置:frameworks/base/core/java/android/widght/Toast$TN.java
源码中的进程间通信实在太多了,我不想说这方面的内容啊啊啊~。有时间专门再写一片博客。这里提前剧透下TN类除了设置参数的作用之外,更大的作用是Toast显示与隐藏的回调。TN类在这里作为Server端。NotificationManagerService$NotificationListeners类作为client端。这个暂且按下不提,下文会详细讲述。
3. show()方法最后只有一个service.enqueueToast(),显示和隐藏在哪里?
源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
在Toast#show()最终会进入到这个方法。首先通过indexOfToastLocked()方法获取应用程序对应的ToastRecord在mToastQueue中的位置,Toast消失后返回-1,否则返回对应的位置。mToastQueue明明是个ArratList对象,却命名Queue,猜测后面会遵循“后进先出”的原则移除对应的ToastRecord对象~。这里先以返回index=-1查看,也就是进入到else分支。如果不是系统程序,也就是应用程序。那么同一个应用程序瞬时在mToastQueue中存在的消息不能超过50条(Toast对象不能超过50个)。否则直接return。这也是上文中为什么快速点击50次之后无法继续显示的原因。既然瞬时Toast不能超过50个,那么运用单例模式使用同一个Toast对象不就可以了嘛?答案是:可行。消息用完了就移除,瞬时存在50个以上的Toast对象相信在正常的程序中也用不上。而且注释中也说这样做是为了放置DOS攻击和防止泄露。其实从这里也可以看出:为了防止内存泄露,创建Toast最好使用getApplicationContext,不建议使用Activity、Service等。
回归主题。接下来创建了一个ToastRecord对象并添加进mToastQueue。接下来调用showNextToastLocked()方法显示一个Toast。
源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java NotificationManagerService#showNextToastLocked()
这里首先调用record.callback.show(),这里的record.callback其实就是TN类。接下来调用scheduleTimeoutLocked()方法,我们知道Toast显示一段时间后会自己消失,所以这个方法肯定是定时让Toast消失。跟进。
果然如此。重点在于使用mHandler.sendMessageDelayed(m, delay)延迟发送消息。这里的delay只有两种值,要么等于LENGTH_LONG,其余统统的等于SHORT_DELAY,setDuration为其他值用正常手段是没有用的(可以反射,不在重点范围内)。 handler收到MESSAGE_TIMEOUT消息后会调用handleTimeout((ToastRecord)msg.obj)。跟进。
啥也不说了,跟进吧~
延迟调用record.callback.hide()隐藏Toast,前文也提到过:record.callback就是TN对象。到这,第三个问题已经解决一半了,至少我们已经直到Toast的显示和隐藏在哪里被调用了,至于怎么显示怎么隐藏的,客观您接着往下看。
源码位置:frameworks/base/core/java/android/widght/ToastTN.javaToastTN#show()
注意下这里直接使用new Handler获取Handler对象,这也是为什么在子线程中不用Looper弹出Toast会出错的原因。跟进handleShow()。
原来addView到WindowManager。这样就完成了Toast的显示。至于隐藏就更简单了。
直接remove掉。
喜欢的话请帮忙转发一下能让更多有需要的人看到吧,有些技术上的问题大家可以多探讨一下。


网友评论