千家信息网

Android的Toast问题怎么解决

发表于:2026-11-04 作者:千家信息网编辑
千家信息网最后更新 2026年11月04日,这篇"Android的Toast问题怎么解决"文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看
千家信息网最后更新 2026年11月04日Android的Toast问题怎么解决

这篇"Android的Toast问题怎么解决"文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇"Android的Toast问题怎么解决"文章吧。

1.回顾

  1. Toast 系统如何构建窗口(通过系统服务NotificationManager来生成系统窗口)

  2. Toast 异常出现的原因(系统调用 Toast的时序紊乱)

2.解决思路

基于第一篇的知识,我们知道,Toast 的窗口属于系统窗口,它的生成和生命周期依赖于系统服务 NotificationManager。一旦 NotificationManager 所管理的窗口生命周期跟我们本地的进程不一致,就会发生异常。那么,我们能不能不使用系统的窗口,而使用自己的窗口,并且由我们自己控制生命周期呢?事实上, SnackBar 就是这样的方案。不过,如果不使用系统类型的窗口,就意味着你的Toast 界面,无法在其他应用之上显示。(比如,我们经常看到的一个场景就是你在你的应用出调用了多次 Toast.show函数,然后退回到桌面,结果发现桌面也会弹出 Toast,就是因为系统的 Toast 使用了系统窗口,具有高的层级)不过在某些版本的手机上,你的应用可以申请权限,往系统中添加 TYPE_SYSTEM_ALERT 窗口,这也是一种系统窗口,经常用来作为浮层显示在所有应用程序之上。不过,这种方式需要申请权限,并不能做到让所有版本的系统都能正常使用。
如果我们从体验的角度来看,当用户离开了该进程,就不应该弹出另外一个进程的 Toast 提示去干扰用户的。Android 系统似乎也意识到了这一点,在新版本的系统更新中,限制了很多在桌面提示窗口相关的权限。所以,从体验上考虑,这个情况并不属于问题。

"那么我们可以选择哪些窗口的类型呢?"

  1. 使用子窗口: 在 Android 进程内,我们可以直接使用类型为子窗口类型的窗口。在 Android 代码中的直接应用是 PopupWindow 或者是 Dialog 。这当然可以,不过这种窗口依赖于它的宿主窗口,它可用的条件是你的宿主窗口可用

  2. 采用 View 系统: 使用 View 系统去模拟一个 Toast 窗口行为,做起来不仅方便,而且能更加快速的实现动画效果,我们的 SnackBar 就是采用这套方案。这也是我们今天重点讲的方案


"如果采用 View 系统方案,那么我要往哪个控件中添加我的 Toast 控件呢?"

在Android进程中,我们所有的可视操作都依赖于一个 Activity 。 Activity 提供上下文(Context)和视图窗口(Window) 对象。我们通过 Activity.setContentView 方法所传递的任何 View对象 都将被视图窗口( Window) 中的 DecorView 所装饰。而在 DecorView 的子节点中,有一个 id 为 android.R.id.content 的 FrameLayout 节点(后面简称 content 节点) 是用来容纳我们所传递进去的 View 对象。一般情况下,这个节点占据了除了通知栏的所有区域。这就特别适合用来作为 Toast 的父控件节点。

"我什么时机往这个content节点中添加合适呢?这个 content 节点什么时候被初始化呢?"

根据不同的需求,你可能会关注以下两个时机:

  1. Content 节点生成

  2. Content 内容显示

实际我们只需要将我们的 Toast 添加到 Content 节点中,只要满足第一条即可。如果你是为了完成性能检测,测量或者其他目的,那么你可能更关心第二条。 那么什么情况下 Content 节点生成呢?刚才我们说了,Content 节点包含在我们的 DecorView 控件中,而 DecorView 是由 Activity 的 Window对象所持有的控件。Window 在 Android 中的实现类是 PhoneWindow,(这部分代码有兴趣可以自行阅读) 我们来看下源码:

//code PhoneWindow.java@Override    public void setContentView(int layoutResID) {        if (mContentParent == null) { //mContentParent就是我们的 content 节点            installDecor();//生成一个DecorView        } else {            mContentParent.removeAllViews();        }        mLayoutInflater.inflate(layoutResID, mContentParent);        final Callback cb = getCallback();        if (cb != null && !isDestroyed()) {            cb.onContentChanged();        }    }

PhoneWindow 对象通过 installDecor 函数生成 DecorView 和 我们所需要的 content 节点(最终会存到 mContentParent) 变量中去。但是, setContentView 函数需要我们主动调用,如果我并没有调用这个 setContentView 函数,installDecor 方法将不被调用。那么,有没有某个时刻,content 节点是必然生成的呢?当然有,除了在 setContentView 函数中调用installDecor外,还有一个函数也调用到了这个,那就是:

//code PhoneWindow.java@Override    public final View getDecorView() {        if (mDecor == null) {            installDecor();        }        return mDecor;}

而这个函数,将在 Activity.findViewById 的时候调用:

//code Activity.javapublic View findViewById(@IdRes int id) {        return getWindow().findViewById(id);}//code Window.javapublic View findViewById(@IdRes int id) {        return getDecorView().findViewById(id);}

因此,只要我们只要调用了 findViewById 函数,一样可以保证 content 被正常初始化。这样我们解释了第一个"就绪"(Content 节点生成)。我们再来看下第二个"就绪",也就是 Android 界面什么时候显示呢?相信你可能迫不及待的回答不是 onResume 回调的时候么?实际上,在 onResume 的时候,根本还没处理跟界面相关的事情。我们来看下 Android 进程是如何处理 resume 消息的:
(注: AcitivityThread 是 Android 进程的入口类, Android 进程处理 resume 相关消息将会调用到 AcitivityThread.handleResumeActivity 函数)

//code AcitivityThread.javavoid handleResumeActivity(...) {    ...    ActivityClientRecord r = performResumeActivity(token, clearHide);    // 之后会调用call onResume    ...    View decor = r.window.getDecorView();    //调用getDecorView 生成 content节点    decor.setVisibility(View.INVISIBLE);    ....    if (r.activity.mVisibleFromClient) {       r.activity.makeVisible();//add to WM 管理    }    ...}//code Activity.java  void makeVisible() {        if (!mWindowAdded) {            ViewManager wm = getWindowManager();            wm.addView(mDecor, getWindow().getAttributes());            mWindowAdded = true;        }        mDecor.setVisibility(View.VISIBLE);    }

Android 进程在处理 resume 消息的时候,将走以下的流程:

  1. 调用 performResumeActivity 回调 Activity 的 onResume 函数

  2. 调用 Window 的 getDecorView 生成 DecorView 对象和 content 节点

  3. 将DecorView纳入 WindowManager (进程内服务)的管理

  4. 调用 Activity.makeVisible 显示当前 Activity

按照上述的流程,在 Activity.onResume 回调之后,才将控件纳入本地服务 WindowManager 的管理中。也就是说, Activity.onResume 根本没有显示任何东西。我们不妨写个代码验证一下:

//code DemoActivity.javapublic DemoActivity extends Activity {   private View view ;    @Override    protected void onCreate( Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        view = new View(this);        this.setContentView(view);    }    @Override    protected void onResume() {        super.onResume();        Log.d("cdw","onResume :" +view.getHeight());// 有高度是显示的必要条件    }}

这里,我们通过在 onResume 中获取高度的方式验证界面是否被绘制,最终我们将输出日志:

D cdw     : onResume :0

那么,界面又是在什么时候完成的绘制呢?是不是在 WindowManager.addView 之后呢?我们在 onResume之后会调用Activity.makeVisible,里面会调用 WindowManager.addView。因此我们在onResume 里post一个消息就可以检测WindowManager.addView 之后的情况:

@Override    protected void onResume() {        super.onResume();        this.runOnUiThread(new Runnable() {            @Override            public void run() {                Log.d("cdw","onResume :" +view.getHeight());            }        });}//控制台输出:01-02 21:30:27.445  2562  2562 D cdw     : onResume :0

从结果上看,我们在 WindowManager.addView 之后,也并没有绘制界面。那么,Android的绘制是什么时候开始的?又是到什么时候结束?

在 Android 系统中,每一次的绘制都是通过一个 16ms 左右的 VSYNC 信号控制的,这种信号可能来自于硬件也可能来自于软件模拟。每一次非动画的绘制,都包含:测量,布局,绘制三个函数。而一般触发这一事件的的动作有:

  1. View 的某些属性的变更

  2. View 重新布局Layout

  3. 增删 View 节点

当调用 WindowManager.addView 将空间添加到 WM 服务管理的时候,会调用一次Layout请求,这就触发了一次 VSYNC 绘制。因此,我们只需要在 onResume 里 post 一个帧回调就可以检测绘制开始的时间:

 @Override    protected void onResume() {        super.onResume();        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {            @Override            public void doFrame(long frameTimeNanos) {                //TODO 绘制开始            }        });    }

我们先来看下 View.requestLayout 是怎么触发界面重新绘制的:

//code View.javapublic void requestLayout() {        ....        if (mParent != null) {            ...            if (!mParent.isLayoutRequested()) {                mParent.requestLayout();            }        }    }

View 对象调用 requestLayout 的时候会委托给自己的父节点处理,这里之所以不称为父控件而是父节点,是因为除了控件外,还有 ViewRootImpl 这个非控件类型作为父节点,而这个父节点会作为整个控件树的根节点。按照我们上面说的委托的机制,requestLayout 最终将会调用到 ViewRootImpl.requestLayout。

//code ViewRootImpl.java@Override    public void requestLayout() {        if (!mHandlingLayoutInLayoutRequest) {            checkThread();            mLayoutRequested = true;            scheduleTraversals();//申请绘制请求        }    }    void scheduleTraversals() {        if (!mTraversalScheduled) {            mTraversalScheduled = true;            ....            mChoreographer.postCallback(                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//申请绘制            ....        }    }

ViewRootImpl 最终会将 mTraversalRunnable 处理命令放到 CALLBACK_TRAVERSAL 绘制队列中去:

 final class TraversalRunnable implements Runnable {        @Override        public void run() {            doTraversal();//执行布局和绘制        }}void doTraversal() {        if (mTraversalScheduled) {            mTraversalScheduled = false;            ...            performTraversals();            ...        }    }

mTraversalRunnable 命令最终会调用到 performTraversals() 函数:

private void performTraversals() {    final View host = mView;    ...    host.dispatchAttachedToWindow(mAttachInfo, 0);//attachWindow    ...    getRunQueue().executeActions(attachInfo.mHandler);//执行某个指令    ...    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);    host.measure(childWidthMeasureSpec, childHeightMeasureSpec);//测量    ....    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//布局    ...    draw(fullRedrawNeeded);//绘制    ...}

performTraversals 函数实现了以下流程:

  1. 调用 dispatchAttachedToWindow 通知子控件树当前控件被 attach 到窗口中

  2. 执行一个命令队列 getRunQueue

  3. 执行 meausre 测量指令

  4. 执行 layout 布局函数

  5. 执行绘制 draw

这里我们看到一句方法调用:

getRunQueue().executeActions(attachInfo.mHandler);

这个函数将执行一个延时的命令队列,在 View 对象被 attach 到 View树之前,通过调用 View.post 函数,可以将执行消息命令加入到延时执行队列中去:

//code View.javapublic boolean post(Runnable action) {        Handler handler;        AttachInfo attachInfo = mAttachInfo;        if (attachInfo != null) {            handler = attachInfo.mHandler;        } else {            // Assume that post will succeed later            ViewRootImpl.getRunQueue().post(action);            return true;        }        return handler.post(action);}

getRunQueue().executeActions 函数执行的时候,会将该命令消息延后一个UI线程消息执行,这就保证了执行的这个命令消息发生在我们的绘制之后:

//code RunQueue.java void executeActions(Handler handler) {            synchronized (mActions) {                ...                for (int i = 0; i < count; i++) {                    final HandlerAction handlerAction = actions.get(i);                    handler.postDelayed(handlerAction.action, handlerAction.delay);//推迟一个消息                }            }        }

所以,我们只需要在视图被 attach 之前通过一个 View 来抛出一个命令消息,就可以检测视图绘制结束的时间点:

//code DemoActivity.java @Override    protected void onResume() {        super.onResume();        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {            @Override            public void doFrame(long frameTimeNanos) {                start = SystemClock.uptimeMillis();                log("绘制开始:height = "+view.getHeight());            }        });    }    @Override    protected void onCreate( Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        view = new View(this);        view.post(new Runnable() {            @Override            public void run() {                log("绘制耗时:"+(SystemClock.uptimeMillis()-start)+"ms");                log("绘制结束后:height = "+view.getHeight());            }        });        this.setContentView(view);    }//控制台输出:01-03 23:39:27.251 27069 27069 D cdw     : --->绘制开始:height = 001-03 23:39:27.295 27069 27069 D cdw     : --->绘制耗时:44ms01-03 23:39:27.295 27069 27069 D cdw     : --->绘制结束后:height = 1232

我们带着我们上面的知识储备,来看下SnackBar是如何做的呢:

3.Snackbar

SnackBar 系统主要依赖于两个类:

  1. SnackBar 作为门面,与业务程序交互

  2. SnackBarManager 作为时序管理器, SnackBar 与 SnackBarManager 的交互,通过 Callback 回调对象进行

SnackBarManager 的时序管理跟 NotifycationManager 的很类似不再赘述

SnackBar 通过静态方法 make 静态构造一个 SnackBar:

public static Snackbar make(@NonNull View view, @NonNull CharSequence text,            @Duration int duration) {        Snackbar snackbar = new Snackbar(findSuitableParent(view));        snackbar.setText(text);        snackbar.setDuration(duration);        return snackbar;    }

这里有一个关键函数 findSuitableParent ,这个函数的目的就相当于我们上面的 findViewById(R.id.content) 一样,给 SnackBar 所定义的 Toast 控件找一个合适的容器:

private static ViewGroup findSuitableParent(View view) {        ViewGroup fallback = null;        do {            if (view instanceof CoordinatorLayout) {                return (ViewGroup) view;            } else if (view instanceof FrameLayout) {                if (view.getId() == android.R.id.content) {//把 `Content` 节点作为容器                    ...                    return (ViewGroup) view;                } else {                    // It's not the content view but we'll use it as our fallback                    fallback = (ViewGroup) view;                }            }            ...        } while (view != null);        // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback        return fallback;    }

我们发现,除了包含 CoordinatorLayout 控件的情况, 默认情况下, SnackBar 也是找的 Content 节点。找到的这个父节点,作为 Snackbar 构造器的形参:

private Snackbar(ViewGroup parent) {        mTargetParent = parent;        mContext = parent.getContext();        ...        LayoutInflater inflater = LayoutInflater.from(mContext);        mView = (SnackbarLayout) inflater.inflate(                R.layout.design_layout_snackbar, mTargetParent, false);        ...    }

Snackbar 将生成一个 SnackbarLayout 控件作为 Toast 控件。最后当时序控制器 SnackBarManager 回调返回的时候,通知 SnackBar 显示,即将 SnackBar.mView 增加到 mTargetParent 控件中去。

这里有人或许会有疑问,这里使用强引用,会不会造成一段时间内的内存泄漏呢?
假如你现在弹了 10 个 Toast ,每个 Toast 的显示时间是 2s 。也就是说你的最后一个 SnackBar 将被 SnackBarManager 持有至少 20s。而 SnackBar 中又存在有父控件 mTargetParent 的强引用。相当于在这20s内, 你的mTargetParent 和它所持有的 Context (一般是 Activity)无法释放

这个其实是不会的,原因在于 SnackBarManager 在管理这种回调 callback 的时候,采用了弱引用。

private static class SnackbarRecord {        final WeakReference callback;    ....}

但是,我们从 SnackBar 的设计可以看出,SnackBar无法定制具体的样式: SnackBar 只能生成 SnackBarLayout 这种控件和布局,可能并不满足你的业务需求。当然你也可以变更 SnackBarLayout 也能达到目的。不过,有了上面的知识储备,我们完全可以写一个自己的 Snackbar。

4.基于Toast的改法

从第一篇文章我们知道,我们直接在 Toast.show 函数外增加 try-catch 是没有意义的。因为 Toast.show 实际上只是发了一条命令给 NotificationManager 服务。真正的显示需要等 NotificationManager 通知我们的 TN 对象 show 的时候才能触发。NotificationManager 通知给 TN 对象的消息,都会被 TN.mHandler 这个内部对象进行处理

//code Toast.java private static class TN {    final Runnable mHide = new Runnable() {// 通过 mHandler.post(mHide) 执行            @Override            public void run() {                handleHide();                mNextView = null;            }        };    final Handler mHandler = new Handler() {                @Override                public void handleMessage(Message msg) {                    IBinder token = (IBinder) msg.obj;                    handleShow(token);// 处理 show 消息                }    };}

在NotificationManager 通知给 TN 对象显示的时候,TN 对象将给 mHandler 对象发送一条消息,并在 mHandler 的 handleMessage 函数中执行。 当NotificationManager 通知 TN 对象隐藏的时候,将通过 mHandler.post(mHide) 方法,发送隐藏指令。不论采用哪种方式发送的指令,都将执行 Handler 的 dispatchMessage(Message msg) 函数:

//code Handler.javapublic void dispatchMessage(Message msg) {        if (msg.callback != null) {            handleCallback(msg);// 执行 post(Runnable)形式的消息        } else {            ...            handleMessage(msg);// 执行 sendMessage形式的消息        }    }

因此,我们只需要在 dispatchMessage 方法体内加入 try-catch 就可以避免 Toast 崩溃对应用程序的影响:

public void dispatchMessage(Message msg) {    try {        super.dispatchMessage(msg);    } catch(Exception e) {}}

因此,我们可以定义一个安全的 Handler 装饰器:

private static class SafelyHandlerWarpper extends Handler {        private Handler impl;        public SafelyHandlerWarpper(Handler impl) {            this.impl = impl;        }        @Override        public void dispatchMessage(Message msg) {            try {                super.dispatchMessage(msg);            } catch (Exception e) {}        }        @Override        public void handleMessage(Message msg) {            impl.handleMessage(msg);//需要委托给原Handler执行        }}

由于 TN.mHandler 对象复写了 handleMessage 方法,因此,在 Handler 装饰器里,需要将 handleMessage 方法委托给 TN.mHandler 执行。定义完装饰器之后,我们就可以通过反射往我们的 Toast 对象中注入了:

public class ToastUtils {    private static Field sField_TN ;    private static Field sField_TN_Handler ;    static {        try {            sField_TN = Toast.class.getDeclaredField("mTN");            sField_TN.setAccessible(true);            sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");            sField_TN_Handler.setAccessible(true);        } catch (Exception e) {}    }    private static void hook(Toast toast) {        try {            Object tn = sField_TN.get(toast);            Handler preHandler = (Handler)sField_TN_Handler.get(tn);            sField_TN_Handler.set(tn,new SafelyHandlerWarpper(preHandler));        } catch (Exception e) {}    }    public static void showToast(Context context,CharSequence cs, int length) {        Toast toast = Toast.makeText(context,cs,length);        hook(toast);        toast.show();    }}

我们再用第一章中的代码测试一下:

public void showToast(View view) {        ToastUtils.showToast(this,"hello", Toast.LENGTH_LONG);        try {            Thread.sleep(10000);        } catch (InterruptedException e) {}}

等 10s 之后,进程正常运行,不会因为 Toast 的问题而崩溃。

以上就是关于"Android的Toast问题怎么解决"这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注行业资讯频道。

0