Toast技术原理

229 阅读4分钟

前言

原生Toast的能力

  1. 提供有关操作的简单信息反馈
  1. 能够将队列中的View有序弹出,并能控制每个View的显示时长
  1. 反馈的信息可以为文本或者自定义View

能够满足的需求

  1. 轻量级的信息提示方式,不占据屏幕,且立马会消失
  1. 适用于即时消息提示
  1. 不打断当前操作,也不获取焦点
  1. 有序对消息进行展示

技术原理

  1. 创建方法

  1. 最简单的创建方式
Context context = getApplicationContext();
CharSequence text = "Hello Toast";
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();
  1. 自定义视图的创建方式
LinearLayout ll = new LinearLayout(this);
Toast toast = new Toast(this);
toast.setView(ll);
toast.setDuration(Toast.LENGTH_SHORT);
toast.setGravity.CENTER, 0, 0);
toast.show();
  1. Toast为系统级别窗口,层级较高
  1. 源码分析

  1. Toast的构造方法 构造方法中创建了TN对象,需要传入Context和Looper
public Toast(@NonNull Context context, @Nullable Looper looper) {
    mContext = context;
    mTN = new TN(context.getPackageName(), looper);
    mTN.mY = context.getResources().getDimensionPixelSize(
            com.android.internal.R.dimen.toast_y_offset);
    mTN.mGravity = context.getResources().getInteger(
            com.android.internal.R.integer.config_toastDefaultGravity);
}

Looper其实是TN在使用,因为我们一般把Toast作为UI控件使用,在主线程中操控,传入的looper为null。而传入一个Looper的作用是在子线程中也能保证其正常显示。Looper.myLooper()为获取当前线程,主线程已经为我们创建好了一个线程,故无需传入looper。


if (looper == null) {
    // Use Looper.myLooper() if looper is not specified.
    looper = Looper.myLooper();
    if (looper == null) {
        throw new RuntimeException(
                "Can't toast on a thread that has not called Looper.prepare()");
    }
}

TN继承自ITransientNotification.Stub,是Toast的内部私有静态类,实现了ITransientNotification的show(IBinder windowToken)和hide()方法。

/** @hide */
oneway interface ITransientNotification {
    void show(IBinder windowToken);
    void hide();
}
  1. show()方法

该方式是也是同样通过AIDL方式获取NotificationManagerService的接口,然后把TN对象以及参数传递给NotificationManagerService中

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
    }
}

其中getService()方法通过单例模式来拿到notification的service对象

static private INotificationManager getService() {
    if (sService != null) {
        return sService;
    }
    sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
    return sService;
}
  1. enqueueToast(pkg, tn, duration) 该方法为NotificationManagerService类下的方法,先判断这个Toast是否为系统Toast,如果是系统Toast则会进入系统队列中,然后创建Token,将TN和Token等参数封装为ToastRecord对象添加到mToastQueue中。如果当前Toast的index为0,则会进行showNextToastLocked()方法。
synchronized (mToastQueue) {
    int callingPid = Binder.getCallingPid();
    long callingId = Binder.clearCallingIdentity();
    try {
        ToastRecord record;
        int index;
        // All packages aside from the android package can enqueue one toast at a time
        if (!isSystemToast) {
            index = indexOfToastPackageLocked(pkg);
        } else {
            index = indexOfToastLocked(pkg, callback);
        }

        // If the package already has a toast, we update its toast
        // in the queue, we don't move it to the end of the queue.
        if (index >= 0) {
            record = mToastQueue.get(index);
            record.update(duration);
            try {
                record.callback.hide();
            } catch (RemoteException e) {
            }
            record.update(callback);
        } else {
            Binder token = new Binder();
            mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
            record = new ToastRecord(callingPid, pkg, callback, duration, token);
            mToastQueue.add(record);
            index = mToastQueue.size() - 1;
        }
        keepProcessAliveIfNeededLocked(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);
    }
}
  1. showNextToastLocked()方法

callbake为ITransientNotification接口实现TN对象,这个show()方法为发送Message调用handleShow(windowToken)方法来进行显示。该token即为上面提到的Token,其作用为表示该Toast为系统窗口,具有较高权限和显示层级。该方法配置好WindowManager的相应参数后,将View弹出。

try {
    record.callback.show(record.token);
    scheduleDurationReachedLocked(record);
    return;
}
  1. cancelToastLocked(index)

scheduleDurationReachedLocked(record)方法为开始计时,在计时到达预定的时间后,发送Message调用cancelToastLocked(index)方法。

而cancelToastLocked(index)方法是调用tn下的hide()方法发送Message调用handleHide()方法,将View移除掉,并释放掉各类资源。

在最后判断mToastQueue中是否还有参数,如果还有则会进行下一轮的View的创建和销毁。

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);
    }

    ToastRecord lastToast = mToastQueue.remove(index);

    mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
            DEFAULT_DISPLAY);
    scheduleKillTokenTimeout(lastToast.token);

    keepProcessAliveIfNeededLocked(record.pid);
    if (mToastQueue.size() > 0) {
        showNextToastLocked();
    }
}
  1. UML图

路径:

Toast.java : \base\core\java\android\widget

ITransientNotification : \base\core\java\android\app

NotificationManagerService :\base\services\core\java\com\android\server\notification

INotificationManager :\base\core\java\android\app

类图:

流程图:

时序图:

  1. 技术总结

  1. 通信方式

总共用到两种通信方式,一种是AIDL消息机制,一种是Handler消息机制

  1. Activity销毁后Toast仍会显示

Toast的token为系统级别的,其次keepProcessAliveIfNeededLocked(callingPid)方法将当前的Toast设置为了前台进程,保证了不会被系统杀死

  1. 子线程调用Toast

子线程调用Toast需要自己新建Looper,上文有提到

  1. Toast显示和隐藏的重点逻辑

    1. Toast类下的show()方法其实是将本地的TN类型的参数传递给NotificationManager中进行操作
    2. NotificationManager在收到Toast的show()请求后,会将一个系统级别的Token添加到ToastRecord中与TN封装在一起,传递给TN类下的show()方法,并开启计时监听
    3. TN对象收到消息就会将Toast中的View添加到WindowManagerService管理,并进行显示。
    4. 在时间到达设定点后,则会远程调用TN类下的hide()方法移除Toast窗口,然后判断mToastQueue中是否还有对象,如果有则继续进行show()和hide()操作。