小小的Toast蕴含大道理(解决关闭通知时原生Toast不显示问题)

目录

一.Toast成员变量

二. Toast显示流程

1. Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration) 

2.show()

3.NotificationManagerService

(1)public void enqueueToast(String pkg, ITransientNotification callback, int duration,int displayId)

(2)void showNextToastLocked()

(3)void scheduleDurationReachedLocked(ToastRecord r)

(4)handleDurationReached((ToastRecord) msg.obj)

(5 )void cancelToastLocked(int index)

四、Toast取消流程

五、原生Toast存在的问题

1.重复创建Toast

2.原生Toast的显示的死循环

3.原生Toast为系统级别的Toast


最近项目中出现一个问题,就是有的手机在关闭系统通知,结果项目中使用的原生Toast在有的手机上竟然不显示了,然后就去查系统源码,发现原来原生的Toast是基于NotificaitionManagerService实现的,难怪有些手机不显示。那些显示的手机厂商应该发现了这个问题,在系统修改了源码。特别记录下这个过程,并且附上可以解决这个问题的源码,供大家参考。

通常我们在使用Toast的时候,都是下面的简单的一行代码就可以解决问题,就可以一个Toast显示。从源码的角度来看下这个Toast是怎么一步一步显示出来的。

Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();

Toast的类本身就仅仅是一个没有任何继承的工具类。通过一个内部类TN管理Toast的添加/移除,主要就是对WindowManager上进行添加/移除Toast的View,NotificationManagerService来控制着在什么时候将View添加到WindowManager中,什么时候将View从WindowManager移除。

public class Toast {
 
     private static class TN extends ITransientNotification.Stub {
    }
}

一.Toast成员变量

TN内部类用来维护一个WindowManager来对Toast上面的View进行添加和移除;

mNextView也就是Toast即将要显示的Toast的View,如果调用setView(),就会将该View赋值到mNextView中。

    //维护着一个WindowManager来添加/移除Toast的View
    final TN mTN;
    @UnsupportedAppUsage
    //延时时间
    int mDuration;
    //调用Toast的时候需要显示的View,初始化的时候就是默认的UI,用户也可以调用setView来重新设置这个View
    View mNextView;

二. Toast显示流程

1. Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration) 

主要是初始化Toast的View,默认的就是一个TextView,然后将mDuration、mNextView赋值到内部管理类TN。

    /**
     * Make a standard toast to display using the specified looper.
     * If looper is null, Looper.myLooper() is used.
     * @hide
     */
    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

     内部类TN本身继承于ITransientNotification.Stub,对Toast的View的进行显示/隐藏的管理,同时NotificationServiceManager可以跨进程访问Toast所在的进程。

  private static class TN extends ITransientNotification.Stub {

  }

 ITransientNotification.aidl文件里面其实就是show()/hide()两个方法,而内部类TN实现了Toast的show()/hide()。

/** @hide */
oneway interface ITransientNotification {
    void show();
    void hide();
}

 而这两个方法中就是就是发送SHOW和HIDE消息给到Handler。

        /**
         * schedule handleShow into the right thread
         */
        @Override
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

        /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.obtainMessage(HIDE).sendToTarget();
        }

从TN的构造方法中可以看到,我们可以传进来一个线程的Looper对象,我们就可以在当前线程中 显示Toast。也就是说可以在线程显示Toast,但是必须传入一个当前子线程的Looper对象。

          if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
           

另外注意的是,在子线程中显示Toast的时候,要注意要将Toast的show()方法要在 Looper.loop()之前,否则在Toast中的Handler无法循环取队列中的消息,该Toast就无法显示。

        new Thread() {
            @Override
            public void run() {
                super.run();
                Looper.prepare();
                Toast.makeText(ToastActivity.this,"22",Toast.LENGTH_SHORT).show();
                Looper.loop();
            }
        }.start();

在内部类TN中的Handler里面就是去处理不同的消息。最终这些消息的发送是在NotificationManagerService中进行维护的。

mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
        }

从源码中也可以看到handleShow()/handleHide()就是将Toast中的View从WindowManager中添加/移除。在 handleShow()方法中主要提一个 if (mView != mNextView) 这个逻辑判断,这个mNextView是开发者在调用setView()之后设置的,如果开发者没有主动调用该方法,那么就直接就是在调用makeText()实例化Toast的布局View。

  public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            // If a cancel/hide is pending - no need to show - at this point
            // the window token is already invalid and no need to do any work.
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                // 省略一些代码。。。。主要就是设置mWM中的一些参数
                //。。。。。。。。。
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                //将mView添加到WindowManager上面
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

在 handleHide()中也就是将mView移除,并置空。所以mNextView就是即将显示的View,然后用mView来保存这个View,显示出来。

 @UnsupportedAppUsage
        public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                // note: checking parent() just to make sure the view has
                // been added...  i have seen cases where we get here when
                // the view isn't yet added, so let's try not to crash.
                //将mView从WindowManager移除
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeViewImmediate(mView);
                }
                // Now that we've removed the view it's safe for the server to release
                // the resources.
                try {
                    getService().finishToken(mPackageName, this);
                } catch (RemoteException e) {
                }

                mView = null;
            }
        }

2.show()

 第一个makeText()方法是实例化Toast对象,那么show()就是将Toast显示出来,我们从源码中看到该主要是通过NotificationManagerService来进行管理所加入的Toast队列

public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;
        final int displayId = mContext.getDisplayId();

        try {
            service.enqueueToast(pkg, tn, mDuration, displayId);
        } catch (RemoteException e) {
            // Empty
        }
    }

我们从源码中可以看到此时已经通过NotificationManagerService来进行显示Toast。

3.NotificationManagerService

(1)public void enqueueToast(String pkg, ITransientNotification callback, int duration,int displayId)

该方法主要的逻辑就是:首先会判断加入的这个Toast,如果之前已经加入过mToastQueue中,则直接更新该Toast的duration(这种情况出现在如果在使用的时候,先把Toast实例化,然后通过该实例在不同的地方分别调用show()的时候,那么此时传入的Toast就会走该逻辑),否则将该Toast加入到mToastQueue中。

当将Toast加入到mToastQueue的时候,也会去判断是否为系统Toast,如果不是系统Toast,那么对应的该应用下,最多同时可加入25个Toast。

最后判断mToastQueue集合中的Toast是否为第一个,只有是集合中的第一个元素的时候,才会显示该Toast。

 @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration,
                int displayId)
        {

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not enqueuing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }

           //。。。。省略部分非重要逻辑。。。。
           //mToastQueue维护着加入到队列中的所有Toast的集合
            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    // ToastRecord 是对Toast的封装、含有Toast的所在的包名、延时时间等
                    ToastRecord record;
                    //找到该Toast对应的索引值
                    int index = indexOfToastLocked(pkg, callback);
                    // If it's already in the queue, we update it in place, we don't
                    // move it to the end of the queue.
                    //(1)如果该Toast已经存在在队列中,则只更新Toast显示的时间
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                    //(2)说明该Toast没有被加入到队列中,后面的逻辑就是将Toast加入到Toast队列 
                    中,并显示第一个Toast
                    //(2.1)如果不是系统的Toast,那么每个应用下只能加入MAX_PACKAGE_NOTIFICATIONS个Toast,超过这个数量之后则不在显示
                        if (!isSystemToast) {
                            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_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

                        (2.2)把符合条件的将Toast封装成ToastRecord,并且加入到mToastQueue中
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, displayId);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token,
                                displayId);
                        mToastQueue.add(record);
                        //(2.3)把当前的索引值index指向刚加入的这个Toast的位置
                        index = mToastQueue.size() - 1;
                        keepProcessAliveIfNeededLocked(callingPid);
                    }
                    // If it's at index 0, it's the current toast.  It doesn't matter if it's
                    // new or just been updated.  Call back and tell it to show itself.
                    // If the callback fails, this will remove it from the list, so don't
                    // assume that it's valid after this.
                    //(3)如果刚加入的这个Toast恰好是该队列中的第一个,则将该Toast显示出来
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

那么现在就有一个问题了,那如果我当前Toast没有显示完的时候,又多次调用

Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();

那么此时该mToastQueue集合中 已经有多个Toast,那么其他的Toast怎么显示呢?带着这个疑问,去看下面的代码。

(2)void showNextToastLocked()

从上面的方法中可以看到此时如果此时mToastQueue有一个Toast的话,就会调用该方法来显示Toast。

 void showNextToastLocked() {
       //(1)取出该Toast封装成的对象ToastRecord
        ToastRecord record = mToastQueue.get(0);
        //这里使用的是一个无限循环,我觉得是有点浪费资源的,不知道源码在写的时候采用这种方式有什么好处,所以在自定义Toast的时候,已经将该逻辑改掉了。
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                //(2)调用该Toast的内部类TN中的show()显示该Toast
                record.callback.show(record.token);
                //(3)这个就是向NotificationManagerService中维护的Handler中发送duration消息来隐藏 
                 该Toast
                scheduleDurationReachedLocked(record);
                return;
            } catch (RemoteException e) {
                Slog.w(TAG, "Object died trying to show notification " + record.callback
                        + " in package " + record.pkg);
                // remove it from the list and let the process die
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveIfNeededLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

(3)void scheduleDurationReachedLocked(ToastRecord r)

该方法的代码逻辑很简单,就是根据Toast.LENGTH_LONG还是Toast.LENGTH_SHORT来得到对应的延时时间发送到Handler对象中来隐藏Toast

    private void scheduleDurationReachedLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
        //(1)根据Toast.LENGTH_LONG还是Toast.LENGTH_SHORT来获得对应的延时时间
        int delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        // Accessibility users may need longer timeout duration. This api compares original delay
        // with user's preference and return longer one. It returns original delay if there's no
        // preference.
        delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
                AccessibilityManager.FLAG_CONTENT_TEXT);
        //(2)发送延时消息来来隐藏Toast
        mHandler.sendMessageDelayed(m, delay);
    }

那么就看下Handler中对应的内容

(4)handleDurationReached((ToastRecord) msg.obj)

这里的逻辑就是找到对应的Toast的索引值,然后调用 cancelToastLocked(index)方法将该Toast隐藏。

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

(5 )void cancelToastLocked(int index)

主要就是找到对应的ToastRecord,回调到Toast内部类中的hide()方法来隐藏Toast。然后从mToastQueue队列中将Toast移除,将该Toast对应的消息从Handler中移除,最后判断下mToastQueue集合中是否有未显示的Toast,如果还有,则重复第2个方法,依次将集合中的第0个Toast显示完。这里也就回答了在第一个方法enqueueToast()中的疑问,完成mToastQueue集合中的所有的Toast依次显示完的逻辑。

 void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        //(1)找到对应的ToastRecord,然后调用Toast中的hide()
        try {
            record.callback.hide();
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to hide notification " + record.callback
                    + " in package " + record.pkg);
            // don't worry about this, we're about to remove it from
            // the list anyway
        }
        //(2)将该Toast从队列中移除
        ToastRecord lastToast = mToastQueue.remove(index);
        mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
                lastToast.displayId);
        // We passed 'false' for 'removeWindows' so that the client has time to stop
        // rendering (as hide above is a one-way message), otherwise we could crash
        // a client which was actively using a surface made from the token. However
        // we need to schedule a timeout to make sure the token is eventually killed
        // one way or another.
        //(3)将该Toast对应的消息从Handler中移除
        scheduleKillTokenTimeout(lastToast);

        keepProcessAliveIfNeededLocked(record.pid);
        //(4)如果集合中仍有Toast还没有显示完,那么就在重复第2个方法进行依次显示集合中的第0个元素
        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();
        }
    }

四、Toast取消流程

如果我们在使用Toast的时候,需要在未显示完的时候就取消该Toast,这个时候就需要调用到cancel(),其实最终还是调用到内部管理类TN的cancel()方法

    /**
     * Close the view if it's showing, or don't show it if it isn't showing yet.
     * You do not normally have to call this.  Normally view will disappear on its own
     * after the appropriate duration.
     */
    public void cancel() {
        mTN.cancel();
    }

TN中的cancel()方法最终就是发送一个CANCEL消息到Handler,具体还是到NotificationManagerService中的canelToast()。

                    handleHide();
                    // Don't do this in handleHide() because it is also invoked by
                     // handleShow()
                    mNextView = null;
                     try {
                          getService().cancelToast(mPackageName, TN.this);
                     } catch (RemoteException e) {
                       }

从NotificationManagerService中可以看到和前面的Toast在延时之后隐藏Toast的逻辑是一致的,只不过前面的是在延时时间到了之后,去调用 cancelToastLocked(index)方法,而这里是调用cancel()方法的时候,即时就调用cancelToastLocked(index)方法。

  @Override
        public void cancelToast(String pkg, ITransientNotification callback) {
            Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback);

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not cancelling notification. pkg=" + pkg + " callback=" + callback);
                return ;
            }

            synchronized (mToastQueue) {
                long callingId = Binder.clearCallingIdentity();
                try {
                    int index = indexOfToastLocked(pkg, callback);
                    if (index >= 0) {
                        cancelToastLocked(index);
                    } else {
                        Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
                                + " callback=" + callback);
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

五、原生Toast存在的问题

1.重复创建Toast

通常我们在用Toast的时候,都会直接调用下面一行代码来显示Toast,从我们在第二部分分析的显示流程中我们可以看到:

Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();

每调用一次这行代码,都会实例化一个Toast,然后加入到NotificationServiceManager的mToastQueue队列中。如果恰好在点击按钮时调用这行代码,很容易会多次调用这行代码,引起重复创建Toast。有时候项目中为了避免重复创建Toast,所以通常会创建一个Toast实例,全局调用这一个Toast实例,例如:

private static Toast toast;
public static void showToast(Context context, String content) {
    if (toast == null) {
        toast = Toast.makeText(context, content, Toast.LENGTH_SHORT);
    } else {
        toast.setText(content);
    }
    toast.show();
}

上面的这行代码其实是会有内存泄漏的问题。例如当这个Toast在显示的时候,会持有Activity对象,当还未消失的时候,关闭了该Activity,就会导致Activity对象无法回收,引起Activity的内存泄漏。所以针对这个问题,已经在自定义的Toast中进行了改进。从源码中可以看到,每次显示的时候,其实都是取了mToastQueue中的第0个元素来显示,直到显示完才将该元素从集合中删除,那么我们完全可以在加入Toast之前,先去判断下该Toast的显示的文字内容与当前的Toast的文字内容是否一致,如果一致的话,可以先不加入到mToastQueue队列中。在自定义的代码(代码路径链接  GitHub 地址为https://github.com/wenjing-bonnie/toast.git)中已针对这点做出了优化。具体在PowerfulToastManagerService中:

    protected void enqueueToast(PowerfulToast toast, String content, ITransientPowerfulToast callBack, int duration) {
            //。。。。。。省略其他代码
            //如果与正在显示的Toast的内容一致,则不将该Toast加入到Toast队列中;
            //(1)恰好该workHandler的延时MESSAGE_DURATION_REACHED到了在执行remove操作的时候,此时为null,会向下加入这个Toast
            //(2)只要这个Toast没有显示完,则取出来的值不为空,则不会加入到显示mToastQueue队列中
            if (mToastQueue != null && !mToastQueue.isEmpty()) {
                PowerfulToastRecord curRecord = mToastQueue.get(0);
                if (curRecord != null && content.equals(curRecord.content)) {
                    return;
                }
            }
            //将新增的toast加入到队列中
            record = new PowerfulToastRecord(toast, content, callBack, duration);
            mToastQueue.add(record);
         //。。。。。。省略其他代码
}

2.原生Toast的显示的死循环

在原生的Toast显示的时候,这里取出mToastQueue的第0个元素,然后显示出来到最后消失的时候,这个循环一直在执行,一直到Toast显示完的时候,这个循环一直存在,其实为什么这里不直接使用一个if(record!=null)来进行判断就可以了呢?这个源码之所以采用这种方式有什么好处,暂时没有想到原因。所以在自定义Toast的时候,已经将该逻辑改了,直接使用的就是if(record!=null)来判断。

    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        //这块为什么会要用一个死循环的方式呢?
        while (record != null) {
            
        }
     }

3.原生Toast为系统级别的Toast

原生Toast在显示的时候,设置的WindowManager.LayoutParams的时候,采用的是下面的这种类型,但是对于自定义的Toast的时候,

   params.type = WindowManager.LayoutParams.TYPE_TOAST;

WindowManager的类型分为应用Window、子Window、系统Window。应用Window对应的一个Activity,子Window不能单独存在,必须附属到父Window中,而系统Window在使用的时候,必须声明权限。

所以我们在自定义的Toast的不能采用这种类型,因为通知权限在关闭后设置显示的类型为TYPE_TOAST会抛android.view.WindowManager$BadTokenException这个异常。而系统Window的类型,在使用的时候,会提示用户给到相应的权限,这样在用户体验很差,所以只能采用应用Window,那么使用应用Window类型的时候,就会有另外一个问题,如果在Toast没有消失的时候,关闭Activity的时候,会抛出 android.view.WindowLeaked: Activity。所以为了避免这种情况,所以监听Activity的生命周期,在Activity关闭的时候,取消所有mToastQueue中的Toast。所以需要在使用自定义Toast的时候,需要先注册该Toast。

public class PowerfulToastManagerService implements Application.ActivityLifecycleCallbacks {   
 /**
     * 将application传入用来管理Activity的生命周期
     *
     * @param application
     */
    protected void registerToast(Application application) {
        application.registerActivityLifecycleCallbacks(this);
    }
 /**
     * {@link android.app.Application.ActivityLifecycleCallbacks}
     */

    @Override
    public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
    }

    @Override
    public void onActivityStarted(@NonNull Activity activity) {
    }

    @Override
    public void onActivityResumed(@NonNull Activity activity) {
    }

    @Override
    public void onActivityPaused(@NonNull Activity activity) {
        Log.logV(TAG, activity.getClass().getSimpleName() + " , is paused ! " + " , size is " + mToastQueue.size());
        cancelAllPowerfulToast();
    }

    @Override
    public void onActivityStopped(@NonNull Activity activity) {
    }

    @Override
    public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
    }

    @Override
    public void onActivityDestroyed(@NonNull Activity activity) {

    }
}

猜你喜欢

转载自blog.csdn.net/nihaomabmt/article/details/108104146
今日推荐