Android 高级自定义Toast及源码解析

时间:2022-04-26
本文章向大家介绍Android 高级自定义Toast及源码解析,主要内容包括Toast概述、Toast的简单使用、自定义Toast、高级自定义Toast、Toast源码解析、2. TN类是个什么鬼?、3. show()方法最后只有一个service.enqueueToast(),显示和隐藏在哪里?、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

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()方法,似乎我们的源码解析到这里就快结束了。不,这只是个开始

这里有三个问题。

  1. 通过getService()怎么就获得一个INotificationManager对象?
  2. TN类是个什么鬼?
  3. 方法最后只有一个service.enqueueToast(),显示和隐藏在哪里?

Toast的精华就在这三个问题里,接下来的内容全部围绕上述三个问题,尤其是第三个。已经全部了解的同学可以去看别的博客了~

1. 通过getService()怎么就获得一个INotificationManager对象?

对Binder机制了解的同学看见XXX.Stub.asInterface肯定会很熟悉,这不就是AIDL中获取client嘛!确实是这样。

tips: 本着追本溯源的精神,先看下ServiceManager.getService("notification")。在上上上上篇博客SystemServer启动流程源码解析中startOtherServices()涉及到NotificationManagerService的启动,代码如下,这里不再赘述。

mSystemServiceManager.startService(NotificationManagerService.class);

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掉。