前些日志QQ群有朋友发了一个Toast的崩溃日志。Toast如此简单的用法怎么会崩溃呢?所以顺便就学习了一下Toast在源码中的实现,不算复杂,但内容挺多的,这里就来分享一下,方便读者。
主要有两种实现方式:
使用方式很简单,直接沟通过静态方法构传入context,显示内容以及显示时长三个参数,构造Toast对象,然后通过show显示。
- Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
- toast.show();
这种使用使用方式也很简单,首先构造一个View,然后通过setView方法传入这个自定义View,最终也是通过show方法显示。
- View selfToastView = View.inflate(getBaseContext(), R.layout.self_toast, null);
- Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
- toast.setView(selfToastView);
- toast.show();
两种使用方式都很简单,区别只是第二种方式多传入了一个自定义View而已。但是为什么要分开来讲呢?因为虽然使用时仅仅只差一步,但是其实现原理是完全不一样的。一个是通过NotificationManagerService去显示的,而另外一个则是APP自身处理的。接下来,我们就依次的讲一下两种使用方式的实现原理。
这个的实现方式还是比较简单的,最终的生成方式传入4个参数,分别为
Context:绑定的上下文对象
Looper:绑定线程的Looper,可以为空。为空时则默认使用当前线程的looper。PS:每个线程都只能绑定唯一的一个Looper,想了解这一块的可以看我的另外一篇文章:android源码学习-Handler机制及其六个核心点_失落夏天的博客-CSDN博客_安卓开发handler机制
text:显示内容
duration:持续时间。有两种参数:
Toast.LENGTH_LONG:显示时间较长,为7S。
Toast.LENGTH_SHORT:显示时间较短,为4S。
这两个时间长度是不是和你们认知有区别?是不是还以为是2.5和3.5S呢?从某个版本开始,Toast的时间确实已经改成了4S和7S了,后面会讲到这两个时间的具体设置的地方。
最终生成一个Toast对象返回,这里需要注意的是,原生Toast和自定义View的Toast的唯一区别就是原生Toast对象中mNextView对象为null。
- public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
- @NonNull CharSequence text, @Duration int duration) {
- //这里默认配置为true,走上面这个判断逻辑
- if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
- Toast result = new Toast(context, looper);
- result.mText = text;
- result.mDuration = duration;
- return result;
- } else {
- Toast result = new Toast(context, looper);
- View v = ToastPresenter.getTextToastView(context, text);
- result.mNextView = v;
- result.mDuration = duration;
-
- return result;
- }
- }
然后Toast的构造方法如下,主要是构建几个后门需要使用到的对象:
- public Toast(@NonNull Context context, @Nullable Looper looper) {
- mContext = context;
- mToken = new Binder();
- looper = getLooper(looper);
- mHandler = new Handler(looper);
- mCallbacks = new ArrayList<>();
- mTN = new TN(context, context.getPackageName(), mToken,
- mCallbacks, 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);
- }
mContext:Context对象
mToken:构造binder对象,后面和NotificationManagerService通信都是通过这个binder。
looper:当前Toast绑定的线程looper,传入null时默认为当前线程。
mTN:Binder.Stub类型对象,作为binder的client端。其接受跨进程传递过来的信息时是在单独的binder线程中处理的。
mTN.mY:纵坐标偏移量,简单来说就是控制Toast在屏幕中显示位置是靠上一点还是靠下一点的。
mTN.mGravity:控制Toast的显示位置。一般是局中,靠下两种。
show方法中主要是执行三段逻辑,
首先把Toast的mNextView赋值给tn.mNextView,如果Toast的mNextView为null,那么tn.mNextView自然也是null;
然后获取NotificationManager的binder引用service;
接下来走一个判断逻辑,
1.如果mNextView==null时,走service.enqueueToast逻辑,通过binder跨进程通讯,会调用到NotificationManagerService中的enqueueToast方法,这个我们会在第三章讲解。
2.如果mNextView!=null时,通过调用service.enqueueTextToast方法,通过binder跨进程通讯,会调用到NotificationManagerService中的enqueueTextToast方法,这个我们会在第四章讲解。
- public void show() {
- ...
-
- INotificationManager service = getService();
- String pkg = mContext.getOpPackageName();
- TN tn = mTN;
- tn.mNextView = mNextView;
- final int displayId = mContext.getDisplayId();
-
- ...
- 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);
- }
- }
- ...
- }
上文讲到通过binder传输,此时NotificationManagerService中的mService对象中的enqueueTextToast()方法会接收到通知,具体参数解释如下:
- /**
- *
- * @param pkg 包名
- * @param token APP端binder
- * @param text 显示内容
- * @param duration 持续时间
- * @param displayId 标记唯一显示区域的ID,对应的实体类是DisplayContent
- * @param callback 跨进程的callBack对象,自定义View的Toast有值。默认的Toast方法为null
- */
- @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);
- }
这个方法会传递到enqueueToast方法(这里稍微扩展一下,其实自定义View的Toast也会走到这个方法)。
我们都知道,Toast显示是有时序的,先调用的Toast一定会先展示,所以这就需要一个集合来维护这个先后的关系,而这个集合就是mToastQueue。
final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();
上一小节的流程进入enqueueToast方法后,其实主要分为两块逻辑,核心代码如下:
- private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
- @Nullable ITransientNotification callback, int duration, int displayId,
- @Nullable ITransientNotificationCallback textCallback) {
- ...
- //上面的内容都是做参数合法性检查
- final int callingUid = Binder.getCallingUid();
-
- ...
- //此方法做权限检查
- if (!checkCanEnqueueToast(pkg, callingUid, isAppRenderedToast, isSystemToast)) {
- return;
- }
-
- synchronized (mToastQueue) {
- int callingPid = Binder.getCallingPid();
- final 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) {
- record = mToastQueue.get(index);
- record.update(duration);
- } else {
- //插入逻辑
- ...
- }
- ...
- if (index == 0) {
- showNextToastLocked(false);
- }
- }
- ...
- }
- }
这个方法中,首先我们看到了加锁的代码:synchronized (mToastQueue),所以说明这是一个多线程的场景。binder机制中,作为server端会有一个线程池来处理client发过来的binder请求,每个请求都会分配一个线程去处理,所以这里才会有多线程的加锁逻辑。
方法中如下逻辑分为以下两块:
1.首先做参数合法性检查以及权限检查,
2.然后进入队列逻辑。
队列逻辑中,首先根据pkg和token通过indexOfToastLocked方法判断在集合中是否存在。
int index = indexOfToastLocked(pkg, token);
如果index>=0,则说明mToastQueue中已经存在了传入APP所对应的binder对象,则直接更新所对应的record的持续时间。
indexOfToastLocked方法如下:
- int indexOfToastLocked(String pkg, IBinder token) {
- ArrayList
list = mToastQueue; - int len = list.size();
- for (int i=0; i
- ToastRecord r = list.get(i);
- if (r.pkg.equals(pkg) && r.token == token) {
- return i;
- }
- }
- return -1;
- }
是通过循环便利的方式来进行判断的,效率略微有些低,这里略微吐槽一下源码,也许使用TreeMap会是一个更好的选择(key=pkg+token.hashcode)。当然,google也是是考虑到Toast排队的场景较少,所才选择使用ArrayList。
由于每个Toast都对应一个binder对象,所以如果toast是复用的,则短时间内多次调用show放,也只会对应同一个Record对象,所以也只会显示一次。
如果index<0,则说明mToastQueue不存在该toast所对应的binder,则进入插入的逻辑。
3.3 插入逻辑
插入逻辑的代码如下
- } 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++;
- if (count >= MAX_PACKAGE_TOASTS) {
- Slog.e(TAG, "Package has already queued " + count
- + " toasts. Not showing more. Package=" + pkg);
- return;
- }
- }
- }
-
- Binder windowToken = new Binder();
- mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId,
- null /* options */);
- record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, token,
- text, callback, duration, windowToken, displayId, textCallback);
- 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) {
- showNextToastLocked(false);
- }
首先判断同一个包名下是否已经存在了5条(含)以上的未显示Toast,如果有则不允许继续添加。
否则,通过getToastRecord方法生成一个ToastRecord对象加入到集合最尾端,并且通过keepProcessAliveForToastIfNeededLocked方法保证弹Toast的进程不被杀死,如果当前只有一条记录的话,则直接调用showNextToastLocke方法进行显示。
ToastRecord其实是一个抽象方法,它有两个实现类,TextToastRecord和CustomToastRecord。getToastRecord方法中会根据callback是否为空来进行对应的生成,其中callback==null时生成的是TextToastRecord类型对象。
3.4 生产者消费者模型
这里又涉及到生产者消费者模式了,既然APP端通过binder方法向mToastQueue集合中插入数据,那么就一定有消费者来消费。而这个消费者就是showNextToastLocked方法。
由于上面所说的加锁逻辑,所以永远只会有一个线程在执行showNextToastLocked方法。
方法如下:
- void showNextToastLocked(boolean lastToastWasTextRecord) {
- if (mIsCurrentToastShown) {
- return; // Don't show the same toast twice.
- }
-
- ToastRecord record = mToastQueue.get(0);
- while (record != null) {
- int userId = UserHandle.getUserId(record.uid);
- boolean rateLimitingEnabled =
- !mToastRateLimitingDisabledUids.contains(record.uid);
- boolean isWithinQuota =
- mToastRateLimiter.isWithinQuota(userId, record.pkg, TOAST_QUOTA_TAG)
- || isExemptFromRateLimiting(record.pkg, userId);
- boolean isPackageInForeground = isPackageInForegroundForToast(record.uid);
-
- if (tryShowToast(
- record, rateLimitingEnabled, isWithinQuota, isPackageInForeground)) {
- scheduleDurationReachedLocked(record, lastToastWasTextRecord);
- mIsCurrentToastShown = true;
- if (rateLimitingEnabled && !isPackageInForeground) {
- mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
- }
- return;
- }
-
- int index = mToastQueue.indexOf(record);
- if (index >= 0) {
- mToastQueue.remove(index);
- }
- record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
- }
- }
代码虽较长,但核心逻辑只有三块:
1.按照先后顺序便利mToastQueue集合,取出record对象。
2.通过tryShowToast方法尝试显示record对象。如果成功,则执行scheduleDurationReachedLocked方法。
3.如果失败,则从集合中删除。就是说如果Toast显示时如果失败了也不会再次尝试。
tryShowToast的逻辑我们下一小节会讲,这里看一下scheduleDurationReachedLocked的实现:
- private void scheduleDurationReachedLocked(ToastRecord r, boolean lastToastWasTextRecord)
- {
- mHandler.removeCallbacksAndMessages(r);
- Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
- int delay = r.getDuration() == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
- //通过无障碍辅助功能修正这个delay值,如果开始无障碍辅助的话,事件会比正常值要长一些
- delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
- AccessibilityManager.FLAG_CONTENT_TEXT);
- //如果上一个Toast还在显示,则流出来上一个Toast的离场动画事件。
- if (lastToastWasTextRecord) {
- delay += 250; // delay to account for previous toast's "out" animation
- }
- //如果是TextToastRecord类型,则流出来动画进场时间。
- if (r instanceof TextToastRecord) {
- delay += 333; // delay to account for this toast's "in" animation
- }
-
- mHandler.sendMessageDelayed(m, delay);
- }
首先从Looper中的mQueue中删除带当前TaskRecord对象的Message,
然后从对象池中重新生成一个带TaskRecord对象的Message,加入到延时任务中。延时时间恰好就是duration中设置的4S或者7S。
handler在时间到了之后,会执行MESSAGE_DURATION_REACHED类型的事件,调用handleDurationReached方法,该方法中又回调用cancelToastLocked方法:
- void cancelToastLocked(int index) {
- //1.回调APP层TN的hide方法进行通知;
- ToastRecord record = mToastQueue.get(index);
- record.hide();
-
- if (index == 0) {
- mIsCurrentToastShown = false;
- }
- //2.对队列中删除ToastRecorde对象
- ToastRecord lastToast = mToastQueue.remove(index);
- //3.删除在WMS中的注册的WindowToken
- mWindowManagerInternal.removeWindowToken(lastToast.windowToken, false /* removeWindows */,
- lastToast.displayId);
- //4.再发一个延时信号确保token删除完成
- scheduleKillTokenTimeout(lastToast);
-
- //5.确保Toast显示过程中进程不会被杀死
- keepProcessAliveForToastIfNeededLocked(record.pid);
-
- //6.集合中如果还有消息,就继续执行
- 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(lastToast instanceof TextToastRecord);
- }
- }
主要执行了以下几段逻辑:
1.回调APP层TN的hide方法进行通知;(后续逻辑4.3小节会讲)
2.对队列中删除ToastRecorde对象
3.删除在WMS中的注册的WindowToken
4.再发一个延时信号确保token删除完成
5.确保Toast显示过程中进程不会被杀死
6.集合中如果还有消息,就继续执行
3.5 tryShowToast方法尝试显示
该方法也是很简单的,进行相关逻辑判断是否可以显示,如果可以直接调用record.show方法。
- private boolean tryShowToast(ToastRecord record, boolean rateLimitingEnabled,
- boolean isWithinQuota, boolean isPackageInForeground) {
- if (rateLimitingEnabled && !isWithinQuota && !isPackageInForeground) {
- reportCompatRateLimitingToastsChange(record.uid);
- Slog.w(TAG, "Package " + record.pkg + " is above allowed toast quota, the "
- + "following toast was blocked and discarded: " + record);
- return false;
- }
- if (blockToast(record.uid, record.isSystemToast, record.isAppRendered(),
- isPackageInForeground)) {
- Slog.w(TAG, "Blocking custom toast from package " + record.pkg
- + " due to package not in the foreground at the time of showing the toast");
- return false;
- }
- return record.show();
- }
这时候,我们就要看ToastRecord的show方法了。之前说了,有两种类实现,分别是TextToastRecord和CustomToastRecord两种类型。CustomToastRecord中的类型就是自定义View的Toast,我们下一章专门来讲,这里我们只讲TextToastRecord的类型。
这里我们先看TextToastRecord类型的实现:
- @Override
- public boolean show() {
- ...
- mStatusBar.showToast(uid, pkg, token, text, windowToken, getDuration(), mCallback);
- return true;
- }
这里的mStatusBar又是一个注册的service,其实现类在StatusBarManagerService.java中:
private final StatusBarManagerInternal mInternalService = new StatusBarManagerInternal() {}
我们直接看其showToast方法:
- 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) { }
- }
- }
又是一层转发逻辑,转交到CommandQueue的showToast方法中:
- @Override
- public void showToast(int uid, String packageName, IBinder token, CharSequence text,
- IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
- synchronized (mLock) {
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = packageName;
- args.arg2 = token;
- args.arg3 = text;
- args.arg4 = windowToken;
- args.arg5 = callback;
- args.argi1 = uid;
- args.argi2 = duration;
- 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;
- }
主线程中通过mCallBacks回调显示,这里的Callbacks的实现在com.android.systemui.toast.ToastUtils.java类中。
3.6 ToastU.showToast完成显示流程
showToast方法中中,委托给ToastPresenter进行逻辑的显示,经典的MVP架构。
- public void showToast(int uid, String packageName, IBinder token, CharSequence text,
- IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
- Runnable showToastRunnable = () -> {
- UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
- Context context = mContext.createContextAsUser(userHandle, 0);
- mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName,
- userHandle.getIdentifier(), mOrientation);
-
- if (mToast.getInAnimation() != null) {
- mToast.getInAnimation().start();
- }
-
- mCallback = callback;
- mPresenter = new ToastPresenter(context, mIAccessibilityManager,
- mNotificationManager, packageName);
- // Set as trusted overlay so touches can pass through toasts
- mPresenter.getLayoutParams().setTrustedOverlay();
- mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString());
- mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(),
- mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(),
- mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation());
- };
-
- if (mToastOutAnimatorListener != null) {
- // if we're currently animating out a toast, show new toast after prev toast is hidden
- mToastOutAnimatorListener.setShowNextToastRunnable(showToastRunnable);
- } else if (mPresenter != null) {
- // if there's a toast already showing that we haven't tried hiding yet, hide it and
- // then show the next toast after its hidden animation is done
- hideCurrentToast(showToastRunnable);
- } else {
- // else, show this next toast immediately
- showToastRunnable.run();
- }
- }
3.7 ToastPresenter.show()方法完成最终Toast的显示
所以接下来我们就要看ToastPresenter中的show方法了,也是最终在该方法中完成了普通Toast的展示。方法如下:
- public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
- int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
- @Nullable ITransientNotificationCallback callback, boolean removeWindowAnimations) {
- checkState(mView == null, "Only one toast at a time is allowed, call hide() first.");
- mView = view;
- mToken = token;
-
- adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset,
- horizontalMargin, verticalMargin, removeWindowAnimations);
- addToastView();
- trySendAccessibilityEvent(mView, mPackageName);
- if (callback != null) {
- try {
- callback.onToastShown();
- } catch (RemoteException e) {
- Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
- }
- }
- }
该方法中,主要还是做了两件事:
1.设置mParams中的属性值。
2.添加到windowManager中完成最终的显示。
我们接下来分开来讲。
3.8 adjustLayoutParams方法配置mParams参数
首先,根据传入的参数调整mParams中的属性值,该属性值决定Toast显示的位置,以及显示时间等等,方法如下:
- private void adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken,
- int duration, int gravity, int xOffset, int yOffset, float horizontalMargin,
- float verticalMargin, boolean removeWindowAnimations) {
- Configuration config = mResources.getConfiguration();
- int absGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());
- params.gravity = absGravity;
- if ((absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
- params.horizontalWeight = 1.0f;
- }
- if ((absGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
- params.verticalWeight = 1.0f;
- }
- params.x = xOffset;
- params.y = yOffset;
- params.horizontalMargin = horizontalMargin;
- params.verticalMargin = verticalMargin;
- params.packageName = mContext.getPackageName();
- params.hideTimeoutMilliseconds =
- (duration == Toast.LENGTH_LONG) ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
- params.token = windowToken;
-
- if (removeWindowAnimations && params.windowAnimations == R.style.Animation_Toast) {
- params.windowAnimations = 0;
- }
- }
这里我们看一下hideTimeoutMilliseconds参数,就是这个来控制最终的显示时间的,SHORT_DURATION_TIMEOUT和LONG_DURATION_TIMEOUT的设置在代码中设置如下:
- private static final long SHORT_DURATION_TIMEOUT = 4000;
- private static final long LONG_DURATION_TIMEOUT = 7000;
这里需要额外说明一点,params的配置的参数在构造方法中也有一部分:
- 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;
- }
我们这里重点看下面这一行
params.type=WindowManager.LayoutParams.TYPE_TOAST;
在安卓中,type代表window的优先层级,数字越大代表优先级越高,就会盖在上面显示。TYPE_TOAST=2005,而Activity所对应的Window优先级是最低的,其所对应的type=1,所以Toast会在Activity的上面显示。
具体代码参考如下:
- public static final int TYPE_BASE_APPLICATION = 1;
- public static final int FIRST_SYSTEM_WINDOW = 2000;
- public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
-
- 所以
- TYPE_TOAST = 2005
- TYPE_BASE_APPLICATION = 1
-
- //Activity中设置的type参数的代码,代码在ActivityThread的handleResumeActivity方法中
- l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
addToastView方法添加到windowManager中
方法内容如下,这里就比较简单了,直接添加到windowManager上。
- private void addToastView() {
- if (mView.getParent() != null) {
- mWindowManager.removeView(mView);
- }
- try {
- 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;
- }
- }
需要注意的是,第三种从service中的binder接收开始,所有的代码都是执行在NoticationManagerService所属的SystemServer进程,所以如果在显示了Toast后立马杀掉APP进程,Toast仍然会正常显示。
Toast隐藏的流程
toast有显示,有windowManager.addView的流程,那么等到持续时间一到,自然有隐藏的Toast的流程。
既然讲到这里,那就不得不讲一下addView之后的流程,主要流程如下:

所以最终会调用WindowManagerService的addWindow方法中。
整个方法流程太长了,所以我们只看和Toast相关的这一部分,代码中会注册一个延时消息,而延时的时间恰恰就是之前设置到mParams中的hideTimeoutMilliseconds,也就是我们上面所说的4S或者7S。
- public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
- int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
- InputChannel outInputChannel, InsetsState outInsetsState,
- InsetsSourceControl[] outActiveControls) {
- ...
- if (type == TYPE_TOAST) {
- if (!displayContent.canAddToastWindowForUid(callingUid)) {
- ProtoLog.w(WM_ERROR, "Adding more than one toast window for UID at a time.");
- return WindowManagerGlobal.ADD_DUPLICATE_ADD;
- }
- // Make sure this happens before we moved focus as one can make the
- // toast focusable to force it not being hidden after the timeout.
- // Focusable toasts are always timed out to prevent a focused app to
- // show a focusable toasts while it has focus which will be kept on
- // the screen after the activity goes away.
- if (addToastWindowRequiresToken
- || (attrs.flags & FLAG_NOT_FOCUSABLE) == 0
- || displayContent.mCurrentFocus == null
- || displayContent.mCurrentFocus.mOwnerUid != callingUid) {
- mH.sendMessageDelayed(
- mH.obtainMessage(H.WINDOW_HIDE_TIMEOUT, win),
- win.mAttrs.hideTimeoutMilliseconds);
- }
- }
- ...
-
- return res;
- }
所以接下来我们就要处理H.WINDOW_HIDE_TIMEOUT事件的代码:
- case WINDOW_HIDE_TIMEOUT: {
- final WindowState window = (WindowState) msg.obj;
- synchronized (mGlobalLock) {
- ...
- window.mAttrs.flags &= ~FLAG_KEEP_SCREEN_ON;
- window.hidePermanentlyLw();
- window.setDisplayLayoutNeeded();
- mWindowPlacerLocked.performSurfacePlacement();
- }
- break;
- }
然后WindowState.java的hidePermanentlyLw方法如如下,通过hide方法去实现隐藏,所以隐藏的流程是不需要客户端或者NotificationManagerService来控制的,而是WMS自己来维护的。
- public void hidePermanentlyLw() {
- if (!mPermanentlyHidden) {
- mPermanentlyHidden = true;
- hide(true /* doAnimation */, true /* requestAnim */);
- }
- }
至于hide隐藏,或者show展示的流程,我们这里不展开讲了,这一块其实属于View的完整显示流程中的内容,会有另外的文章专门来讲。这里我们只需要知道,把Window注册到WMS中后,并不是立马显示的,而是在下一个Vsync信号来临时执行的渲染流程并最终显示到屏幕上的就好了。
四.自定义View的Toast流程讲解
4.1转发到APP层执行逻辑
上文讲到,自定义View的实现类型是CustomToastRecord,其show()方法如下:
- public boolean show() {
- ...
- callback.show(windowToken);
- ...
- }
就是简单的完成了callback的show回调。而这个callback又是binder对象,其实现是APP侧Toast中TN对象。所以我们接着看一下TN中的show方法:
- public void show(IBinder windowToken) {
- mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
- }
通过mHander从binder线程转发事件到Toast所绑定的looper的线程进行处理(一般是主线程,但并不绝对是)。handler中会执行handleShow方法,代码如下:
- public void handleShow(IBinder windowToken) {
- ...
- //如果已经传入了取消和隐藏的信号,那就没必要继续显示了
- if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
- return;
- }
- //mNextView是自定义的View,而mView是上一次显示的内容(如果Toast复用的话)
- 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对象首次显示的话,mView==null。则后续主要分为两块逻辑:
1.先调用handleHide隐藏当前的mView,其最终的实现也是通过ToastPresenter.hide来实现的。具体实现逻辑我们4.3中再讲
2.通过ToastPresenter.show方法进行显示流程。
4.2 ToastPresenter.show显示Toast
执行ToastPresenter.show之前,首先会把mNextView设置为当前将要显示的mView。
mView = mNextView;
show()方法上面的3.7章节我们已经讲过了,就不再重复讲述了。唯一的区别就是这里此时的代码是在APP进程中执行的,而3.7是在SystemServer进程中。
所以自定义的View最终也是通过WindowManager.addView的方式进行显示的。
4.3 Toast的隐藏流程
上面3.4小节的时候还讲到,等到设置的显示时间到了,会通过binder机制通知到Toast.TN中的hide方法。
hide方法中通过handler从binder线程转发到looper所在的线程。
然后Handler中交给handleHide方法进行处理,另外4.1中显示一个自定义view的Toast之前,也会调用handleHide的逻辑,handleHide的代码如下,主要是交给ToastPresenter.hide进行处理
- public void handleHide() {
-
- if (mView != null) {
- ...
- mPresenter.hide(new CallbackBinder(getCallbacks(), mHandler));
- mView = null;
- }
- }
ToastPresenter中hide方法如下:
- 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.如果mView有parent的话,首先从WindowManager中删除,其中mView一定是最顶层的View。
2.通知NotificationManagerService
3.执行毁掉onToastHidden进行通知
4.清空mView和mToken,因为上一个流程执行完成了。
4.4 小节
所以总结一下,自定义View的toast显示和隐藏,其实就类似于APP侧把一个自定义View添加到WindowManager上,然后定时时间到了之后在从WindowManager中移除该自定义View。
五.总结
我们来总结一下,其实Toast显示主要分为两种类型,Text类型和Custom类型。
如果Text类型的话,最终交给SystemServer进程的负责显示,最终会交给ToastUI负责最终的显示工作。它再向WMS注册添加window的时候,会附带传入结束时候,由WMS在时间到了之后负责隐藏Window。
而Custom类型,最终交回给APP进程负责显示,最终也是通过向WMS添加Window的方式进行显示的。此时NotificationManagerService负责记录时间,时间到了之后通知APP进程进行隐藏工作。
Toast显示流程的主要流程图可以总结如下:

六.几个相关问题的拓展
1.Toast可以子线程使用吗?
答:这个问题和子线程中是否可以更新UI有一点类似。只不过检查点和流程略微有一些区别。
首先生成Toast对象的时候会有一个检查Toast.getLooper()方法中:
- private Looper getLooper(@Nullable Looper looper) {
- if (looper != null) {
- return looper;
- }
- return checkNotNull(Looper.myLooper(),
- "Can't toast on a thread that has not called Looper.prepare()");
- }
如果子线程默认当前线程是不会绑定looper的,所以会报错。那么如果我们子线程中初始化Looper呢?那就可以了,只不过最终实现上还有一些区别。TextToastRecord最终仍然会在SystemServer进程中被add到WindowManager中,而CustomToastRecord类型的最终会在APP侧的子线程中显示。另外要注意,prepare一定要和loop方法搭配使用才可以,如下:
- new Thread(new Runnable() {
- @Override
- public void run() {
- Looper.prepare();
- Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
- toast.show();
- Looper.loop();
- }
- }).start();
2.为什么有时候显示Toast会提示要打开通知权限?
上面3.2小节中有讲到,显示Toast之前会进行权限检查,代码如下:
- private boolean checkCanEnqueueToast(String pkg, int callingUid,
- boolean isAppRenderedToast, boolean isSystemToast) {
- //当前APP是否被挂起
- final boolean isPackageSuspended = isPackagePaused(pkg);
- //当前APP是否有通知权限android.Manifest.permission.INTERACT_ACROSS_USERS
- final boolean notificationsDisabledForPackage = !areNotificationsEnabledForPackage(pkg,
- callingUid);
- //APP是否在后台
- final boolean appIsForeground;
- final long callingIdentity = Binder.clearCallingIdentity();
- try {
- appIsForeground = mActivityManager.getUidImportance(callingUid)
- == IMPORTANCE_FOREGROUND;
- } finally {
- Binder.restoreCallingIdentity(callingIdentity);
- }
- //首先在非系统Toast情况下,APP进程在后台并且没有INTERACT_ACROSS_USERS权限,或者APP进程被挂起,都不会显示
- if (!isSystemToast && ((notificationsDisabledForPackage && !appIsForeground)
- || isPackageSuspended)) {
- Slog.e(TAG, "Suppressing toast from package " + pkg
- + (isPackageSuspended ? " due to package suspended."
- : " by user request."));
- return false;
- }
- //上一个自定义toast卡住了
- if (blockToast(callingUid, isSystemToast, isAppRenderedToast,
- isPackageInForegroundForToast(callingUid))) {
- Slog.w(TAG, "Blocking custom toast from package " + pkg
- + " due to package not in the foreground at time the toast was posted");
- return false;
- }
-
- return true;
- }
总结一下:
首先在非系统Toast情况下,APP进程在后台并且没有INTERACT_ACROSS_USERS权限,或者APP进程被挂起,都不会显示Toast。
也就是说,如果有INTERACT_ACROSS_USERS权限就可以在后台显示Toast了。
3.先调用show的Toast一定会先显示吗?
其实这道题问的有点没有意义,一般来说是Toast越先调用show方法会越早显示。
但是也有一些特殊情况,比如两个进程或者两个线程中,A先执行Toast,但是卡住了没有立马执行到显示流程。这时候B线程中也只执行了一次show方法。过了100毫秒,A又执行了一次show。
大体流程如下
- A线程中: toast.show()
- B线程中: toast.show()
- //100号秒后
- A线程中: toast.show()
-
这种情况下说A在B前面也行,说A在B后面也行,最终仍然是A先执行。A的第二次show调用在最终显示之前只是会更新其在NMS中对应的ToastRecord对象中的参数而异。
4.为什么Toast会显示在Activity上面,而不会被Activity覆盖?
这个就涉及到Window的优先级的概念了。3.8小节中有细讲。
5.显示Toast后立马杀掉进程,Toast会立马消失吗?
不会,一样分两种场景:
如果是默认Toast,最终addWindow和removeWindow的操作都在SystemServer进程中,自然杀掉APP进程对Toast的展示没有任何影响。(实际场景验证过)
如果是自定义Toast,显示会在APP进程。3.3小节中有讲到,显示Toast时会调用keepProcessAliveForToastIfNeededLocked方法保证显示Toast的进程不被杀死,所以此时自定义Toast应该还是可以正常显示的。(从代码进行的推论,没有验证过,有热心的朋友可以帮忙验证下)
6.Toast的显示时间一定是4S或者7S嘛?
由于时间计算是在系统侧的,所以只能是4S或者7S,当然并不一定是绝对值。
3.4小节中有讲到,获取到delay延时时间后,如果开启了无障碍辅助功能,首先会通过无障碍修正这个delay时间。其次,Toast的入场和出场动画,也都是单独计算时间的。
所以最终显示时间会略大于4S或者7S。
下面在举两个Toast中经常容易遇到的错误。
7.show的时候提示not attached to window manager 错误解决
具体报错如下:
- java.lang.IllegalArgumentException: View=androidx.recyclerview.widget.RecyclerView{adb693 VFED..... ........ 0,0-1080,1548 #7f0800d1 app:id/recycler} not attached to window manager
- at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:534)
- at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:438)
- at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:157)
- at android.widget.ToastPresenter.addToastView(ToastPresenter.java:303)
- at android.widget.ToastPresenter.show(ToastPresenter.java:231)
- at android.widget.ToastPresenter.show(ToastPresenter.java:214)
- at android.widget.Toast$TN.handleShow(Toast.java:699)
- at android.widget.Toast$TN$1.handleMessage(Toast.java:631)
- at android.os.Handler.dispatchMessage(Handler.java:106)
- at android.os.Looper.loopOnce(Looper.java:201)
- at android.os.Looper.loop(Looper.java:288)
首先我们看addToastView方法:
- if (mView.getParent() != null) {
- mWindowManager.removeView(mView);
- }
如果mView.getParent不为空,则去WindowManager中removeView该view。
然后在看WindowManagerGlobal中findViewLocked方法:
- private int findViewLocked(View view, boolean required) {
- final int index = mViews.indexOf(view);
- if (required && index < 0) {
- throw new IllegalArgumentException("View=" + view + " not attached to window manager");
- }
- return index;
- }
会去mViews中寻找该view,如果找不到,则会抛出上文中的错误。
mViews中保存着APP中所有注册的window的rootView。该View不在mViews中,但又有parentView,则说明该View已经绑定了parentView。
我们在根据下面代码分析可得出结论,setView传入的view是有问题的,是已经绑定了parentView的,这种View自然不能作为rootView。
View=androidx.recyclerview.widget.RecyclerView{adb693 VFED..... ........ 0,0-1080,1548 #7f0800d1 app:id/recycler}
8.hide的时候提示not attached to window manager 错误解决
具体报错如下:
- java.lang.IllegalArgumentException: View=android.widget.LinearLayout{12b02f3 V.E...... ......ID 0,52-629,368} not attached to window manager
- at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:572)
- at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:476)
- at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:144)
- at android.widget.ToastPresenter.hide(ToastPresenter.java:230)
- at android.widget.Toast$TN.handleHide(Toast.java:826)
- at android.widget.Toast$TN$1.handleMessage(Toast.java:746)
- at android.os.Handler.dispatchMessage(Handler.java:106)
- at android.os.Looper.loop(Looper.java:236)
- at com.xxx.NeverCrash$1.run(NeverCrash.java:39)
- at android.os.Handler.handleCallback(Handler.java:938)
- at android.os.Handler.dispatchMessage(Handler.java:99)
- at android.os.Looper.loop(Looper.java:236)
- at android.app.ActivityThread.main(ActivityThread.java:8060)
- at java.lang.reflect.Method.invoke(Native Method)
- at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
- at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
我们可以看流程图中画红圈的部分:

当tryShowToast返回true时,就一定会走到ToastPresenter的hide流程。但是tryShowToast返回true的时候一定会显示成功吗?
上面的流程图中我们可以知道tryShowToast的返回值是有CustomToastRecord的show()方法返回,我们看一下这个方法:
- @Override
- public boolean show() {
- ...
- try {
- callback.show(windowToken);
- return true;
- } catch (RemoteException e) {
- ...
- mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
- return false;
- }
- }
只有binder通讯失败的时候才会返回false,其余都返回true。而最终APP层一定能WindowManager.addView()成功吗?其答案自然是否定的。
我们在看最终显示的ToastPresenter.addToastView方法:
- private void addToastView() {
- if (mView.getParent() != null) {
- mWindowManager.removeView(mView);
- }
- try {
- mWindowManager.addView(mView, mParams);
- } catch (WindowManager.BadTokenException e) {
- return;
- }
- }
也就是说既然添加失败了也没有任何处理。
所以也就是说,如果显示自定义Toast的时候,如果因为某种原因导致最终addView失败,那么等到时间到了,就会导致上面所说的崩溃。这个可以理解为源码中存在的问题,show的时候做了保护,但是hide的时候并未做任何的保护。
那么如何解决这种问题呢?既然是系统的问题,解决起来还是比较麻烦的。
我的想法是这这样的:既然是show的时候try catch了,那么hide的时候能否也进行try catch呢?
我们可以通过反射的时候拿到Toast.TN中的Handler对象,然后通过对其进行代理来实现我们先要的效果。
首先,通过反射拿到Toast.TN中的Handler对象mHandler,然后在创建一个和其同looper的Handler对象,通过反射替换掉原来的mHandler。
自定义的Handler实现伪代码如下:
- Handler oldHandler = null;//反射拿到的原来的handler
- Handler mHandler = new Handler(oldHandler.getLooper(), null) {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case 0: {
- oldHandler.obtainMessage(0, msg.obj).sendToTarget();
- break;
- }
- case 1: {
- try{
- //反射调用handleHide();方法
- }catch (Exception e){
- e.printStackTrace();
- }
- //反射把mNextView = null;
- break;
- }
- case 2: {
- //和1的流程类似
- }
- }
- }
- };
总体来说,因为过多的使用反射,效率不高,但是理论上确实能解决Toast问题问题。