前言:

前些日志QQ群有朋友发了一个Toast的崩溃日志。Toast如此简单的用法怎么会崩溃呢?所以顺便就学习了一下Toast在源码中的实现,不算复杂,但内容挺多的,这里就来分享一下,方便读者。

一.基本使用方式

主要有两种实现方式:

1.最基本的使用方式:

使用方式很简单,直接沟通过静态方法构传入context,显示内容以及显示时长三个参数,构造Toast对象,然后通过show显示。

 Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);toast.show();

2.自定义View的实现方式

这种使用使用方式也很简单,首先构造一个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();

3.使用方式总结

两种使用方式都很简单,区别只是第二种方式多传入了一个自定义View而已。但是为什么要分开来讲呢?因为虽然使用时仅仅只差一步,但是其实现原理是完全不一样的。一个是通过NotificationManagerService去显示的,而另外一个则是APP自身处理的。接下来,我们就依次的讲一下两种使用方式的实现原理。

二.Toast的创建显示流程原理讲解

1.Toast.makeText

这个的实现方式还是比较简单的,最终的生成方式传入4个参数,分别为

Context:绑定的上下文对象

Looper:绑定线程的Looper,可以为空。为空时则默认使用当前线程的looper。PS:每个线程都只能绑定唯一的一个Looper,想了解这一块的可以看我的另外一篇文章:android源码学习-Handler机制及其六个核心点_失落夏天的博客-CSDN博客_安卓开发handler机制

text:显示内容

duration:持续时间。有两种参数:
Toast.LENGTH_LONG:显示时间较长,为3.5S。其3500ms的值定义在NotificationManagerService.LONG_DELAY。

Toast.LENGTH_SHORT:显示时间较短,为2S。其2000ms的值定义在​​​​​​​NotificationManagerService.SHORT_DELAY。

但是真实显示的时间,却不是3.5和2S,实际显示时间会比这两个时间更长一些,这个后面会讲。

最终生成一个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的显示位置。一般是局中,靠下两种。

2.Toast.show()方法

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

三.Toast显示的完整流程

3.1 Service中接收

上文讲到通过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*/@Overridepublic 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也会走到这个方法)。

3.2 enqueueToast方法中处理队列逻辑

我们都知道,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<ToastRecord> list = mToastQueue;int len = list.size();for (int i=0; i<len; 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中设置的2S或者3.5S。

另外还要流出来进场和出场的动画时间,所以最终的延时时间会比2S或者3.5S要长。这一块的逻辑其实就是删除Toast的,所以,这里的延时时间变长了,就会导致最终的实际时间要比要比设置值更长一些。

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中的注册的WindowTokenmWindowManagerInternal.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类型的实现:

@Overridepublic 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) { }}}

这里的mBar其实是一个binder的引用,其server的实现在SystemUI进程中,实现类是CommandQueue。所以,最终会转交到CommandQueue的showToast方法中进行处理:

 @Overridepublic 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.ToastUIjava类中。

3.6 ToastUI.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 toastsmPresenter.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 hiddenmToastOutAnimatorListener.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 donehideCurrentToast(showToastRunnable);} else {// else, show this next toast immediatelyshowToastRunnable.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参数,就是这个来控制最终的显示时间的,当然,这个时间是显示的最长时间,实际情况下,3.4小节中有讲到,在延时时间到了最后,会发通知销毁Toast,所以,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进程,以及ToastPresenter所在的SystemUI进程,都不是APP进程,所以如果在显示了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 necessaryhandleHide();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() {@Overridepublic 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_USERSfinal 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 managerat 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 managerat 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()方法返回,我们看一下这个方法:

 @Overridepublic 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;//反射拿到的原来的handlerHandler mHandler = new Handler(oldHandler.getLooper(), null) {@Overridepublic 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问题问题。

android源码学习-Toast实现原理讲解相关推荐

  1. 【Android 源码学习】Zygote启动原理

    Android 源码学习 Zygote启动原理 望舒课堂 Zygote进程启动原理学习记录整理. Zygote简介 Zygote是进程在init进程启动时创建的,进程本身是app_process,来源 ...

  2. 【Android 源码学习】SystemServer启动原理

    Android 源码学习 SystemServer启动原理 望舒课堂 SystemServer进程启动原理学习记录整理. 参考文章: Android系统启动流程(三)解析SyetemServer进程启 ...

  3. Android源码学习之浅析SystemServer脉络

    在之前的博文中<Android源码学习之如何创建使用JNI>和<Android源码学习之如何使用eclipse+NDK>中,浅谈了如何创建使用JNI和如何利用NDK工具开发创建 ...

  4. 【Android 源码学习】 init启动

    目录 Android 源码学习 init启动 从main.cpp开始 init.cpp 部分逻辑 init启动zygote 属性服务 总结 Android 源码学习 init启动 Android 11 ...

  5. 【Android 源码学习】系统架构和启动流程

    Android 源码学习 系统架构和启动流程 望舒课堂 学习记录整理.以及以下参考文章的整理汇总.便于我个人的学习记录. 感谢IngresGe,Gityuan的精彩文章.为我们这些初探android系 ...

  6. 【Android 源码学习】SharedPreferences 源码学习

    第一章:SharedPreferences 源码学习 文章目录 第一章:SharedPreferences 源码学习 Android SharedPreferences的缺陷 MMKV.Jetpack ...

  7. Android源码学习之工厂方法模式应用

    主要内容: 工厂方法模式定义 工厂方法模式优势 工厂方法模式在Android源码中的应用 一.工厂方法模式定义 工厂方法模式定义: Define an interface for creating a ...

  8. Android源码学习以及在工作中的应用01-TextView

    有人说种下一棵树最好的时间是十年前,其次是现在.我已经浪费了整整十年,所以从现在起,脚踏实地,静下心来学习,一切从头开始.期望十年后的自己,无怨无悔. 我们在自动化测试的工作中,有一个这样的场景需求. ...

  9. Android源码学习之handler

    前言 是滴!我又来了...今天来讲讲老少皆宜的大名鼎鼎的handler.是的,想必handler这个东西已经被讨论的天花乱坠了,也经常被我们用在实际开发中,但是其中很多细节知识还是值得我们去学习深究的 ...

最新文章

  1. 剑桥大学:机器学习模型部署都有哪些坑?
  2. VC 6.0 + SP6 下载 及 安装详细说明
  3. 46个PPT下载丨QCon 2019年全球软件开发大会PPT
  4. 带你探索CPU调度的奥秘
  5. ionic 集成websocket
  6. 在的微型计算机系统中 外设可和,微机原理第七章题库
  7. VB6 GDI+ 入门教程[7] Graphics 其他内容
  8. 生成一定范围内的互不相同的随机数的方法比较
  9. 【多线程经典实例】实现一个线程安全的单例模式
  10. python脚本微博自动转发抽奖_微博自动转发抽奖软件
  11. C++银行账户管理程序2
  12. 原生汇率计算器系统源代码
  13. 30天突破英语口语!(MP3版)
  14. ec----------
  15. 实验3-11 求一元二次方程的根
  16. 公司合伙人股权的进入和退出机制
  17. 多线程:模仿火车站售票
  18. 史上最全的HTML、CSS知识点总结,浅显易懂。适合入门新手
  19. 解决mac系统向日葵远控无法被远程控制问题(白屏)
  20. 对于一个网络营销新手,需要掌握哪些网络营销基础知识

热门文章

  1. 根据两点经纬度,计算距离、方位角
  2. linux vim使用 详解,vim使用详解
  3. 电大本科计算机考试英语成绩查询,电大考试平台
  4. Vue中获取滚动条的高度的方法
  5. 如何将FLAC格式转为MP3格式
  6. Python浮点数数组求和结果不精确问题
  7. Synchonized 实现原理
  8. 图片加载框架之Gilde详细讲解(一)
  9. 图解红黑树原理,再也不怕面试被问到,不详细算我输!
  10. 我穿越了,未来与历史任我畅游,智慧博物馆新技术实践案例应用分享