Android源码阅读系列(二)之Toast显示原理

一、引言

在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被隐藏了。