一文读懂Toast显示流程

使用方式

一般使用

Toast.makeText(this,"Short Toast", Toast.LENGTH_SHORT).show();

自定义Toast,通过如下接口注入自定义视图

##Toast.java##
public void setView(View view) {
    mNextView = view;
}

接下来步入正题,Toast的显示流程

Toast流程分析

显示show()

  1. 从show方法开始,自定义toast会创建Toast.TN binder对象,用于与NMS交互;最终将toast相关信息转入NMS入队
##Toast.java##

public void show() {
    //..
    //1.获取NMS
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    //2.创建TN,用于与NMS通信的binder对象,自定义的toast会用到
    TN tn = mTN;
    //3.自定义toast视图
    tn.mNextView = mNextView;
    final int displayId = mContext.getDisplayId();

    //4.调用NMS接口toast入队(主要区别为自定义toast会传入TN参数,后续用来与NMS交互)
    try {
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
            if (mNextView != null) {
                // It's a custom toast
                service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
            } else {
                // It's a text toast
                ITransientNotificationCallback callback =
                        new CallbackBinder(mCallbacks, mHandler);
                service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
            }
        } else {
            service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
        }
    } catch (RemoteException e) {
        // Empty
    }
}
  1. NMS中的入队流程,入队接口最终汇入同一主流;显示会进行一系列前置条件判断;之后会判断当前toast是否存在于队列中,存在则直接刷新;反之,则将消息封装成ToastRecord则进行入队;
    若入队为首个,则直接开始调度显示showNextToastLocked()
    PS:入队之前还会有一个门槛,NMS会有单应用toast个数阈值,阈值以下,正常入队,阈值以上则直接拦截;直到该应用toast个数消费小于阈值后可正常入队(推测这也是android的一种防攻击降负载的策略)
##NotificationManagerService.java##

final IBinder mService = new INotificationManager.Stub() {
    @Override
    public void enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration,
            int displayId, @Nullable ITransientNotificationCallback callback) {
        enqueueToast(pkg, token, text, null, duration, displayId, callback);
    }

    @Override
    public void enqueueToast(String pkg, IBinder token, ITransientNotification callback,
            int duration, int displayId) {
        enqueueToast(pkg, token, null, callback, duration, displayId, null);
    }
    
    private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
        @Nullable ITransientNotification callback, int duration, int displayId,
        @Nullable ITransientNotificationCallback textCallback) {
        //...
        synchronized (mToastQueue) {
          int callingPid = Binder.getCallingPid();
          long callingId = Binder.clearCallingIdentity();
          try {
              ToastRecord record;
              int index = indexOfToastLocked(pkg, token);
              // 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) {
                  //1.相同toast只更新时长,不改变位置
                  record = mToastQueue.get(index);
                  record.update(duration);
              } else {
                  // Limit the number of toasts that any given package can enqueue.
                  // Prevents DOS attacks and deals with leaks.
                  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++;
                          //2.队列中同包名toast大于阈值,则直接return,不入队
                          if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                              Slog.e(TAG, "Package has already posted " + count
                                      + " toasts. Not showing more. Package=" + pkg);
                              return;
                          }
                      }
                  }
                  Binder windowToken = new Binder();
                  mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId);
                  //3.将toast信息封装为ToastRecord
                  record = getToastRecord(callingUid, callingPid, pkg, token, text, callback,
                          duration, windowToken, displayId, textCallback);
                  //4.入队
                  mToastQueue.add(record);
                  index = mToastQueue.size() - 1;
                  keepProcessAliveForToastIfNeededLocked(callingPid);
              }
              // If it's at index 0, it's the current toast.  It doesn't matter if it's
              // new or just been updated, show it.
              // If the callback fails, this will remove it from the list, so don't
              // assume that it's valid after this.
              if (index == 0) {
                  //5.显示toast
                  showNextToastLocked();
              }
          } finally {
              Binder.restoreCallingIdentity(callingId);
          }
      }
    }
  1. 再来看下Toast信息封装成ToastRecord部分getToastRecord();ToastRecord为抽象类,实现类分别为:
    TextToastRecord :默认文本类Toast
    CustomToastRecord :自定义Toast
    此处的callback实现即为Toast.TN,自定义Toast时会创建传入非空
##NotificationManagerService.java##

private ToastRecord getToastRecord(int uid, int pid, String packageName, IBinder token,
        @Nullable CharSequence text, @Nullable ITransientNotification callback, int duration,
        Binder windowToken, int displayId,
        @Nullable ITransientNotificationCallback textCallback) {
    if (callback == null) {
        return new TextToastRecord(this, mStatusBar, uid, pid, packageName, token, text,
                duration, windowToken, displayId, textCallback);
    } else {
        return new CustomToastRecord(this, uid, pid, packageName, token, callback, duration,
                windowToken, displayId);
    }
}
  1. 接着第2点,NMS中调度Toast显示showNextToastLocked(),while循环,取出列表首个record进行显示
##NotificationManagerService.java##

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        //Toast显示
        if (record.show()) {
            //成功则启动退出的延迟计时
            scheduleDurationReachedLocked(record);
            return;
        }
        //失败则取出下一个
        int index = mToastQueue.indexOf(record);
        if (index >= 0) {
            mToastQueue.remove(index);
        }
        record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
    }
}
  1. ToastRecord中的show(),如第3点可知,此处存在两种业务
    (1) 先看默认文本类Toast:TextToastRecord ;此处的mStatusBar为StatusBarManagerService的binder代理
##TextToastRecord.java##
//...
private final StatusBarManagerInternal mStatusBar;
//...
@Override
public boolean show() {
    if (DBG) {
        Slog.d(TAG, "Show pkg=" + pkg + " text=" + text);
    }
    if (mStatusBar == null) {
        Slog.w(TAG, "StatusBar not available to show text toast for package " + pkg);
        return false;
    }
    mStatusBar.showToast(uid, pkg, token, text, windowToken, getDuration(), mCallback);
    return true;
}

再来看StatusBarManagerService,又会通过IStatusBar的binder将信息传入下一级,IStatusBar又是哪位的代理?!SystemUI

##StatusBarManagerService.java##

private volatile IStatusBar mBar;
@Override
public void showToast(int uid, String packageName, IBinder token, CharSequence text,
        IBinder windowToken, int duration,
        @Nullable ITransientNotificationCallback callback) {
    if (mBar != null) {
        try {
            mBar.showToast(uid, packageName, token, text, windowToken, duration, callback);
        } catch (RemoteException ex) { }
    }
}

SystemUI中存在一个叫CommandQueue的类,实现了该binder接口;之后Handler消息调度,将消息传递至CommandQueue.Callbacks转出

##CommandQueue##

public class CommandQueue extends IStatusBar.Stub implements CallbackController<Callbacks>,
        DisplayManager.DisplayListener {
    @Override
    public void showToast(int uid, String packageName, IBinder token, CharSequence text,
        IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
        synchronized (mLock) {
            //参数赋值
            mHandler.obtainMessage(MSG_SHOW_TOAST, args).sendToTarget();
        }
}

//Handler实现:
case MSG_SHOW_TOAST: {
    args = (SomeArgs) msg.obj;
    String packageName = (String) args.arg1;
    IBinder token = (IBinder) args.arg2;
    CharSequence text = (CharSequence) args.arg3;
    IBinder windowToken = (IBinder) args.arg4;
    ITransientNotificationCallback callback =
            (ITransientNotificationCallback) args.arg5;
    int uid = args.argi1;
    int duration = args.argi2;
    for (Callbacks callbacks : mCallbacks) {
        callbacks.showToast(uid, packageName, token, text, windowToken, duration,
                callback);
    }
    break;
}

这里虽然通过遍历Callbacks集合回调,但所有的Callbacks中只有一处真正实现了showToast()接口,就是SystemUI中的ToastUI
ToastUI在启动时即会将自身注册至CommandQueue中,至此便到了Toast显示的最后一步
ToastPresenter

##ToastUI.java##

@Override
public void start() {
    mCommandQueue.addCallback(this);
}

@Override
@MainThread
public void showToast(int uid, String packageName, IBinder token, CharSequence text,
        IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
    if (mPresenter != null) {
        hideCurrentToast();
    }
    Context context = mContext.createContextAsUser(UserHandle.getUserHandleForUid(uid), 0);
    View view = ToastPresenter.getTextToastView(context, text);
    mCallback = callback;
    mPresenter = new ToastPresenter(context, mAccessibilityManager, mNotificationManager,
            packageName);
    mPresenter.show(view, token, windowToken, duration, mGravity, 0, mY, 0, 0, mCallback);
}

ToastPresenter为android系统提供的API,show方法中会进行layoutParams调整,同时移除之前;最终调用WMS的addView添加窗口,完成Toast的显示
从createLayoutParams()方法中,可以看出Toast视图的统一层级为:WindowManager.LayoutParams.TYPE_TOAST;
PS:推测google通过ToastPresenter来统一Toast的显示,规范层级使用

##ToastPresenter.java##

/**
 * Shows the toast in {@code view} with the parameters passed and callback {@code callback}.
 */
public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
        int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
        @Nullable ITransientNotificationCallback callback) {
    checkState(mView == null, "Only one toast at a time is allowed, call hide() first.");
    mView = view;
    mToken = token;
    //根据Toast参数重新调整默认layoutParams
    adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset,
            horizontalMargin, verticalMargin);
    if (mView.getParent() != null) {
        mWindowManager.removeView(mView);
    }
    try {
        //WMS添加窗口
        mWindowManager.addView(mView, mParams);
    } catch (WindowManager.BadTokenException e) {
        // Since the notification manager service cancels the token right after it notifies us
        // to cancel the toast there is an inherent race and we may attempt to add a window
        // after the token has been invalidated. Let us hedge against that.
        Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e);
        return;
    }
    trySendAccessibilityEvent(mView, mPackageName);
    if (callback != null) {
        try {
            callback.onToastShown();
        } catch (RemoteException e) {
            Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
        }
    }
}

/**
 * Creates {@link WindowManager.LayoutParams} with default values for toasts.
 */
private WindowManager.LayoutParams createLayoutParams() {
    WindowManager.LayoutParams params = new WindowManager.LayoutParams();
    params.height = WindowManager.LayoutParams.WRAP_CONTENT;
    params.width = WindowManager.LayoutParams.WRAP_CONTENT;
    params.format = PixelFormat.TRANSLUCENT;
    params.windowAnimations = R.style.Animation_Toast;
    //指定视图层级
    params.type = WindowManager.LayoutParams.TYPE_TOAST;
    params.setFitInsetsIgnoringVisibility(true);
    params.setTitle(WINDOW_TITLE);
    params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
            | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
    setShowForAllUsersIfApplicable(params, mPackageName);
    return params;
}

至此,默认文本类的TextToastRecord流程结束

(2)自定义Toast:CustomToastRecord;此处的callback是否有点熟悉?对,没错,就是之前提到的自定义Toast会创建一个用于与NMS交互的binder - Toast.TN

##CustomToastRecord.java##

public final ITransientNotification callback;
@Override
public boolean show() {
    if (DBG) {
        Slog.d(TAG, "Show pkg=" + pkg + " callback=" + callback);
    }
    try {
        callback.show(windowToken);
        return true;
    } catch (RemoteException e) {
        Slog.w(TAG, "Object died trying to show custom toast " + token + " in package "
                + pkg);
        mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
        return false;
    }
}

Toast.TN,show()方法也是通过Handler调度;一路看下去可发现;最终也是通过ToastPresenter进行最终的显示

##Toast.java##

private static class TN extends ITransientNotification.Stub {
    @Override
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public void show(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    }
}
//Handler调度
case SHOW: {
    IBinder token = (IBinder) msg.obj;
    handleShow(token);
    break;
}
//show业务实现
public void handleShow(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
            + " mNextView=" + mNextView);
    // If a cancel/hide is pending - no need to show - at this point
    // the window token is already invalid and no need to do any work.
    if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
        return;
    }
    if (mView != mNextView) {
        // remove the old view if necessary
        handleHide();
        mView = mNextView;
        mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
                mHorizontalMargin, mVerticalMargin,
                new CallbackBinder(getCallbacks(), mHandler));
    }
}

显示流程小总结:

默认文本类Toast:(分别跨越了app,system_server,systemui三个进程)
Toast -> NMS -> TextToastRecord -> StatusBarManagerService -> CommandQueue -> ToastUI -> ToastPresenter

自定义类Toast:(跨越了app,system_server两个进程)
Toast -> NMS -> CustomToastRecord -> Toast.TN -> ToastPresenter
至此,Toast已经走完了显示的所有流程,正常显示在了屏幕上;如何退出的呢?

延迟退出计时

  1. 回到第4点中所讲的;record.show()流程无异常都会返回true,进行后续流程显示;延迟退出则通过scheduleDurationReachedLocked(record)
##NotificationManagerService.java##

@GuardedBy("mToastQueue")
void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        if (record.show()) {
            scheduleDurationReachedLocked(record);
            return;
        }
        int index = mToastQueue.indexOf(record);
        if (index >= 0) {
            mToastQueue.remove(index);
        }
        record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
    }
}

延迟的业务即通过Handler发送延迟消息,此处我们在Toast中设置的时长就会排上用场;
默认情况下:Toast.LENGTH_LONG - 3.5s,Toast.LENGTH_SHORT - 2s;
当然,此时长也会受其他因素的影响,即:无障碍辅助功能;注释也很清楚,如下;
辅助功能用户可能需要更长的超时时间。该api将原始延迟与用户的偏好进行比较,并返回较长的延迟。如果没有偏好,则返回原始延迟。
对应会在Settings中存在设置项:本地模拟器Pixel(API_30),设置-无障碍-等待操作的时长 可进行时长或默认设置

##NotificationManagerService.java##

//PhoneWindowManager中此处定义为3.5s
static final int LONG_DELAY = PhoneWindowManager.TOAST_WINDOW_TIMEOUT;
static final int SHORT_DELAY = 2000; // 2 seconds

private void scheduleDurationReachedLocked(ToastRecord r)
{
    mHandler.removeCallbacksAndMessages(r);
    Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
    //默认时长计算
    int delay = r.getDuration() == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
    //无障碍功能对delay的影响
    // Accessibility users may need longer timeout duration. This api compares original delay
    // with user's preference and return longer one. It returns original delay if there's no
    // preference.
    delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
            AccessibilityManager.FLAG_CONTENT_TEXT);
    mHandler.sendMessageDelayed(m, delay);
}

退出cancel()

  1. 延迟计时结束后;最后就来到的Toast的退出;延迟消息到达走如下方法
##NotificationManagerService.java##

private void handleDurationReached(ToastRecord record)
{
    if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " token=" + record.token);
    synchronized (mToastQueue) {
        int index = indexOfToastLocked(record.pkg, record.token);
        if (index >= 0) {
            cancelToastLocked(index);
        }
    }
}

cancelToastLocked()具体业务如下;获取对应的ToastRecord对象,调用hide()方法;最后判断,Toast队列非空则重新开始调度显示下一个Toast

##NotificationManagerService.java##

void cancelToastLocked(int index) {
    ToastRecord record = mToastQueue.get(index);
    record.hide();

    ToastRecord lastToast = mToastQueue.remove(index);

    mWindowManagerInternal.removeWindowToken(lastToast.windowToken, false /* removeWindows */,
            lastToast.displayId);
    // We passed 'false' for 'removeWindows' so that the client has time to stop
    // rendering (as hide above is a one-way message), otherwise we could crash
    // a client which was actively using a surface made from the token. However
    // we need to schedule a timeout to make sure the token is eventually killed
    // one way or another.
    scheduleKillTokenTimeout(lastToast);

    keepProcessAliveForToastIfNeededLocked(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();
    }
}
  1. ToastRecord的hide()方法与show()方法流程大致相同,此处不再过多赘述,最终都是到ToastPresenter中,通过WMS接口立即移除视图
##ToastPresenter.java##

public void hide(@Nullable ITransientNotificationCallback callback) {
    checkState(mView != null, "No toast to hide.");

    if (mView.getParent() != null) {
        mWindowManager.removeViewImmediate(mView);
    }
    try {
        mNotificationManager.finishToken(mPackageName, mToken);
    } catch (RemoteException e) {
        Log.w(TAG, "Error finishing toast window token from package " + mPackageName, e);
    }
    if (callback != null) {
        try {
            callback.onToastHidden();
        } catch (RemoteException e) {
            Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastHide()", e);
        }
    }
    mView = null;
    mToken = null;
}
  1. Toast的显示/隐藏回调;接口如下,CallbackBinder为binder对象,把收到的toast显示状态传递到Callback给到外部使用者;那CallbackBinder的状态又是哪里传递的呢?在ToastPresenter中
##Toast.java##

public abstract static class Callback {
    /**
     * Called when the toast is displayed on the screen.
     */
    public void onToastShown() {}

    /**
     * Called when the toast is hidden.
     */
    public void onToastHidden() {}
}

private static class CallbackBinder extends ITransientNotificationCallback.Stub {
    @Override
    public void onToastShown() {
        mHandler.post(() -> {
            for (Callback callback : getCallbacks()) {
                callback.onToastShown();
            }
        });
    }
    @Override
    public void onToastHidden() {
        mHandler.post(() -> {
            for (Callback callback : getCallbacks()) {
                callback.onToastHidden();
            }
        });
    }
}

分别在show与hide时进行状态回调;不同在于:
默认类Toast的最终显示是在SystemUI进程的ToastPresenter,因此回调需要跨进程
自定义类Toast的最终显示是在App进程(准确来讲是调用Toast.show()方法的进程)Toast.TN的ToastPresenter中,因此回调不会涉及跨进程
感兴趣的话可以自行看下这两种CallbackBinder的创建与注入即可。

##ToastPresenter.java##

public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
        int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
        @Nullable ITransientNotificationCallback callback) {
    //..
    if (callback != null) {
        try {
            callback.onToastShown();
        } catch (RemoteException e) {
            Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
        }
    }
}

public void hide(@Nullable ITransientNotificationCallback callback) {
    //..
    if (callback != null) {
        try {
            callback.onToastHidden();
        } catch (RemoteException e) {
            Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastHide()", e);
        }
    }
    //..
}

时序图

附上时序图
在这里插入图片描述

到这里Toast流程分析便告一段落,over

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值