Android 9.0 Toast源码改变引发的问题
问题描述
在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的显示原理,下文的分析基于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) {@Overridepublic 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对象后调用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()
方法:
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {// ...// 是否为系统级应用final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));// ...ToastRecord record;int index;if (!isSystemToast) {// 不是系统级应用的Toastindex = indexOfToastPackageLocked(pkg);} else {index = indexOfToastLocked(pkg, callback);}if (index >= 0) {// 应用已经显示了Toastrecord = mToastQueue.get(index);record.update(duration);try {record.callback.hide();} catch (RemoteException e) {}record.update(callback);} else {// 应用未显示ToastBinder 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类中来看一下这个方法:
@Override
public void show(IBinder windowToken) {// ...mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}mHandler = new Handler(looper, null) {@Overridepublic 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秒,我们接着来看一下消息的处理:
@Override
public 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()
方法。
@Override
public void hide() {// ...mHandler.obtainMessage(HIDE).sendToTarget();
}mHandler = new Handler(looper, null) {@Overridepublic 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) {@Overridepublic void handleMessage(Message msg) {switch (msg.what) {// ...case HIDE: {handleHide();// 将mNextView置为nullmNextView = null;break;}// ...}}
};public void handleHide() {// ...if (mView != null) {// ...// 将mView置为nullmView = 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()
方法:
@Override
public 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的运行原理分析得不是很详细,可能有些地方分析得不是很准确,如果有不对的地方欢迎大家提出。
Android 9.0 Toast源码改变引发的问题相关推荐
- 【转】Ubuntu 14.04.3上配置并成功编译Android 6.0 r1源码
http://www.linuxidc.com/Linux/2016-01/127292.htm 终于成功把Android 6.0 r1源码的源码编译.先上图,这是在Ubuntu中运行的Android ...
- android 系统源码调试 局部变量值_如何方便快速的整编Android 9.0系统源码?
点击上方"刘望舒",选择"星标" 多点在看,就是真爱! 作者 : 刘望舒 | 来源 :刘望舒的博客地址:http://liuwangshu.cn/fram ...
- Android 8.0系统源码分析--开篇
个人分类: Android框架总结Android源码解析android framework 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/sinat ...
- Android 11.0 Settings源码分析 - 主界面加载
Android 11.0 Settings源码分析 - 主界面加载 本篇主要记录AndroidR Settings源码主界面加载流程,方便后续工作调试其流程. Settings代码路径: packag ...
- Android 4.0.1 源码下载,编译和运行
[牛人分享]Android 4.0.1 源码下载,编译和运行 转自 http://blog.csdn.net/rambo2188/article/details/6943382 ----------- ...
- android 4.0模拟器启动不了,Android 4.0 framework源码修改编译,模拟器运行不起来,求助...
当前位置:编程学习 > wap >> Android 4.0 framework源码修改编译,模拟器运行不起来,求助 我下载编译了android 4.0 ICS的源码,然后能利用编译 ...
- Android 8.0系统源码分析--Camera processCaptureResult结果回传源码分析
相机,从上到下概览一下,真是太大了,上面的APP->Framework->CameraServer->CameraHAL,HAL进程中Pipeline.接各种算法的Node.再往下的 ...
- java toast_详解Android中的Toast源码
Toast源码实现 Toast入口 我们在应用中使用Toast提示的时候,一般都是一行简单的代码调用,如下所示: [java] view plaincopyprint?在CODE上查看代码片派生 ...
- Ubuntu16.04编译Android 6.0系统源码过程简要记录总结
一,安装VMware Workstation,百度网盘下载(内含注册机) 链接: https://pan.baidu.com/s/1wz4hdNQBikTvyUMNokSVYg 提取码: yed7 V ...
最新文章
- 博弈论-囚徒困境与重复囚徒困境的启示
- JavaScript从内容中筛选出手机号码集合
- iOS实现基于VLC播放器的封装效果
- 【python记录】使用ip摄像头 vlc打开rtsp服务 python远程调用摄像头记录过程
- windows命令行无法启动redis_Win10 3分钟简单、快速安装Redis
- 《我爱拼模型》的背景音乐提取(还没有完成)
- html中如何实现选择存储路径的功能_Tomcat 路由请求的实现 Mapper
- 【mysql】悲观锁和乐观锁的实现原理
- 被逼至“盗版合法化”,俄罗斯要把 RuTracker 放出来了?
- 01-路由跳转 安装less this.$router.replace(path) 解决vue/cli3.0语法报错问题
- vue视频保存不下来_女子直播吃章鱼被“反杀”!拔不下来了,视频超痛……
- Ubuntu下配置JDK
- 网络调试助手无法连接tcp服务器,W5500 TCP 客户端网络调试助手连不上
- Linux C程序实现查看文件夹大小
- pcb板生产的工艺流程有哪些?
- Linux---带你区分根目录 和 家目录
- Linux命令之expr详解
- Inno一个程序打包安装工具
- 非核心版本的计算机上_软件测试之兼容性测试(上)
- 如何写SCI文章-转自知乎