Toast是一个独立的顶级窗口,显示时浮在其他窗口之上,不依赖于任何Activity,即使在任何activity未启动的情况下或者当前位于前台的程序是别的app时,依然可以显示。
各个app都可以随心所欲地在屏幕上弹出Toast,为了避免“百花齐放”,必须有第三者来管理,使其顺序显示。这个第三者就是系统服务INotificationManager。INotificationManager会维护一个Toast显示队列,各个程序抛出的显示Toast的请求会被依次加入该队列,然后逐个取出显示后从队列里去除。这个过程实际上属于进程通信,通过AIDL的方式。
我们知道AIDL进程通信的媒介是Binder,Toast定义了一个内部类TN,继承了ITransientNotification.Stub类,作为通信媒介传给服务端,当轮到某个Toast弹出时,服务端通过调用它TN对象的show和hide方法控制Toast窗口的显示和超出显示时长时后的隐藏。
private static class TN extends ITransientNotification.Stub {
//定义handler
final Handler mHandler = new Handler();
//显示Toast窗口
final Runnable mShow = new Runnable() {
@Override
public void run() {
handleShow();
}
};
//隐藏Toast窗口
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
mNextView = null;
}
};
//供服务端调用的show方法
@Override
public void show() {
mHandler.post(mShow);
}
//供服务端调用的hide方法
@Override
public void hide() {
mHandler.post(mHide);
}
public void handleShow() {
...
if (mView != mNextView) {
...
handleHide();
...
mView = mNextView;
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...
//将Toast的View添加为窗口
mWM.addView(mView, mParams);
...
}
}
public void handleHide() {
...
if (mView != null) {
...
if (mView.getParent() != null) {
mWM.removeView(mView);
}
...
mView = null;
}
}
}
我们可以看到,TN首先定义了一个Handler和两个供Handler发送执行的Runnable对象mShow和mHide。mShow里执行的是显示Toast窗口的代码,mHide里执行隐藏Toast窗口的代码。
TN暴露给服务端调用的show和hide方法直接通过Handler发送执行对应的Runnable对象。之所以采用handler方式是确保UI操作在主线程进行。
另外,TN还封装了Toast的相关信息,如添加到窗口的View,窗口位置信息及其他布局参数等。
private static class TN extends ITransientNotification.Stub {
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
...
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
//添加到窗口的View
View mView;
View mNextView;
int mDuration;
WindowManager mWM;
...
}
TN是何时被传给服务端的呢?当Toast调用show方法时,Tn对象会被传递给INotificationManager,INotificationManager通过TN的show和hide方法控制Toast窗口的显示和隐藏。
public class Toast{
..
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
//获得INotificationManager
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
//将该Toast的TN对象抛给INotificationManager,加入显示队列
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
...
}
Toast的Duration耗尽后,INotificationManager会通过调用TN的hide方法让窗口消失,并将其从显示队列里去除。如果我们用Toast的cancel方法显示取消掉一个Toast会执行什么呢?
public class Toast{
...
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
...
}
可以看到,调用cancel后,首先调用TN的hide方法立即隐藏掉Toast窗口,然后调用INotificationManager的cancelToast方法将其从队列中去除。
另外,我们看到在TN内,用mView来保存添加到窗口的布局,那么mNextView是做什么的?
首先,Toast本身有一个mNextView成员变量,保存Toast的View。当调用Toast的show方法时,会将Toast的mNextView赋值给TN的mNextView。当执行到TN的handleShow方法时,会先判断是否与TN的mView为同一个对象,不是同一对象才会真正添加到窗口。在handleHide方法的末尾,方才将TN的mView置空。handleHide执行完之后,再将TN的mNextView置空。目的就是防止同一Toast实例,当其窗口正在显示时,再次添加其View到窗口。这也是单例Toast迅速多次调用show方法不会重复显示Toast窗口的原因。
注意,Toast的工作机制依赖于INotificationManager,需要系统通知权限,如果app系统通知权限被禁用,你的app的Toast将无法弹出。以淘宝app和优酷app的"再按一次退出程序"的Toast提示为例,关闭通知权限,Toast将不再显示。