千家信息网

Android的Toast问题有哪些

发表于:2026-11-02 作者:千家信息网编辑
千家信息网最后更新 2026年11月02日,这篇文章主要讲解了"Android的Toast问题有哪些",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"Android的Toast问题有哪些"吧!1.
千家信息网最后更新 2026年11月02日Android的Toast问题有哪些

这篇文章主要讲解了"Android的Toast问题有哪些",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"Android的Toast问题有哪些"吧!

1. 异常和偶尔不显示的问题

当你在程序中调用了 Toast 的 API,你可能会在后台看到类似这样的 Toast 执行异常:

android.view.WindowManager$BadTokenException    Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?    android.view.ViewRootImpl.setView(ViewRootImpl.java:826)    android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:369)    android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)    android.widget.Toast$TN.handleShow(Toast.java:459)

另外,在某些系统上,你没有看到什么异常,却会出现 Toast 无法正常展示的问题。为了解释上面这些问题产生的原因,我们需要先读一遍 Toast 的源码。

2. Toast 的显示和隐藏

首先,所有 Android 进程的视图显示都需要依赖于一个窗口。而这个窗口对象,被记录在了我们的 WindowManagerService(后面简称 WMS) 核心服务中。WMS 是专门用来管理应用窗口的核心服务。当 Android 进程需要构建一个窗口的时候,必须指定这个窗口的类型。 Toast 的显示也同样要依赖于一个窗口, 而它被指定的类型是:

public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//系统窗口

可以看出, Toast 是一个系统窗口,这就保证了 Toast 可以在 Activity 所在的窗口之上显示,并可以在其他的应用上层显示。那么,这就有一个疑问:

"如果是系统窗口,那么,普通的应用进程为什么会有权限去生成这么一个窗口呢?"

实际上,Android 系统在这里使了一次 "偷天换日" 小计谋。我们先来看下 Toast 从显示到隐藏的整个流程:

// code Toast.javapublic void show() {        if (mNextView == null) {            throw new RuntimeException("setView must have been called");        }        INotificationManager service = getService();//调用系统的notification服务        String pkg = mContext.getOpPackageName();        TN tn = mTN;//本地binder        tn.mNextView = mNextView;        try {            service.enqueueToast(pkg, tn, mDuration);        } catch (RemoteException e) {            // Empty        }    }

我们通过代码可以看出,当 Toast 在 show 的时候,将这个请求放在 NotificationManager 所管理的队列中,并且为了保证 NotificationManager 能跟进程交互, 会传递一个 TN 类型的 Binder 对象给 NotificationManager 系统服务。而在 NotificationManager 系统服务中:

//code NotificationManagerServicepublic void enqueueToast(...) {    ....    synchronized (mToastQueue) {                    ...                    {                        // Limit the number of toasts that any given package except the android                        // package can enqueue.  Prevents DOS attacks and deals with leaks.                        if (!isSystemToast) {                            int count = 0;                            final int N = mToastQueue.size();                            for (int i=0; i= MAX_PACKAGE_NOTIFICATIONS) {                                         //上限判断                                         return;                                     }                                 }                            }                        }                        Binder token = new Binder();                        mWindowManagerInternal.addWindowToken(token,                                WindowManager.LayoutParams.TYPE_TOAST);//生成一个Toast窗口                        record = new ToastRecord(callingPid, pkg, callback, duration, token);                        mToastQueue.add(record);                        index = mToastQueue.size() - 1;                        keepProcessAliveIfNeededLocked(callingPid);                    }                    ....                     if (index == 0) {                        showNextToastLocked();//如果当前没有toast,显示当前toast                    }                } finally {                    Binder.restoreCallingIdentity(callingId);                }            }}

(不去深究其他代码的细节,有兴趣可以自行研究,挑出我们所关心的Toast显示相关的部分)

我们会得到以下的流程(在 NotificationManager系统服务所在的进程中):

  • 判断当前的进程所弹出的 Toast 数量是否已经超过上限 MAX_PACKAGE_NOTIFICATIONS ,如果超过,直接返回

  • 生成一个 TOAST 类型的系统窗口,并且添加到 WMS 管理

  • 将该 Toast 请求记录成为一个 ToastRecord 对象

代码到这里,我们已经看出 Toast 是如何偷天换日的。实际上,这个所需要的这个系统窗口 token ,是由我们的 NotificationManager 系统服务所生成,由于系统服务具有高权限,当然不会有权限问题。不过,我们又会有第二个问题:

既然已经生成了这个窗口的 Token 对象,又是如何传递给 Android进程并通知进程显示界面的呢?

我们知道, Toast 不仅有窗口,也有时序。有了时序,我们就可以让 Toast 按照我们调用的次序显示出来。而这个时序的控制,自然而然也是落在我们的 NotificationManager 服务身上。我们通过上面的代码可以看出,当系统并没有 Toast 的时候,将通过调用 showNextToastLocked(); 函数来显示下一个 Toast。

void showNextToastLocked() {        ToastRecord record = mToastQueue.get(0);        while (record != null) {            ...            try {                record.callback.show(record.token);//通知进程显示                scheduleTimeoutLocked(record);//超时监听消息                return;            } catch (RemoteException e) {                ...            }        }    }

这里,showNextToastLocked 函数将调用 ToastRecord的 callback 成员的 show 方法通知进程显示,那么 callback 是什么呢?

final ITransientNotification callback;//TN的Binder代理对象

我们看到 callback 的声明,可以知道它是一个 ITransientNotification 类型的对象,而这个对象实际上就是我们刚才所说的 TN 类型对象的代理对象:

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

那么 callback对象的show方法中需要传递的参数 record.token呢?实际上就是我们刚才所说的NotificationManager服务所生成的窗口的 token。
相信大家已经对 Android 的 Binder 机制已经熟门熟路了,当我们调用 TN 代理对象的 show 方法的时候,相当于 RPC 调用了 TN 的 show 方法。来看下 TN 的代码:

// code TN.javafinal Handler mHandler = new Handler() {            @Override            public void handleMessage(Message msg) {                IBinder token = (IBinder) msg.obj;                handleShow(token);//处理界面显示            }        };@Override        public void show(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "SHOW: " + this);            mHandler.obtainMessage(0, windowToken).sendToTarget();        }

这时候 TN 收到了 show 方法通知,将通过 mHandler 对象去 post 出一条命令为 0 的消息。实际上,就是一条显示窗口的消息。最终,将会调用 handleShow(Binder) 方法:

public void handleShow(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView                    + " mNextView=" + mNextView);            if (mView != mNextView) {                ...                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);                ....                mParams.token = windowToken;                ...                mWM.addView(mView, mParams);                ...            }        }

而这个显示窗口的方法非常简单,就是将所传递过来的窗口 token 赋值给窗口属性对象 mParams, 然后通过调用 WindowManager.addView 方法,将 Toast 中的 mView 对象纳入 WMS 的管理。

上面我们解释了 NotificationManager 服务是如何将窗口 token 传递给 Android 进程,并且 Android 进程是如何显示的。我们刚才也说到, NotificationManager 不仅掌管着 Toast 的生成,也管理着 Toast 的时序控制。因此,我们需要穿梭一下时空,回到 NotificationManager 的 showNextToastLocked() 方法。大家可以看到:在调用 callback.show 方法之后又调用了个 scheduleTimeoutLocked 方法:

record.callback.show(record.token);//通知进程显示 scheduleTimeoutLocked(record);//超时监听消息

而这个方法就是用于管理 Toast 时序:

private void scheduleTimeoutLocked(ToastRecord r)    {        mHandler.removeCallbacksAndMessages(r);        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;        mHandler.sendMessageDelayed(m, delay);    }

scheduleTimeoutLocked 内部通过调用 Handler 的 sendMessageDelayed 函数来实现定时调用,而这个 mHandler 对象的实现类,是一个叫做 WorkerHandler 的内部类:

private final class WorkerHandler extends Handler    {        @Override        public void handleMessage(Message msg)        {            switch (msg.what)            {                case MESSAGE_TIMEOUT:                    handleTimeout((ToastRecord)msg.obj);                    break;                ....            }    }     private void handleTimeout(ToastRecord record)    {        synchronized (mToastQueue) {            int index = indexOfToastLocked(record.pkg, record.callback);            if (index >= 0) {                cancelToastLocked(index);            }        }    }

WorkerHandler 处理 MESSAGE_TIMEOUT 消息会调用 handleTimeout(ToastRecord) 函数,而 handleTimeout(ToastRecord) 函数经过搜索后,将调用 cancelToastLocked 函数取消掉 Toast 的显示:

void cancelToastLocked(int index) {        ToastRecord record = mToastQueue.get(index);            ....            record.callback.hide();//远程调用hide,通知客户端隐藏窗口            ....        ToastRecord lastToast = mToastQueue.remove(index);        mWindowManagerInternal.removeWindowToken(lastToast.token, true);        //将给 Toast 生成的窗口 Token 从 WMS 服务中删除        ...

cancelToastLocked 函数将做以下两件事:

  1. 远程调用 ITransientNotification.hide 方法,通知客户端隐藏窗口

  2. 将给 Toast 生成的窗口 Token 从 WMS 服务中删除

上面我们就从源码的角度分析了一个Toast的显示和隐藏,我们不妨再来捋一下思路,Toast 的显示和隐藏大致分成以下核心步骤:

  1. Toast 调用 show 方法的时候 ,实际上是将自己纳入到 NotificationManager 的 Toast 管理中去,期间传递了一个本地的 TN 类型或者是 ITransientNotification.Stub 的 Binder 对象

  2. NotificationManager 收到 Toast 的显示请求后,将生成一个 Binder 对象,将它作为一个窗口的 token 添加到 WMS 对象,并且类型是 TOAST

  3. NotificationManager 将这个窗口 token 通过 ITransientNotification 的 show 方法传递给远程的 TN 对象,并且抛出一个超时监听消息 scheduleTimeoutLocked

  4. TN 对象收到消息以后将往 Handler 对象中 post 显示消息,然后调用显示处理函数将 Toast 中的 View 添加到了 WMS 管理中, Toast 窗口显示

  5. NotificationManager 的 WorkerHandler 收到 MESSAGE_TIMEOUT 消息, NotificationManager 远程调用进程隐藏 Toast 窗口,然后将窗口 token 从 WMS 中删除

3. 异常产生的原因

上面我们分析了 Toast 的显示和隐藏的源码流程,那么为什么会出现显示异常呢?我们先来看下这个异常是什么呢?

Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?    android.view.ViewRootImpl.setView(ViewRootImpl.java:826)    android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:369)

首先,这个异常发生在 Toast 显示的时候,原因是因为 token 失效。那么 token 为什么会失效呢?

通常情况下,按照正常的流程,是不会出现这种异常。但是由于在某些情况下, Android 进程某个 UI 线程的某个消息阻塞。导致 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息之后,迟迟没有执行。这时候,NotificationManager 的超时检测结束,删除了 WMS 服务中的 token 记录。也就是如图所示,删除 token 发生在 Android 进程 show 方法之前。这就导致了我们上面的异常。我们来写一段代码测试一下:

public void click(View view) {        Toast.makeText(this,"test",Toast.LENGTH_SHORT).show();        try {            Thread.sleep(10000);        } catch (InterruptedException e) {            e.printStackTrace();        }}

我们先调用 Toast.show 方法,然后在该 ui 线程消息中 sleep 10秒。当进程异常退出后我们截取他们的日志可以得到:

12-28 11:10:30.086 24599 24599 E AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@2e5da2c is not valid; is your activity running?12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.view.ViewRootImpl.setView(ViewRootImpl.java:679)12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.widget.Toast$TN.handleShow(Toast.java:434)12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.widget.Toast$TN$2.handleMessage(Toast.java:345)

果然如我们所料,我们复现了这个问题的堆栈。那么或许你会有下面几个疑问:

在 Toast.show 方法外增加 try-catch 有用么?

当然没用,按照我们的源码分析,异常是发生在我们的下一个 UI 线程消息中,因此我们在上一个 ui 线程消息中加入 try-catch 是没有意义的

为什么有些系统中没有这个异常,但是有时候 toast不显示?

我们上面分析的是7.0的代码,而在8.0的代码中,Toast 中的 handleShow发生了变化:

//code handleShow() android 8.0                try {                    mWM.addView(mView, mParams);                    trySendAccessibilityEvent();                } catch (WindowManager.BadTokenException e) {                    /* ignore */                }

在 8.0 的代码中,对 mWM.addView 进行了 try-catch 包装,因此并不会抛出异常,但由于执行失败,因此不会显示 Toast

有哪些原因引起的这个问题?

  1. 引起这个问题的也不一定是卡顿,当你的 TN 抛出消息的时候,前面有大量的 UI 线程消息等待执行,而每个 UI 线程消息虽然并不卡顿,但是总和如果超过了 NotificationManager 的超时时间,还是会出现问题

  2. UI 线程执行了一条非常耗时的操作,比如加载图片,大量浮点运算等等,比如我们上面用 sleep 模拟的就是这种情况

  3. 在某些情况下,进程退后台或者息屏了,系统为了减少电量或者某种原因,分配给进程的 cpu 时间减少,导致进程内的指令并不能被及时执行,这样一样会导致进程看起来"卡顿"的现象

感谢各位的阅读,以上就是"Android的Toast问题有哪些"的内容了,经过本文的学习后,相信大家对Android的Toast问题有哪些这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

0