Toast 原理剖析

Toast 对大家来说是一个熟悉的不能再熟悉的东西了,但是究竟什么是Toast,Toast的原理是什么,可能就会有一部分人不知道了,今天我们就来细致的剖析下这个Toast,做到知其然并知其所以然

什么是Toast

借鉴官方的表述:

A toast is a view containing a quick little message for the user. The toast class helps you create and show those.

Toast 就是一个向用户快速展示简短信息的View,Toast这个类就是帮助你做这个事情的

Toast能做什么

同样借鉴官方的描述:

When the view is shown to the user, appears as a floating view over the application. It will never receive focus. The user will probably be in the middle of typing something else. The idea is to be as unobtrusive as possible, while still showing the user the information you want them to see. Two examples are the volume control, and the brief message saying that your settings have been saved.

当向用户展示这个View时,应用程序显示为浮动View,它不会获得焦点,展示的时候用户可能在做其他事情,设计Toast的想法就是尽可能不引起用户的注意,同时应用也向用户展示想让用户看到的信息。系统中两个例子,音量调节和设置信息被保存的提示

关于Toast的使用就参考官方文档吧,Toast Notifications developer guide

Toast原理剖析

接下来就是本文的重头戏了,剖析Toast原理(以下源码都是基于Android5.0分析的,大家注重学习原理,具体细节可自己通过源码去分析)

本文相关代码:
framework/base/core/java/android/widget/Toast.java
framework/base/services/core/java/com/android/server/notification/NotificationManagerService.java
framework/base/core/java/android/app/ITransientNotification.aidl

Toast对象的创建

Toast.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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); //获得Toast默认布局
...

result.mNextView = v; //
result.mDuration = duration; //Toast持续时间

...
}

public Toast(Context context) {
mContext = context;
mTN = new TN(); //**这里是关键**

...
}

Toast的show方法

Toast.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public void show() {
...
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
service.enqueueToast(pkg, tn, mDuration); //调用NotificationManagerService(后边会用NMS代替)的enqueueToast方法
} catch (RemoteException e) {
// Empty
}
}

通过上述的代码,我们发现TN这个类很关键,下面我们先分析下这个类

TN的声明

Toast.java
1
private static class TN extends ITransientNotification.Stub {
ITransientNotification.aidl
1
2
3
4
5
6
package android.app;

oneway interface ITransientNotification {
void show();
void hide();
}

由此可见TN是一个binder对象,那么它是做什么的呢?上边show方法中Toast的mTN变量传递给了NMS,这个调用属于IPC调用,而TN就是IPC回调接口

TN的show方法

通过查看TN的show方法,在show方法中通过Handler发送消息,并调用handleShow方法,这是因为IPC show回调在binder线程中运行

Toast.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void handleShow() {
...

if (mView != mNextView) {
// remove the old view if necessary
handleHide(); //隐藏旧的view
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...//此处略过参数设置
mWM.addView(mView, mParams); //通过WMS添加一个view
...
}
}

这样Toast的Window就显示在了窗口上

TN的hide方法

TN的hide方法和上述的show方法原理是一样的,通过WMS removeView来实现隐藏

NMS的enqueueToast方法

接下来让我们回到上述的enqueueToast方法中,其中mToastQueue是一个保存ToastRecord(用于保存对应Toast的相关数据,如package,TN callback等)的列表

NotificationManagerService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
...
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback); //存列表中查找Toast

if (index >= 0) { //找到进行更新操作
...
//找到进行更新操作
...
} else { //没有在列表中找到
...
//如果不是系统Toast,一个应用未处理的Toast最大上限是50个
...
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record); // 创建ToastRecord并添加到列表中
index = mToastQueue.size() - 1;
keepProcessAliveLocked(callingPid); //保持发Toast请求的应用进程是前端进程
}

if (index == 0) {
showNextToastLocked(); //开始处理Toast
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}

NMS的showNextToastLocked方法

接下来我们分析下showNextToastLocked方法,至于其他方法对我们分析Toast原理的意义不是很大,当然也很简单,这里就略过了

NotificationManagerService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
record.callback.show(); //这个IPC 回调TN的show方法
scheduleTimeoutLocked(record); //发送一个延时消息
return;
} catch (RemoteException e) {
...
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index); //如果IPC调用失败,要将record从列表中移除
}
...
}
}
}

NMS的scheduleTimeoutLocked方法

NotificationManagerService.java
1
2
3
4
5
6
7
8
9
private void scheduleTimeoutLocked(ToastRecord r) {
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;

//由此可以看出Toast的LENGTH_LONG显示3500ms,LENGTH_SHORT显示2000ms

mHandler.sendMessageDelayed(m, delay);
}

下面我们看看MESSAGE_TIMEOUT消息的处理

NotificationManagerService.java
1
2
3
4
5
6
7
8
9
private void handleTimeout(ToastRecord record) {
if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback); //查找ToastRecord
if (index >= 0) {
cancelToastLocked(index); //调用cancel方法,
}
}
}

NMS的cancelToastLocked方法

NotificationManagerService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide(); //IPC回调TN的hide方法
} catch (RemoteException e) {
...
}
mToastQueue.remove(index); //从队列中移除ToastRecord
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
showNextToastLocked(); //继续处理队列中下一个消息
}
}

总结

下面我们用一个简单的图做下总结,其中一些方法是通过发送消息调用的,这些细节没有在图中体现出来,能看出大概原理就好,希望能帮助到大家