使用方式
一般使用
Toast.makeText(this,"Short Toast", Toast.LENGTH_SHORT).show();
自定义Toast,通过如下接口注入自定义视图
##Toast.java##
public void setView(View view) {
mNextView = view;
}
接下来步入正题,Toast的显示流程
Toast流程分析
显示show()
- 从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
}
}
- 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);
}
}
}
- 再来看下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);
}
}
- 接着第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;
}
}
- 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已经走完了显示的所有流程,正常显示在了屏幕上;如何退出的呢?
延迟退出计时
- 回到第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()
- 延迟计时结束后;最后就来到的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();
}
}
- 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;
}
- 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