前言:

前些日志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
         */
        @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也会走到这个方法)。

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中的注册的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) { }
            }
        }

这里的mBar其实是一个binder的引用,其server的实现在SystemUI进程中,实现类是CommandQueue。所以,最终会转交到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.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 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参数,就是这个来控制最终的显示时间的,当然,这个时间是显示的最长时间,实际情况下,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 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问题问题。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐