问题描述
在Android开发中,Toast的重复显示问题很早就有人提出了解决方案,具体做法就是全局使用一个Toast对象,就像下面这样:
private static Toast mToast = null;/** * 显示一个Toast提示 * * @param context context 上下文对象 * @param text toast字符串 * @param duration toast显示时间 */public static void showToast(Context context, String text, int duration) { if (mToast == null) { mToast = Toast.makeText(context, text, duration); } else { mToast.setText(text); mToast.setDuration(duration); } mToast.show();}
相信大多数人的项目中都会有一个类似的工具类,但其实这个方法存在一个问题,在Android 9.0的手机上(我不确定是否都会有这个问题,有的手机厂商可能会定制自己的Toast),当前Toast还未消失时弹出下一个Toast,会导致当前Toast消失,并且下一个Toast也不会显示,之后短时间内弹出的Toast也不会显示,如下图所示:
Toast消失
问题产生的原因
想要弄清楚这个问题产生的原因就要从源码入手了,我们首先来了解一下Toast的显示原理,下文的分析基于Android 9.0(API Level 28)的源码。
1.Toast的显示原理
我们先来看Toast的makeText()
方法,makeText()
有三个重载方法,最终调用的都是下面的方法:
public static Toast makeText(@NonNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration) { // 创建Toast对象 Toast result = new Toast(context, looper); // 加载Toast布局 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); // 给Toast对象的mNextView和mDuration赋值 result.mNextView = v; result.mDuration = duration; return result;}
方法内部首先创建了一个Toast对象,之后加载Toast的布局,将其赋值给Toast的mNextView。接下来我们看一下Toast的构造方法:
public Toast(@NonNull Context context, @Nullable Looper looper) { mContext = context; // 创建TN对象 mTN = new TN(context.getPackageName(), 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);}
Toast的构造方法内部创建了一个TN类型的对象,将其赋值给Toast中的成员变量mTN,我们来看一下这个TN是什么。
private static class TN extends ITransientNotification.Stub { // ... TN(String packageName, @Nullable Looper looper) { final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = com.android.internal.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; mPackageName = packageName; if (looper == null) { looper = Looper.myLooper(); if (looper == null) { // Toast不能创建在没有Looper的线程中 throw new RuntimeException( "Can't toast on a thread that has not called Looper.prepare()"); } } 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(); mNextView = null; break; } case CANCEL: { handleHide(); mNextView = null; try { getService().cancelToast(mPackageName, TN.this); } catch (RemoteException e) { } break; } } } }; // ...}
TN继承自ITransientNotification.Stub,是一个Binder类型,作用肯定就是用于跨进程了。在TN的构造方法中首先设置了Toast窗口的一些属性,包括宽高等等,然后创建了一个Handler对象,将其赋值给TN内部的成员变量mHandler,在创建Handler对象的时候传入了Looper对象,根据上面的判断不难看出Toast不能创建在没有Looper的线程中。关于这个Handler的作用后面会分析,这里先跳过。
到这里Toast对象的创建过程就完成了,用一张图总结一下:
Toast对象创建完成
创建出Toast对象后调用show()
方法Toast就会显示出来了,接下来我们来看Toast的show()
方法。
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; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty }}
show()
方法内部的逻辑还是比较简单的,将Toast的mNextView对象赋值给mTN内部的同名成员变量mNextView,然后通过getService()
方法获取到NotificationManagerService,调用它的enqueueToast()
方法,参数传入了包名、mTN和Toast显示时长。NotificationManagerService和ActivityManagerService类似,是系统的通知服务,这就解释了为什么在有的手机上关掉应用的通知权限会导致Toast不显示,由于这个问题不是本文要研究的重点,网上也有一些相关的文章,这里就不介绍了。
接下来我们来看NotificationManagerService的enqueueToast()
方法:
@Overridepublic void enqueueToast(String pkg, ITransientNotification callback, int duration) { // ... // 是否为系统级应用 final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg)); // ... ToastRecord record; int index; if (!isSystemToast) { // 不是系统级应用的Toast index = indexOfToastPackageLocked(pkg); } else { index = indexOfToastLocked(pkg, callback); } if (index >= 0) { // 应用已经显示了Toast record = mToastQueue.get(index); record.update(duration); try { record.callback.hide(); } catch (RemoteException e) { } record.update(callback); } else { // 应用未显示Toast Binder token = new Binder(); mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY); record = new ToastRecord(callingPid, pkg, callback, duration, token); mToastQueue.add(record); index = mToastQueue.size() - 1; } // ... if (index == 0) { showNextToastLocked(); } // ...}
NotificationManagerService会将每一个Toast封装为ToastRecord对象,并添加到mToastQueue中,mToastQueue的类型是ArrayList。在enqueueToast()
方法中首先会调用indexOfToastPackageLocked()
方法根据传入的包名获取mToastQueue中相应ToastRecord的索引,将返回值赋值给index。
int indexOfToastPackageLocked(String pkg) { 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)) { return i; } } return -1;}
如果index大于或等于0,说明当前应用已经显示了Toast,这里我们先不管这种情况,后面会具体分析。我们直接来看index小于0(等于-1)的情况,这种情况说明应用未显示Toast,此时mToastQueue的size为0,会创建出ToastRecord对象,添加到mToastQueue中,此时mToastQueue的size变为1,因此index被赋值为0,进而调用showNextToastLocked()
方法。
void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { // ... record.callback.show(record.token); scheduleDurationReachedLocked(record); return; }}
showNextToastLocked()
方法取出mToastQueue中的第一个元素,对应着要显示的Toast,接下来进入了一个while循环,循环体内部首先执行了record.callback.show()
,结合之前创建ToastRecord对象的代码不难看出这里的record.callback其实就是enqueueToast()
的callback参数,也就是Toast对象的mTN,因此这里调用的就是TN的show()
方法,我们回到TN类中来看一下这个方法:
@Overridepublic void show(IBinder windowToken) { // ... mHandler.obtainMessage(SHOW, windowToken).sendToTarget();}mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { case SHOW: { IBinder token = (IBinder) msg.obj; handleShow(token); break; } // ... } }};
show()
方法内部其实就是使用TN内部的mHandler发送了一条消息,我们找到mHandler对消息的处理,发现接着调用了handleShow()
方法。
public void handleShow(IBinder windowToken) { // ... if (mView != mNextView) { // ... mView = mNextView; // ... mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); // ... mWM.addView(mView, mParams); // ... }}
handleShow()
方法内部会调用WindowManager的addView()
方法将Toast对应的View添加到Window中,Toast也就显示出来了。
到这里还没完,我们知道Toast一段时间后就会消失,那么Toast的消失是如何控制的呢,我们回到NotificationManagerService的showNextToastLocked()
方法,在调用TN的show()
方法显示出Toast后又调用了scheduleDurationReachedLocked()
方法,我们来看一下这个方法做了什么。
private void scheduleDurationReachedLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r); long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); }
scheduleDurationReachedLocked()
方法内部也是使用Handler发送了一条延时消息,延时的时间由Toast的显示时长决定,Toast.LENGTH_LONG对应的延时时间为LONG_DELAY,为3.5秒;Toast.LENGTH_SHORT对应的延时时间为SHORT_DELAY,为2秒,我们接着来看一下消息的处理:
@Overridepublic void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_DURATION_REACHED: handleDurationReached((ToastRecord) msg.obj); break; // ... }}private void handleDurationReached(ToastRecord record) { // ... int index = indexOfToastLocked(record.pkg, record.callback); if (index >= 0) { cancelToastLocked(index); } // ...}
接着调用了handleDurationReached()
方法,方法内部首先获取当前Toast的索引,然后调用cancelToastLocked()
方法,从方法名上我们也能猜到这个方法就是为了隐藏Toast,结合上面的延时消息,其实差不多就能清楚Toast是如何消失的了,我们来具体看一下cancelToastLocked()
方法。
void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); // ... record.callback.hide(); // ... ToastRecord lastToast = mToastQueue.remove(index); // ... if (mToastQueue.size() > 0) { showNextToastLocked(); }}
cancelToastLocked()
方法内部执行了record.callback.hide()
,和此前的show()
方法类似,这里同样是调用了TN的hide()
方法。
@Overridepublic void hide() { // ... mHandler.obtainMessage(HIDE).sendToTarget();}mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { // ... case HIDE: { handleHide(); mNextView = null; break; } // ... } }};
接下来的步骤其实也和Toast的显示类似,最终会调用到handleHide()
方法。
public void handleHide() { // ... if (mView != null) { // ... mWM.removeViewImmediate(mView); // ... mView = null; }}
handleHide()
方法调用WindowManager的removeViewImmediate()
方法将Toast对应的View从Window移除,Toast也就消失了。回到NotificationManagerService的cancelToastLocked()
方法,在Toast隐藏后会将Toast对应的ToastRecord从mToastQueue中移除,如果此时mToastQueue的size大于0,则接着调用showNextToastLocked()
方法显示下一个Toast。
到这里我们基本上就清楚了Toast的显示原理,不难看出Toast内部的TN对象扮演着重要的作用,Toast的显示和隐藏都是通过TN中的对应方法实现的。
Toast消失的原因
上文已经简单分析了Toast的显示和隐藏过程,下面我们就要回到文章开头提出的问题上,分析一下为什么Toast会消失。在上文enqueueToast()
方法的分析中,我们只分析了index小于0也就是应用未显示Toast的情况,接下来我们来看一下index大于或等于0,对应应用已经显示Toast的情况。
if (index >= 0) { record = mToastQueue.get(index); record.update(duration); try { record.callback.hide(); } catch (RemoteException e) { } record.update(callback);}
首先获取到ToastRecord对象,这个对象就是对应着应用此时显示的Toast,更新它的duration和callback属性,注意这里在更新callback属性之前执行了record.callback.hide()
,根据此前的分析,之后会隐藏当前显示的Toast。之后的过程是一样的,调用showNextToastLocked()
显示Toast。
回到具体场景中,如果全局使用的是一个Toast对象,那么当然TN对象也是共用的,当第一个Toast还未消失时再次调用Toast的show()
方法显示下一个Toast,由于此时Toast对应的ToastRecord对象还未从mToastQueue中移除,因此indexOfToastPackageLocked()
方法获得的index等于0(这里不考虑多个Toast排队等待执行的情况,认为mToastQueue中只有一个ToastRecord对象),会调用到TN的hide()
方法:
mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { // ... case HIDE: { handleHide(); // 将mNextView置为null mNextView = null; break; } // ... } }};public void handleHide() { // ... if (mView != null) { // ... // 将mView置为null mView = null; }}
执行到这里会导致第一个Toast消失,之后调用showNextToastLocked()
方法显示第二个Toast,最终调用到TN的handleShow()
方法:
public void handleShow(IBinder windowToken) { // ... if (mView != mNextView) { // ... mView = mNextView; // ... mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); // ... mWM.addView(mView, mParams); // ... }}
由于所有的Toast都对应一个TN对象,因此此时mView和mNextView均为null,不会执行mWM.addView()
,Toast也就不会显示。
根据此前的分析,当Toast显示后会发送一条延时消息,根据makeText()
方法传入的时长在一段时间间隔后隐藏Toast同时将ToastRecord从mToastQueue中移除,因此如果弹出第二个Toast时第一个Toast已经消失了,那么是可以正常显示的。
看到这里我们基本上就清楚了Toast无法显示的原因,不过想想全局使用一个Toast的方案很早就提出了,有很多人在用,如果一直都存在上述问题,不是早就应该有人提出了吗,我们不妨与其他版本的Toast源码对比一下。以Android 8.0(API Level 28)为例,我们来看一下NotificationManagerService的enqueueToast()
方法:
@Overridepublic void enqueueToast(String pkg, ITransientNotification callback, int duration) { // ... final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg)); // ... ToastRecord record; int index = indexOfToastLocked(pkg, callback); if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { if (!isSystemToast) { // 限制一个应用弹出的Toast上限 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; } } } } Binder token = new Binder(); mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY); record = new ToastRecord(callingPid, pkg, callback, duration, token); mToastQueue.add(record); index = mToastQueue.size() - 1; keepProcessAliveIfNeededLocked(callingPid); } if (index == 0) { showNextToastLocked(); }}
对比Android 9.0中的enqueueToast()
方法,最大的区别就是当应用已经显示Toast(对应index>=0)时,只调用了ToastRecord的update()
方法,没有调用TN的hide()
方法,因此就不会有第二个Toast不显示的问题。
如何解决问题
综合上面的所有分析,在Android 9.0中,当前应用在已经显示了Toast的情况下会调用TN的hide()
方法,因此我们不需要全局使用一个Toast对象,每次直接执行Toast.makeText().show()
就可以了,由于每一个Toast对象对应不同的TN对象,这样就不会因为mView==mNextView而导致Toast不显示。同时也正是由于调用了hide()
方法,当显示下一个Toast时会隐藏当前正在显示的Toast,因此我们不必再自己处理Toast的重复显示问题,算是官方的优化吧。在Android 9.0以下版本,依然可以采用全局一个Toast的方案解决重复显示问题,不会造成Toast消失的问题。
但是还没完,正好前些天我的手机升级到了Android 10版本,我发现上文分析的Toast消失问题又不存在了,于是点开源码看了看,这不看不知道,一看吓一跳,NotificationManagerService中的enqueueToast()
方法竟然又改回去了:
可以看出Android 10.0中又去掉了record.callback.hide()
这行代码,因此表现上和Android 9.0以下版本一致,我们依然需要使用一个全局Toast来解决重复弹出的问题。我不得不吐槽一下,Android 9.0明明已经优化了Toast的重复弹出问题,为什么Android 10.0又给改回去了,这不是开历史倒车吗,而且这样改来改去对于开发者来说也是非常不友好。当然官方可能也有自己的考虑,由于我能力的不足而没有理解到,如果大家有自己的见解欢迎提出。
好了,最终完善后的示例代码如下:
private static Toast mToast = null;/** * 显示一个toast提示 * * @param context context 上下文对象 * @param text toast字符串 * @param duration toast显示时间 */public static void showToast(Context context, String text, int duration) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { Toast.makeText(context, text, duration).show(); } else { if (mToast == null) { mToast = Toast.makeText(context, text, duration); } else { mToast.setText(text); mToast.setDuration(duration); } mToast.show(); }}
我们来看一下运行效果:
OK,的确解决了Toast消失的问题。
总结
Toast在很多人看来可能都是再简单不过的了,每个项目中都会用,使用起来也很简单,这次算是我第一次了解Toast的原理,通过查阅相关文章,了解到Toast在使用时还存在一些需要注意问题,比如关闭通知权限导致Toast不显示、Android 7.1上Toast抛出BadTokenException异常等等问题,这里就不介绍了,可以自行了解一下。
限于个人水平的原因。关于Toast的运行原理分析得不是很详细,可能有些地方分析得不是很准确,如果有不对的地方欢迎大家提出。
最后编辑于
:
©
著作权归作者所有,转载或内容合作请联系作者
人面猴
序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
沈念sama阅读 174,493评论 5赞 416
死咒
序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
沈念sama阅读 73,611评论 2赞 334
救了他两次的神仙让他今天三更去死
文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
开封第一讲书人阅读 123,360评论 0赞 288
道士缉凶录:失踪的卖姜人
文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
开封第一讲书人阅读 47,406评论 0赞 245
港岛之恋(遗憾婚礼)
正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
茶点故事阅读 56,176评论 3赞 325
恶毒庶女顶嫁案:这布局不是一般人想出来的
文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
开封第一讲书人阅读 42,743评论 1赞 241
城市分裂传说
那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
沈念sama阅读 33,832评论 3赞 355
双鸳鸯连环套:你想象不到人心有多黑
文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
开封第一讲书人阅读 32,406评论 0赞 228
万荣杀人案实录
序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
沈念sama阅读 36,439评论 1赞 269
护林员之死
正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
茶点故事阅读 32,120评论 2赞 275
白月光启示录
正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
茶点故事阅读 33,773评论 1赞 288
活死人
序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
沈念sama阅读 29,889评论 3赞 284
日本核电站爆炸内幕
正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
茶点故事阅读 35,050评论 3赞 279
男人毒药:我在死后第九天来索命
文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
开封第一讲书人阅读 27,059评论 0赞 11
一桩弑父案,背后竟有这般阴谋
文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
开封第一讲书人阅读 28,223评论 1赞 232
情欲美人皮
我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
沈念sama阅读 38,151评论 2赞 309
代替公主和亲
正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
茶点故事阅读 37,849评论 2赞 313
推荐阅读更多精彩内容
IPC在Toast中的应用
1、Toast概念和问题的引出 Toast 中文名"土司",应该算是 Android 使用频率很高的一个 widg...
未见哥哥阅读 758评论 0赞 5
简单源码分析之小小的Toast
前言:toast再常见不过,但是一个小小的toast居然内有乾坤,呵(w)呵(t)呵(f) 源码如下: publi...
super超_9754阅读 1,234评论 0赞 0
android Toast 吐司 源码分析
什么是土司(Toast)? Toast是Android系统提供的一种非常好的提示方式,在程序中可以使用它将一些短小...
一航jason阅读 1,035评论 0赞 1
Window的创建
前言 上篇文章中讲到, Android中所有视图都是通过Window来呈现的, 如Activity, Dialog...
海之韵Baby阅读 433评论 0赞 1
由禅意想到
禅意的生活是什么,一直以来总是觉得那应该是空灵的,脱离人间烟火气的,但其实那就是在生活中各种细节中与物与环境的一种...
007玩设计的灰太狼阅读 528评论 0赞 0