前面我们已经讲述了Activity的Window创建过程、Dialog的Window创建过程, 本文将继续探索Window相关的知识:Toast的创建过程 及 其 View界面的展示。
#####代码示例
Toast的一般使用非常简单, 一行代码就可以搞定:
Toast.makeText(this, "Toast测试", Toast.LENGTH_SHORT).show();
通过makeText创建一个Toast, 然后调用show方法,去真正的显示出来这个Toast, 这一点和前文的Dialog很相似。
我们去看看这个makeText源码:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
Toast result = new Toast(context);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
代码也很简单, 就是 new 一个Toast, 然后填充一个默认视图,最后把这个新建的Toast对象返回。从这里也可以看出, 其实我们可以自己new一个Toast,然后填充我们的自定义布局, 就可以定义我们自己想要的Toast了。这里Toast为我们提供了setView(View v)方法实现我们的自定义布局。
#####Toast的Window创建过程
Toast和前文讲到的Dialog有所不同,Toast的工作过程要更复杂一些。Toast也是基于Window的, 这是毋庸置疑的,但是由于Toast有具有定时取消的特点,即我们通过设置Toast.LENGTH_SHORT或者Toast.LENGTH~LONG,Toast具有固定显示实现, 分别是2.5s和3.5s,显示完后就要消失, 所以这里系统采用了handler的延时机制去完成。
在Toast内部, 有两个不同的IPC过程, 第一个是Toast访问NotificationManagerService, 第二个是NotificationManagerService回调给Toast里的TN接口(这里的TN就是一个类,它的类名就是TN), 这里说是一个回调其实是为了方便我们理解, 它的本质其实是NotificationManagerService通过IPC来访问我们的Toast,同时把数据传过来。关于Binder通信机制可以自行搜索一下, 或者参考我的博客**Android的IPC机制–实现AIDL的最简单例子(上)(下)**, AIDL本质就是一个Binder通信。
除了这两个IPC过程, 最后Window的添加也是一个IPC过程, 所以Toast的IPC过程总共有三个,其中创建过程两个, 添加过程一个。
Toast是一个系统级Window(这个后面会说明), show和cancel两个方法用于显示和隐藏Toast。
刚刚看过了makeText()方法,里面很简单,接着我们看看show方法:
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
注意这一行代码:
INotificationManager service = getService();
这里就是去获取一个NotificationManagerService的本地代理 service对象, 然后通过service.enqueueToast(pkg, tn, mDuration) 向NotificationManagerService发送了消息:我要创建一个Toast啦。 这里就是我们刚刚所提到的第一个IPC过程,其中enqueueToast的三个参数别是包名,TN对象以及 toast的时长。 我们去看一下这个getService()方法:
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
如果了解AIDL的通信, 是不是觉得这里很眼熟, 这其实就是一个AIDL啊,还不了解AIDL的同学,推荐看完本文后,再移步看一下我的另两篇博客**Android的IPC机制–实现AIDL的最简单例子(上)(下)**。跨进程通信不是本篇的重点, 这里只是提一下。
回到刚刚的位置, 我们通过service.enqueueToast()方法向NotificationManagerService发送消息后, 将会执行NotificationManagerService的enqueueToas方法, 继续进去查看源码:
private final IBinder mService = new INotificationManager.Stub() {
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
...
synchronized (mToastQueue) {
...
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
// 1. 如果这个toast已经存在于queue了,则只是更新它,但是并不会把它移动到队尾
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
// 2. 判断如果是同一个包, 则最多只能存在50个toast,
//否则不再允许添加进队列
if (r.pkg.equals(pkg)) {
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
return;
}
}
}
}
record = new ToastRecord(callingPid, pkg, callback, duration);
//把传过来的toast加入进队列, 等待显示
mToastQueue.add(record);
...
}
// 3. 如果是第一个toast, 则直接显示
if (index == 0) {
showNextToastLocked();
}
...
上面这一段源码,主要有三个重要代码:
-
- 判断这个Toast是否已存在,如果这个toast已经存在于queue了,则只是更新它,但是并不会把它移动到队列的队尾, 这里的mToastQueue其实是一个ArrayList;
-
- 判断如果是同一个包, 则最多只能存在50个toast,否则不再允许添加进队列, 这一点非常重要,试想一下, 如果我们通过循环去大量弹出toast, 那么这个toast队列里面的toast就会无穷无尽, 这个时候去打开其他APP, 它们都没机会弹出toast了。正常情况下, 一个应用的toast也打不到50个,完全够用了;
-
- 如果是第一个toast, 则直接调用showNextToastLocked方法弹出toast。
要注意这个enqueueToast的三个参数, 我们传递过来的分别是包名、 TN对象、 显示时间, 这里把TN对象赋值给了callback, TN实际上实现了ITransientNotification接口的。
#####Toast的Window显示过程
接着,我们去看看showNextToastLocked是如何显示toast的:
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
//关键代码 1:回调show方法
record.callback.show();
// 关键代码2:执行toast的超时后的移除处理
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
//这里处理由于某些意外导致 跨进程通信失败,即这个调用Toast的显示失败
//这个时候就从队列把这个Toast移除掉, 执行下一个Toast的显示
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
这里的逻辑一样很简单, 主要看try代码块就行了, 二tyr代码块就两行代码,都是关键代码:
- 关键代码1: 回调是record.callback.show(): 这个callback对象其实就是我们的TN对象, 所以这里就回调给我们的toast中的TN了, 这里就是第二次跨进程通信;
- 关键代码2 调用scheduleTimeoutLocked:从这个方法名上也可看出一二, 这是用来处理超时的方法, 主要是当显示超过我们的设定的Toast的显示时间后(即SHORT或者LONG), 就移除掉这个toast,然后继续调用下一个Toast。具体细节感兴趣的可以去跟一下
我们继续按照主路径走, 去看我们的关键代码, 这里回调给了TN的show方法, TN是Toast的一个内部类:
private static class TN extends ITransientNotification.Stub {
final Runnable mShow = new Runnable() {
@Override
public void run() {
handleShow();
}
};
...
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
...
}
我们看到 TN extends ITransientNotification.Stub, 了解AIDL的同学一看就知道这是一个跨进程通信的类, 它继承了ITransientNotification.Stub, 也就实现了ITransientNotification接口。 我们继续跟进show方法, 里面调用了 mHandler.post(mShow), 具体的执行在mShow里面,接着看, mShow里面又调用了handleShow(), 看一下这个方法:
public void handleShow() {
if (mView != mNextView) {
// 移除掉前一个Toast的view
handleHide();
//把我们的Toast的View传进来
mView = mNextView;
...
//关键代码, 获取WindowManagerb本地代理对象
mWM = (WindowManager)context.getSystemService
...
//关键代码, 向WindowManager添加Window, 把我们的view传过去
mWM.addView(mView, mParams);
}
}
这个方法逻辑也比较清楚, 就是把上一个toast的view已移除掉,然后把要显示的Toast添加进入WindowManger, 这同样是一个IPC过程, Window的添加过程可以参考**从Window的添加过程理解Window和WindowManager**, 文中配了一张图片, 很清晰的展示了Window的添加过程。 这就是我们前面提到的到的Toast的 第三次IPC过程。
然后, Toast就真正的显示在我们的界面上啦~~
到这里, 整个Toast的view添加, Window添加,以及Toast的超时后自动消失的 整个流程就讲完了~~
#####Toast的一些不为人知的细节
######1、为什么我们通常只能在UI主线程使用Toast?
上面一段代码中, 我们看到,当NotificationManagerService回调给TN, 通知它可以显示Toast的时候, 回调给了TN的show方法, 我们再次看一些这个show方法:
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
注意到没有, 这里使用mHandler, 去提交的一个任务, 看一下这个mHandler的定义:
final Handler mHandler = new Handler();
就是一个普通的Handler, 而我们知道, 只有在主线程, 才能直接这么使用handler, 否则都会报错,因为主线程在初始化时, 就已经调用了Looper.prepare()方法。如果需要在子线程使用Handler的话, 必须调用Looper.prepare()方法。所以, 一般情况下, 我们在主线程使用Toast, 但是如果想要在子线程使用Toast, 也是可以的, 就在子线程的run方法最后一行, 调用Looper.prepare()即可。为什么是在最后一行调用呢, 因为这个Looper.prepare()内部是一个死循环, 在这个方法后面的代码就无法执行了。
######2、为什么说Toast是一个系统级的Window?
Window分为系统级Window, 应用级Window, 子Window, 其中子Window必须附属在其他两类Window上,这个在**从Window的添加过程理解Window和WindowManager** 一文中有所介绍, 那为什么说Toast是一个系统级Window呢, 我们只需要去看看它的层级即可。
我们再去看看Toast的Window的添加过程:
//关键代码, 向WindowManager添加Window, 把我们的view传过去
mWM.addView(mView, mParams);
层级是在WindowManager.LayoutParams.type中设置的, 我们从这里的mParams去看看, 找一下这个mParams在哪里赋值了, 找了一圈, 除了刚刚的handleShow方法, mparams在TN的构造方法里也有一些初始化:
TN() {
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
//关键代码,给Window层级赋值
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
}
我们看到, type的值为WindowManager.LayoutParams.TYPE_TOAST, 去看看这个值:
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
可以看到它的值是FIRST_SYSTEM_WINDOW+5, 这个FIRST_SYSTEM_WINDOW表示系统Window的第一个层级, 比它大的都是系统Window,FIRST_SYSTEM_WINDOW的值为2000。系统Window的层级为 2000~2999.
到这里,Toast的Window的创建到消失的整个流程就讲完了。
喜欢的朋友麻烦点一个赞吧~~