0x00 前言
Toast想必大家都很熟悉了。我们经常用Toast对用户进行一些不需要交互的提示。可以说是app开发肯定会用到的。可是原始的Toast还不够友好,或者说,对用户不够友好。在app安全方面也有一些不够完善的点。但是通过对Toast的封装,能够尽可能的避免这些问题,为用户带来更好的体验。
尽管Toast封装的博客已经非常多了,但是这几天查阅了很多,发现要么就是只是简单的封装了调用的方法,并没有解决实质性的问题;要么就是一个源文件了事,却对其中实现的原理只字不提,让人摸不到头脑。所以本文在一篇源码的基础上进行分析,明白其中的原理,希望对读者能够有所帮助。
0X01 Toast探究
Toast是直接依托于Windows来实现的。Toast所创建的视图不属于任何Activity,甚至不属于任何Application,它是直接在整个系统window之上进行创建的。Toast的生命周期也不会依赖于Activity和Application。所以我们经常会看到这样的现象:即使在退出应用后,Toast仍然会持续显示,直到它的duration耗尽。
另外还有一个现象大家也很熟悉,当连续地显示几个Toast的时候,后来的Toast并不会直接覆盖前面的toast,而是会等待之前的Toast显示完毕,耗尽其duration之后才会显示,处在后面的Tosat显示就会被滞后。若是只有两三的Toast还好一点,若是连续七八个,十几个,我想没人搞的清楚到底哪个Toast是对应刚才操作的提示吧。从其先被触发的先显示的逻辑来看,我们猜测其底层实现应该是队列的形式。那么就深入去看看。
首先是makeText()方法:
1 | public static Toast makeText(Context context, CharSequence text, @Duration int duration) { |
可以看出,其实Toast的显示也是用一个布局加载解析而成的。所以Toast的显示应该是支持自定义布局的。但这并不是我们此时想要研究的。继续看show()方法:
1 | public void show() { |
虽然不知道mNextView代表什么,但是看到enqueueToast()这个方法立刻就能让我联想到队列,看来之前的猜测已经对了一半,从代码来看,Toast将其有关的内容通过INotificationManager 这个AIDL接口加入了一个队列中,再想深入暂时就没办法了。
既然可以加入队列,那么就有对应的出队的操作。Toast的有个方法cancle()可以取消Toast的显示:
1 | public void cancel() { |
对比其中的cancelToast()方法与上面的enqueueToast()的参数,发现二者的前两个参数是基本一致的。那么我推测cancelToast()的底层应该就是出队操作。
通过以上分析,Toast的显示是可以取消的,所以是有办法让Toast实时刷新的。接下来看另外一个问题。
一般我们在使用Toast的时候会使用Activity类型的Context去创建一个Toast。这就会造成一些问题。首先在一个子线程中当需要展示一个Toast的时候,就无法提供一个Context。聪明一点的会在当前的Activity中存储一个静态Context并引用自己,来供当前Activity下的子线程引用。然而这样会造成一个更危险的问题,就是内存泄漏。
考虑一个场景,app的当前Activity刚刚创建了一个Toast并把它展示了出来。然而很不幸,这个Activity因为某种原因意外退出了。上面我们提到,Toast的生命周期是不依赖于Activity的,即使创建它的Avtivity被关闭了,Toast仍然会按照它预定的duration显示。这个时候,需要明确一点,Toast中仍然保持着对这个Acticity的Context的引用。这样子导致的结果就是,这个Activity现在无法被正常关闭。因为Toast仍然持有Activity的context,所以GC按照其回收原则是无法销毁这个Activity的。那么这个Activity所占用的内存也就无法被释放。也就是发生了内存泄漏了。
0x02 Toast封装
通过以上几个方面的了解,无论是从用户体验来讲,还是从应用安全的角度,封装Toast都是很有必要的。下面就开始对Toast进行封装。
首先要解决Toast的内存泄漏问题。这个问题是由于Toast持有本应该被销毁的Activity的Context的引用,所以导致Activity无法正常被回收。考虑一下,在一个app中,Toast是由一些预设的条件被用户或场景触发,而对用户进行提示。那么同一个app中,不会同时需要显示一个以上的Toast,即使有连续的Toast需要显示,那么肯定也是存在先后顺序的。因此,我们可以让整个app全局只存在一个Toast对象,当需要显示新的Toast的时候,只要先将之前的Toast取消,再创建新的Toast就可以了。这应该就是传说中的单例设计模式了。
同时默认不使用Activity类型的Context去创建Toast,而是使用Application的Context来创建Toast。根据Android官方的介绍,Application是一个全局对象,它是在这个应用/包被创建的时候进行实例化。如果某个单例需要引用context,就可以在自定义的Application对象的初始化方法中进行引用。
/**
* Base class for those who need to maintain global application state.In
* most situation, static singletons can provide the same functionality in a
* more modular way. If your singleton needs a global context (for example
* to register broadcast receivers), the function to retrieve it can be
* given a {@link android.content.Context} which internally uses
* {@link android.content.Context#getApplicationContext() Context.getApplicationContext()}
* when first constructing the singleton.
*/
而此时我们正好是单例的引用需要,所以大胆地使用Application的Context。但是在非Activity和非Application是不能直接使用getApplicationContext()这个方法获取Application的context的,所以直接在Application中创建一个静态对象存储Application的context,这样任何时候都可以直接调用这个context了。
新建MyApplication :
1 | public class MyApplication extends Application { |
然后修改manifest文件:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:name=".MyApplication"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
前面我们提到,Toast是被加入到一个队列中,然后按照先进先出的顺序被显示。既然被放进了队列中,肯定要有人去取出来才能够显示,尽管Toast的cancle()方法或许有类似的功效,但是很明显,这个方法只能出队,并不能显示Toast。《Android开发艺术探索》中提到,Toast的实现底层是基于Handler消息机制来完成。我们之前发现的enqueueToast()方法就是将Toast加入到对应的Toast队列中。既然是Handler的实现,那么肯定少不了Looper,否则谁去把队列中的内容拿出来进行分发呢(参考Android消息机制浅析)。所以我们需要创建一个可以用来处理 main 线程的 Handler 对象来处理 Looper 取出的 Toast。为什么是 main 线程呢,为了使全局都可以任意使用Toast,显然将显示Toast的操作放在main线程里是最合适的。另外还有一点需要知道,我们在初始化Handler的时候,其构造方法内部已经帮助我们调用了Looper.prepare()和Loop.loop()方法。在没有调用这两个方法的线程里,是无法为当前线程建立MessageQueue并且进行消息轮询的。
Handler的两种构造方法:
mHandler = new Handler(Looper.myLooper()); //可产生用来处理当前线程的Handler对象。
mHandler = new Handler(Looper.getMainLooper()); //可产生用来处理main线程的Handler对象。
其中new Handler(Looper.myLooper())
与 new Handler()
是等价的。
接下来构造一个ToastUtils工具类对Toast进行封装:
1 | public class ToastUtils { |
这样一来,当需要显示Toast的时候,只要调用ToastUtils.toast()方法就可以尽情显示了,并且不用怕显示滞后,和内存泄漏的问题了。同时由于Toast的显示和取消都是在main线程里,所以在子线程里也可以尽情使用Toast进行提示,并且调用方法与主线程里一模一样。
本文有几处文字表达或许并不是非常正确,如果你发现了表述错误或者其他不合理的地方,恳请留言批评指正。万分感谢。