Android Toast 源码深度分析
1.最简单的创建方法
1.1 Toast构造方法
Toast只会弹出一段信息,告诉用户某某事情已经发生了,过一段时间后就会自动消失。它不会阻挡用户的任何操作。
Toast是没有焦点,而且Toast显示的时间有限,过一定的时间就会自动消失。
- 通过new Toast(context)直接创建,除了将mContext = context,还有一步重要的操作,创建TN,下面会说到……
1
2
3
4
5
6
7
8public 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却还没显示结束
1
Toast.makeText(this,"吐司",Toast.LENGTH_SHORT).show();
1.3 简单改造避免重复创建
为了解决1.2中的重复创建问题,则可以这样解决
- 如下所示,简易型代码,需要注意问题,这里传递的上下文context需要是activity.getApplicationContext()全局上下文,避免静态toast对象内存泄漏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 吐司工具类 避免点击多次导致吐司多次,最后导致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中就会用到这个。
1
2
3
4
5
6
7
8public 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文件的实现而已
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 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文件
1
2
3
4
5/** @hide */
oneway interface ITransientNotification {
void show();
void hide();
}
2.2 show()方法源码分析
通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口,然后把TN对象和一些参数传递到远程NotificationManagerService中去
- 当 Toast在show的时候,然后把这个请求放在 NotificationManager 所管理的队列中,并且为了保证 NotificationManager 能跟进程交互,会传递一个TN类型的 Binder对象给NotificationManager系统服务,接着看下面getService方法做了什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public 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对象。
1
2
3
4
5
6
7
8
9
10
11//远程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队列中,不会被黑名单阻止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37synchronized (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 系统服务所生成,由于系统服务具有高权限,果真是厉害呀。
- 已经分析了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消息机制实现的。
当创建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()源码如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14public 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()方法
1
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
接着看看isCallerSystem()方法源码,isCallerSystem的源码也比较简单,就是判断当前Toast所属进程的uid是否为SYSTEM_UID、0、PHONE_UID中的一个,如果是,则为系统Toast;如果不是,则不为系统Toast。
1
2
3
4
5
6
7
8private 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将不再弹出。
自定义对话框,其中包括:自定义Toast,采用builder模式,支持设置吐司多个属性;自定义dialog控件,仿IOS底部弹窗;自定义DialogFragment弹窗,支持自定义布局,也支持填充recyclerView布局;自定义PopupWindow弹窗,轻量级,还有自定义Snackbar等等;还有自定义loading加载窗,简单便用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//判断是否有权限
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 能够满足的需求
- 可以设置吐司的位置,偏移,吐司文字颜色,吐司背景颜色等等。简单的代码就可以实现你需要的多种场景。也可以设置定义布局的吐司。
4.2 具有的优势
采用builder构造者模式,链式编程,一行代码调用即可设置吐司Toast。
为了避免静态toast对象内存泄漏,固可以使用应用级别的上下文context。所以这里我就直接采用了应用级别Application上下文,需要在application进行初始化一下。即可调用……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//初始化
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”,具体看下面代码。
1
2
3
4
5
6
7
8/**
* 检查上下文不能为空,必须先进性初始化操作
*/
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
1 | android.view.WindowManager$BadTokenException |
查询报错日志是从哪里来的
发生该异常的原因
- 这个异常发生在Toast显示的时候,原因是因为token失效。通常情况下,一般是不会出现这种异常。但是由于在某些情况下, Android进程某个UI线程的某个消息阻塞。导致 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息之后,迟迟没有执行。这时候,NotificationManager 的超时检测结束,删除了 WMS 服务中的 token 记录。删除 token 发生在 Android 进程 show 方法之前。这就导致了上面的异常。
- 测试代码。模拟一下异常的发生场景,其实很容易,只需要这样做就可以出现上面这个问题
1
2
3
4
5
6Toast.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运行在子线程问题
先来看看问题代码,会出现什么问题呢?
1
2
3
4
5
6new Thread(new Runnable() {
@Override
public void run() {
ToastUtils.showRoundRectToast("潇湘剑雨-杨充");
}
}).start();- 报错日志如下所示:
然后找找报错日志从哪里来的
子线程中吐司的正确做法,代码如下所示
1
2
3
4
5
6
7
8new 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上搜索,也较少得到这方面的解答,这块有点难以解决这个问题。