一、引言
在Android中Toast使用频率是相当高的,可以用来做用户提示,也可以在测试的时候方便的显示测试结果,关键是它使用方法非常简单,一行代码就搞定了
Toast.makeText(this, msg , Toast.LENGTH_SHORT).show();
但是使用久了就有个疑问,Toast.LENGTH_SHORT和Toast.LENGTH_LONG是用于设置Toast显示时长的,但是这两个时长到底是多少呢?去到Toast源码发现这两个参数是final类型的
public static final int LENGTH_LONG = 1;
public static final int LENGTH_SHORT = 0;
LENGTH_LONG的值是1,LENGTH_SHORT的值是0,理论上当我们设置显示时长为LENGTH_SHORT时,Toast应该会一闪而逝,毕竟是0嘛,但是我们知道,实际使用并不是这样的,设置显示时长为LENGTH_SHORT时,Toast依然会停留2秒左右。看来,这个参数不是那么简单了。无果,只能深入源码分析下Toast的显示原理了。
二、Toast显示原理
首先我们看下显示Toast时所用到的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
}
}
从上面的代码可以看到,显示Toast的过程用到了NotificationManagerService的enqueueToast方法
// Toasts
// ============================================================================
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (DBG) Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback + " duration=" + duration);
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
if (!isSystemToast) {
Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
return;
}
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
if (r.pkg.equals(pkg)) {
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveLocked(callingPid);
}
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated. Call back and tell it to show itself.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
enqueueToast方法有三个参数,第一个参数当前应用的包名,第二个参数是一个callback,第三个参数duration,就是我们创建Toast时设置的显示时长,这个参数又参与了ToastRecord的初始化
private static final class ToastRecord
{
final int pid;
final String pkg;
final ITransientNotification callback;
int duration;
ToastRecord(int pid, String pkg, ITransientNotification callback, int duration)
{
this.pid = pid;
this.pkg = pkg;
this.callback = callback;
this.duration = duration;
}
...
}
继续往下看,会执行showNextToastLocked()方法
private 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();
scheduleTimeoutLocked(record, false);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to show notification " + record.callback
+ " in package " + record.pkg);
// remove it from the list and let the process die
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;
}
}
}
}
里面有一个scheduleTimeoutLocked方法,考虑到我们要研究的是显示时长,这个方法很明显和超时设置有关,进去看一下
private void scheduleTimeoutLocked(ToastRecord r, boolean immediate)
{
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = immediate ? 0 : (r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY);
mHandler.removeCallbacksAndMessages(r);
mHandler.sendMessageDelayed(m, delay);
}
太好了,Toast.LENGTH_LONG这个参数出现了,这里面根据r.duration计算出了delay的值,通过上面的分析,r.duration就是我们创建Toast时设置的时长,所以当我们设置的时长是Toast.LENGTH_LONG时,delay为LONG_DELAY,当设置LENGTH_SHORT时,delay为SHORT_DELAY
private static final int LONG_DELAY = 3500; // 3.5 seconds
private static final int SHORT_DELAY = 2000; // 2 seconds
通过源码我们可以看到这两个时长分别的3.5秒和2秒,从scheduleTimeoutLocked的源码里面也可以大概看出scheduleTimeoutLocked通过Handler发送了一个延时消息,最终WorkerHandler会接收消息并处理
private final class WorkerHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord)msg.obj);
break;
}
}
}
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);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
private void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide();
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to hide notification " + record.callback
+ " in package " + record.pkg);
// don't worry about this, we're about to remove it from
// the list anyway
}
mToastQueue.remove(index);
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();
}
}
可以看到,在指定延时之后,Toast被隐藏了。