Toast源码深度分析
目录介绍
1.最简单的创建方法
- 1.1 Toast构造方法
- 1.2 最简单的创建
- 1.3 简单改造避免重复创建
- 1.4 为何会出现内存泄漏
- 1.5 吐司是系统级别的
2.源码分析
- 2.1 Toast(Context context)构造方法源码分析
- 2.2 show()方法源码分析
- 2.3 mParams.token = windowToken是干什么用的
- 2.4 scheduleTimeoutLocked吐司如何自动销毁的
- 2.5 TN类中的消息机制
- 2.6 普通应用的Toast显示数量是有限制的
- 2.7 为何Activity销毁后Toast仍会显示
3.经典总结
- 3.1 判断应用程序获取通知权限是否开启
- 3.2 使用Toast注意事项
- 3.3 Toast的显示和隐藏重点逻辑
- 3.4 Snackbar和Toast比较
4.Toast封装库介绍
- 4.1 能够满足的需求
- 4.2 具有的优势
5.Toast遇到的问题
- 5.1 Toast偶尔报错Unable to add window
- 5.2 Toast运行在子线程问题
- 5.3 Toast如何添加系统窗口的权限
- 5.4 token null is not valid
好消息
- 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
- 链接地址:https://github.com/yangchong2...
- 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!
- Toast封装库项目地址:https://github.com/yangchong2...
- 最简单的创建,简单改造避免重复创建,show()方法源码分析,scheduleTimeoutLocked吐司如何自动销毁的,TN类中的消息机制是如何执行的,普通应用的Toast显示数量是有限制的,用代码解释为何Activity销毁后Toast仍会显示,Toast偶尔报错Unable to add window是如何产生的,Toast运行在子线程问题,Toast如何添加系统窗口的权限等等
- 最简单的使用方法,onCreate(@Nullable Bundle savedInstanceState)源码分析,重点分析弹窗展示和销毁源码,使用中show()方法遇到的IllegalStateException分析
- 显示PopupWindow,注意问题宽和高属性,showAsDropDown()源码,dismiss()源码分析,PopupWindow和Dialog有什么区别?为何弹窗点击一下就dismiss呢?
- 最简单的创建,Snackbar的make方法源码分析,Snackbar的show显示与点击消失源码分析,显示和隐藏中动画源码分析,Snackbar的设计思路,为什么Snackbar总是显示在最下面
- DialogFragment使用中show()方法遇到的IllegalStateException,什么常见产生的?Toast偶尔报错Unable to add window,Toast运行在子线程导致崩溃如何解决?
1.最简单的创建方法
1.1 Toast构造方法
- Toast只会弹出一段信息,告诉用户某某事情已经发生了,过一段时间后就会自动消失。它不会阻挡用户的任何操作。
Toast是没有焦点,而且Toast显示的时间有限,过一定的时间就会自动消失。
- 通过new Toast(context)直接创建,除了将mContext = context,还有一步重要的操作,创建TN,下面会说到……
public Toast(Context context) { mContext = context; mTN = new TN(); 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); }
1.2 最简单的创建
一行代码调用,十分方便,但是这样存在一种弊端。
- 使用中遇到的问题:例如,当点击有些按钮,需要吐司进行提示时;快速连续点击了多次按钮,Toast就触发了多次。系统会将这些Toast信息提示框放到队列中,等前一个Toast信息提示框关闭后才会显示下一个Toast信息提示框。可能导致Toast就长时间关闭不掉了。又或者我们其实已在进行其他操作了,应该弹出新的Toast提示,而上一个Toast却还没显示结束
Toast.makeText(this,"吐司",Toast.LENGTH_SHORT).show();
1.3 简单改造避免重复创建
为了解决1.2中的重复创建问题,则可以这样解决
- 如下所示,简易型代码,需要注意问题,这里传递的上下文context需要是activity.getApplicationContext()全局上下文,避免静态toast对象内存泄漏
/** * 吐司工具类 避免点击多次导致吐司多次,最后导致Toast就长时间关闭不掉了 * 注意:这里如果传入context会报内存泄漏;传递activity..getApplicationContext() * @param content 吐司内容 */ private static Toast toast; @SuppressLint("ShowToast") public static void showToast(String content) { checkContext(); if (toast == null) { toast = Toast.makeText(mApp, content, Toast.LENGTH_SHORT); } else { toast.setText(content); } toast.show(); }
这样用的原理
- 先判断Toast对象是否为空,如果是空的情况下才会调用makeText()方法来去生成一个Toast对象,否则就直接调用setText()方法来设置显示的内容,最后再调用show()方法将Toast显示出来。由于不会每次调用的时候都生成新的Toast对象,因此刚才我们遇到的问题在这里就不会出现
1.4 为何会出现内存泄漏
- 原因在于:如果在 Toast 消失之前,Toast 持有了当前 Activity,而此时,用户点击了返回键,导致 Activity 无法被 GC 销毁, 这个 Activity 就引起了内存泄露。
1.5 吐司是系统级别的
- 经常看到的一个场景就是你在你的应用出调用了多次 Toast.show函数,然后退回到桌面,结果发现桌面也会弹出 Toast,就是因为系统的 Toast 使用了系统窗口,具有高的层级
2.源码分析
2.1 Toast(Context context)构造方法源码分析
在构造方法中,创建了NT对象,那么有人便会问,NT是什么东西呢?于是带着好奇心便去看看NT的源码,可以发现NT实现了ITransientNotification.Stub,提到这个感觉是不是很熟悉,没错,在aidl中就会用到这个。
- 针对aidl,如果有人不明白,可以参考我的这边文章Aidl进程间通信详细介绍主要是Aidl相关属性介绍,实际开发中案例操作,部分源码解析,客户端绑定服务端service原理
public Toast(Context context) { mContext = context; mTN = new TN(); 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); }
在TN类中,可以看到,实现了AIDL的show与hide方法
- TN是Toast内部的一个私有静态类,继承自ITransientNotification.Stub,ITransientNotification.Stub是出现在服务端实现的Service中,就是一个Binder对象,也就是对一个aidl文件的实现而已
/** * schedule handleShow into the right thread */ @Override public void show(IBinder windowToken) { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(0, windowToken).sendToTarget(); } /** * schedule handleHide into the right thread */ @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.post(mHide); }
接着看下这个ITransientNotification.aidl文件
/** @hide */ oneway interface ITransientNotification { void show(); void hide(); }
2.2 show()方法源码分析
通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口,然后把TN对象和一些参数传递到远程NotificationManagerService中去
- 当 Toast在show的时候,然后把这个请求放在 NotificationManager 所管理的队列中,并且为了保证 NotificationManager 能跟进程交互,会传递一个TN类型的 Binder对象给NotificationManager系统服务,接着看下面getService方法做了什么?
public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } //通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口,当前Toast类相当于上面例子的客户端!!!相当重要!!! INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { //把TN对象和一些参数传递到远程NotificationManagerService中去 service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }
接着看看getService方法
- 通过单利模式获取sService对象。
//远程NotificationManagerService的服务访问接口 private static INotificationManager sService; static private INotificationManager getService() { //单例模式 if (sService != null) { return sService; } //通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口 sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; }
接下来看看service.enqueueToast(pkg, tn, mDuration)这段代码,相信有的小伙伴会质疑,这段代码报红色,如何查看呢?
- 于是,我直接在studio中全局搜索NotificationManagerService,终于给找到了,如下所示:
- 下面就到重点呢……注意:record是将Toast封装成ToastRecord对象,放入mToastQueue中。通过下面代码可以得知:通过isSystemToast判断是否为系统Toast。如果当前Toast所属的进程的包名为“android”,则为系统Toast。如果是系统Toast一定可以进入到系统Toast队列中,不会被黑名单阻止。
synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; int index; //判断是否是系统级别的吐司 if (!isSystemToast) { index = indexOfToastPackageLocked(pkg); } else { index = indexOfToastLocked(pkg, callback); } if (index >= 0) { record = mToastQueue.get(index); record.update(duration); record.update(callback); } else { //创建一个Binder类型的token对象 Binder token = new Binder(); //生成一个Toast窗口,并且传递token等参数 mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY); record = new ToastRecord(callingPid, pkg, callback, duration, token); //添加到吐司队列之中 mToastQueue.add(record); //对当前索引重新进行赋值 index = mToastQueue.size() - 1; } //将当前Toast所在的进程设置为前台进程 keepProcessAliveIfNeededLocked(callingPid); if (index == 0) { //如果index为0,说明当前入队的Toast在队头,需要调用showNextToastLocked方法直接显示 showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } }
接下来看一下showNextToastLocked()方法中的源代码,看看这个方法中做了什么……
- 首先获取吐司消息队列中第一个ToastRecord对象,然后判断该对象如果不为null的话,就开始通过callback进行show,且传递了token参数,注意这个show是通知进程显示。然后再调用scheduleTimeoutLocked(record)方法执行超时后自动取消的逻辑【下面详细分析】。同时需要注意的时,如果出现了异常,则会从吐司消息队列中移除该record……
- 那么callback是干嘛的呢,一般印象中callback是处理回调的?从ITransientNotification callback得知,这个callback哥们竟然是是一个 ITransientNotification 类型的对象,也就是前面说到的TN的Binder代理对象,那么他传递的这个token参数是干什么用的呢?这里我们程序员小伙伴可以接着往下看哈!
2.3 mParams.token = windowToken是干什么用的
如果你仔细一点,你可以看到在handleShow(IBinder windowToken)这个方法中,将windowToken赋值给mParams.token,那么就会思考这个token是干什么用的呢?它是哪里传递过来的呢?
- 这个所需要的这个系统窗口 token ,是由我们的 NotificationManager 系统服务所生成,由于系统服务具有高权限,果真是厉害呀。
- 上文2.3中我已经分析了showNextToastLocked()方法部分源码record.callback.show(record.token),可以知道callback对象的show方法中需要传递的参数 record.token实际上就是上面所说的NotificationManager服务所生成的窗口的 token。
- 这个显示窗口的方法比较简单,就是将所传递过来的窗口 token 赋值给窗口属性对象 mParams, 然后通过调用 WindowManager.addView 方法,将 Toast中的mView对象纳入WindowManager中,而WindowManager看源码可知是一个接口,具体是放在WindowManagerService中处理。
2.4 scheduleTimeoutLocked吐司如何自动销毁的
接下来再来看看scheduleTimeoutLocked(record)这部分代码,这个主要是超时监听消息逻辑
- 通过看这段代码知道,handler延迟delay时间后发送消息,并且这个delay时间只有原生自带的两种时间类型,无法开发者自己定义。
既然发送了消息,那肯定有地方接收消息并且处理消息呀。接着看下面代码,重点看cancelToastLocked源码!
- 可以看到当接收到消息时,先判断是否吐司,如果是有的话,也就是索引index>=0,那么就去cancel,在cancelToastLocked(int index)这段源码里面,我们终于可以看到record.callback.hide()这个方法了,前面我们知道callback是前面提到TN的binder代理对象,所以这个方法是调用了TN类中的hide()方法,下面2.5中将详细讲解TN中的消息机制。
- 同时结束吐司之后,移除消息队列中对象,同时判断吐司消息队列中是否还有剩下的消息,如果是有的话,则会接着调用showNextToastLocked()继续弹吐司,关于showNextToastLocked()可以看2.3中的源码分析。
cancelToastLocked源码逻辑主要是
- 调用 ITransientNotification.hide 方法,通知客户端隐藏窗口,并且移除队列中对象
- 将给Toast 生成的窗口Token从WMS 服务中删除
- 判断吐司消息队列中是否存在消息,如果存在消息,则继续开始show吐司……
2.5 TN类中的消息机制
- 看源码可知,TN中的消息机制也是通过handler消息机制实现的。如果对handler 消息机制还不太熟悉,可以查看我的这篇博客:Handler消息机制
当创建TN对象的时候,就创建了handler和runnable对象。
- 然后看看show与hide方法,在show方法中发送消息,当mHandler接受到消息之后,就调用handleShow(token)处理逻辑,通过WindowManager将view添加进来,同时在该方法中也设置了大量的布局属性。
- 在把Toast的View添加之前发现Toast的View已经被添加过(有partent)则删掉;把Toast的View添加到窗口,其中mParams.type在构造函数中赋值为TYPE_TOAST!
同时,当toast执行show之后,过了一会儿会自动销毁,那么这又是为啥呢?那么是哪里调用了hide方法呢?
- 回调了Toast的TN的show,当timeout可能就是hide呢。从上面我分析NotificationManagerService源码中的showNextToastLocked()的scheduleTimeoutLocked(record)源码,可以知道在NotificationManagerService通过handler延迟delay时间发送消息,然后通过callback调用hide,由于callback是TN中Binder的代理对象, 所以便可以调用到TN中的hide方法达到销毁吐司的目的。handleHide()源码如下所示
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. if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeViewImmediate(mView); } mView = null; } }
2.6 普通应用的Toast显示数量是有限制的
如何判断是否是系统吐司呢?如果当前Toast所属的进程的包名为“android”,则为系统Toast,或者调用isCallerSystem()方法
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
接着看看isCallerSystem()方法源码,isCallerSystem的源码也比较简单,就是判断当前Toast所属进程的uid是否为SYSTEM_UID、0、PHONE_UID中的一个,如果是,则为系统Toast;如果不是,则不为系统Toast。
private static boolean isUidSystem(int uid) { final int appid = UserHandle.getAppId(uid); return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0); } private static boolean isCallerSystem() { return isUidSystem(Binder.getCallingUid()); }
为什么要这样判断是否是系统吐司呢?从源码可知:首先系统Toast一定可以进入到系统Toast队列中,不会被黑名单阻止。然后系统Toast在系统Toast队列中没有数量限制,而普通pkg所发送的Toast在系统Toast队列中有数量限制。
- 那么关于数量限制这个结果从何而来,大概是多少呢?查看将要入队的Toast是否已经在系统Toast队列中。这是通过比对pkg和callback来实现的。通过下面源码分析可知:只要Toast的pkg名称和tn对象是一致的,则系统把这些Toast认为是同一个Toast。
- 然后再看看下面这个源码截图,可知,非系统Toast,每个pkg在当前mToastQueue中Toast有总数限制,不能超过MAX_PACKAGE_NOTIFICATIONS,也就是50
2.7 为何Activity销毁后Toast仍会显示
记得以前昊哥问我,为何toast在activity销毁后仍然会弹出呢,我毫不思索地说,因为toast是系统级别的呀。那么是如何实现的呢,我就无言以对呢……今天终于可以回答呢!
- 还是回到NotificationManagerService类中的enqueueToast方法中,直接查看keepProcessAliveIfNeededLocked(callingPid)方法。这段代码的意思是将当前Toast所在进程设置为前台进程,这里的mAm = ActivityManager.getService(),调用了setProcessImportant方法将当前pid的进程置为前台进程,保证不会系统杀死。这也就解释了为什么当我们finish当前Activity时,Toast还可以显示,因为当前进程还在执行。
3.经典总结
3.1 判断应用程序获取通知权限是否开启
- 一行代码调用即可:DialogUtils.requestMsgPermission(this);
- 大部分手机通知权限是开启的。如果关闭了,则吐司是无法显示的,但是仍有部分手机,比如某型号小米手机,锤子手机等就权限需要手动开启。
- Toast的展示是由NMS服务控制的,NMS服务会做一些权限、token等的校验,当通知权限一旦关闭,Toast将不再弹出。
具体可以参考我的弹窗封装库:https://github.com/yangchong2...
- 自定义对话框,其中包括:自定义Toast,采用builder模式,支持设置吐司多个属性;自定义dialog控件,仿IOS底部弹窗;自定义DialogFragment弹窗,支持自定义布局,也支持填充recyclerView布局;自定义PopupWindow弹窗,轻量级,还有自定义Snackbar等等;还有自定义loading加载窗,简单便用。
//判断是否有权限 NotificationManagerCompat.from(context).areNotificationsEnabled() //如果没有通知权限,则直接跳转设置中心设置 @SuppressLint("ObsoleteSdkInt") private static void toSetting(Context context) { Intent localIntent = new Intent(); localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (Build.VERSION.SDK_INT >= 9) { localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS"); localIntent.setData(Uri.fromParts("package", context.getPackageName(), null)); } else if (Build.VERSION.SDK_INT <= 8) { localIntent.setAction(Intent.ACTION_VIEW); localIntent.setClassName("com.android.settings", "com.android.setting.InstalledAppDetails"); localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName()); } context.startActivity(localIntent); }
3.2 使用Toast注意事项
- 通过分析TN类的handler可以发现,如果想在非UI线程使用Toast需要自行声明Looper,否则运行会抛出Looper相关的异常;UI线程不需要,因为系统已经帮忙声明。
- 在使用Toast时context参数尽量使用getApplicationContext(),可以有效的防止静态引用导致的内存泄漏。
- 有时候我们会发现Toast弹出过多就会延迟显示,因为上面源码分析可以看见Toast.makeText是一个静态工厂方法,每次调用这个方法都会产生一个新的Toast对象,当我们在这个新new的对象上调用show方法就会使这个对象加入到NotificationManagerService管理的mToastQueue消息显示队列里排队等候显示;所以如果我们不每次都产生一个新的Toast对象(使用单例来处理)就不需要排队,也就能及时更新呢。
3.3 Toast的显示和隐藏重点逻辑
- Toast调用show方法 ,其实就是是将自己纳入到NotificationManager的Toast管理中去,期间传递了一个本地的TN类型或者是 ITransientNotification.Stub的Binder对象
- NotificationManager 收到 Toast 的显示请求后,将生成一个 Binder 对象,将它作为一个窗口的 token 添加到 WMS 对象,并且类型是 TOAST
- NotificationManager 将这个窗口token通过ITransientNotification的show方法传递给远程的TN对象,并且抛出一个超时监听消息 scheduleTimeoutLocked
- TN 对象收到消息以后将往 Handler 对象中 post 显示消息,然后调用显示处理函数将 Toast 中的 View 添加到了 WMS 管理中,Toast窗口显示
- NotificationManager的WorkerHandler收到MESSAGE_TIMEOUT消息, NotificationManager远程调用hide方法进程隐藏Toast 窗口,然后将窗口token从WMS中删除,并且判断吐司消息队列中是否还有消息,如果有,则继续吐司!
3.4 Snackbar和Toast比较
- 可以使用snackBar替代Toast,即使用户禁掉了通知权限,也可以显示出来。SnackBar,其实就是使用View系统去模拟一个窗口行为,而且还能更加快速的实现动画效果,是不是很棒。
- Snackbar是Android自5.0系统推出MaterialDesign后官方推荐的控件,在交互友好性方面比Toast要好
4.Toast封装库介绍
4.1 能够满足的需求
- 可以设置吐司的位置,偏移,吐司文字颜色,吐司背景颜色等等。简单的代码就可以实现你需要的多种场景。也可以设置定义布局的吐司。项目地址:https://github.com/yangchong2...
4.2 具有的优势
- 采用builder构造者模式,链式编程,一行代码调用即可设置吐司Toast。
为了避免静态toast对象内存泄漏,固可以使用应用级别的上下文context。所以这里我就直接采用了应用级别Application上下文,需要在application进行初始化一下。即可调用……
//初始化 ToastUtils.init(this); //可以自由设置吐司的背景颜色,默认是纯黑色 ToastUtils.setToastBackColor(this.getResources().getColor(R.color.color_7f000000)); //直接设置最简单吐司,只有吐司内容 ToastUtils.showRoundRectToast("自定义吐司"); //设置吐司标题和内容 ToastUtils.showRoundRectToast("吐司一下","他发的撒经济法的解放军"); //第三种直接设置自定义布局的吐司 ToastUtils.showRoundRectToast(R.layout.view_layout_toast_delete); //或者直接采用bulider模式创建 ToastUtils.Builder builder = new ToastUtils.Builder(this.getApplication()); builder .setDuration(Toast.LENGTH_SHORT) .setFill(false) .setGravity(Gravity.CENTER) .setOffset(0) .setDesc("内容内容") .setTitle("标题") .setTextColor(Color.WHITE) .setBackgroundColor(this.getResources().getColor(R.color.blackText)) .build() .show();
因为看到网上有许多toast的封装,需要传递上下文,后来感觉是不是不需要传递这个参数,直接统一初始化一下就好呢。所以才有了这个toast的改良版。
- 如果没有调用ToastUtils.init(this)初始化,则会提示报错ToastUtils context is not null,please first init",具体看下面代码。
/** * 检查上下文不能为空,必须先进性初始化操作 */ private static void checkContext(){ if(mApp==null){ throw new NullPointerException("ToastUtils context is not null,please first init"); } }
5.Toast遇到的异常问题
5.1 Toast偶尔报错Unable to add window
报错日志,是不是有点眼熟呀?更多可以看我的开源项目:https://github.com/yangchong211
android.view.WindowManager$BadTokenException Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
查询报错日志是从哪里来的
发生该异常的原因
- 这个异常发生在Toast显示的时候,原因是因为token失效。通常情况下,一般是不会出现这种异常。但是由于在某些情况下, Android进程某个UI线程的某个消息阻塞。导致 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息之后,迟迟没有执行。这时候,NotificationManager 的超时检测结束,删除了 WMS 服务中的 token 记录。删除 token 发生在 Android 进程 show 方法之前。这就导致了上面的异常。
- 测试代码。模拟一下异常的发生场景,其实很容易,只需要这样做就可以出现上面这个问题
Toast.makeText(this,"潇湘剑雨-yc",Toast.LENGTH_SHORT).show(); try { Thread.sleep(20000); } catch (InterruptedException e) { e.printStackTrace(); }
解决办法,目前见过好几种,思考一下那种比较好……
- 第一种,既然是报is your activity running,那可以不可以在吐司之前先判断一下activity是否running呢?
第二种,抛出异常增加try-catch,代码如下所示,最后仍然无法解决问题
- 按照源码分析,异常是发生在下一个UI线程消息中,因此在上一个ui线程消息中加入try-catch是没有意义的。而且用到吐司地方这么多,这样做也不方便啦!
- 第三种,那就是自定义类似吐司Toast的view控件。个人建议除非要求非常高,不然不要这样做。毕竟发生这种异常还是比较少见的
哪些情况会发生该问题?
- UI 线程执行了一条非常耗时的操作,比如加载图片等等,就类似上面用 sleep 模拟情况
- 进程退后台或者息屏了,系统为了减少电量或者某种原因,分配给进程的cpu时间减少,导致进程内的指令并不能被及时执行,这样一样会导致进程看起来”卡顿”的现象
- 当TN抛出消息的时候,前面有大量的 UI 线程消息等待执行,而每个 UI 线程消息虽然并不卡顿,但是总和如果超过了 NotificationManager 的超时时间,还是会出现问题
5.2 Toast运行在子线程问题
先来看看问题代码,会出现什么问题呢?
new Thread(new Runnable() { @Override public void run() { ToastUtils.showRoundRectToast("潇湘剑雨-杨充"); } }).start();
- 报错日志如下所示:
然后找找报错日志从哪里来的
- ![image]()
子线程中吐司的正确做法,代码如下所示
new Thread(new Runnable() { @Override public void run() { Looper.prepare(); ToastUtils.showRoundRectToast("潇湘剑雨-杨充"); Looper.loop(); } }).start();
得出的结论
- Toast也可以在子线程执行,不过需要手动提供Looper环境的。
- Toast在调用show方法显示的时候,内部实现是通过Handler执行的,因此自然是不阻塞Binder线程,另外,如果addView的线程不是Loop线程,执行完就结束了,当然就没机会执行后续的请求,这个是由Hanlder的构造函数保证的。可以看看handler的构造函数,如果Looper==null就会报错,而Toast对象在实例化的时候,也会为自己实例化一个Hanlder,这就是为什么说“一定要在主线程”,其实准确的说应该是 “一定要在Looper非空的线程”。
- Handler的构造函数如下所示:
5.3 Toast如何添加系统窗口的权限
- 作为程序员,都知道任何视图的显示都要依赖于一个视图窗口Window,同样Toast的显示也需要一个窗口,而且它还是一个系统窗口,这个窗口最终会被WindowManagerService(WMS)标记管理。当显示一个Toast时,调用show方法后,会通过TN 类中的handleShow方法处理展示的逻辑,同时WMS会生成一个token,而我们知道WMS本身就是一个系统级的服务,所以由它生成的token必然拥有权限添加系统窗口,最后WMS调用addView方法将view和mParams参数带进来,这样就可以展示吐司呢。
需要注意:WindowManager检查当前窗口的token是否有效,如果有效,则添加窗口展示Toast;如果无效,则抛出异常,会发生5.1这种类型的异常。
- 在那个地方检查token呢?在mWM.addView(mView, mParams)这里检查token,点击去可以发现ViewManager是个接口,这时候可以去看WindowManagerImpl类,继承ViewManager。
5.4 token null is not valid
- 看了美团的技术文档分享得知,这个异常其实并非是Toast的异常,而是Google对WindowManage的一些限制导致的。Android从7.1.1版本开始,对WindowManager做了一些限制和修改,特别是TYPE_TOAST类型的窗口,必须要传递一个token用于权限校验才允许添加。在stackoverflow上搜索,也较少得到这方面的解答,这块有点难以解决这个问题。
关于其他内容介绍
01.关于博客汇总链接
02.关于我的博客
- 我的个人站点:www.yczbj.org,www.ycbjie.cn
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/...
- 简书:http://www.jianshu.com/u/b7b2...
- csdn:http://my.csdn.net/m0_37700275
- 喜马拉雅听书:http://www.ximalaya.com/zhubo...
- 开源中国:https://my.oschina.net/zbj161...
- 泡在网上的日子:http://www.jcodecraeer.com/me...
- 邮箱:[email protected]
- 阿里云博客:https://yq.aliyun.com/users/a... 239.headeruserinfo.3.dT4bcV
- segmentfault头条:https://segmentfault.com/u/xi...