本文共 16950 字,大约阅读时间需要 56 分钟。
目录介绍
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封装库介绍
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万字],转载请注明出处,谢谢!
链接地址:
如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!
Toast封装库项目地址:
1.最简单的创建方法
1.1 Toast构造方法
1.2 最简单的创建
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相关属性介绍,实际开发中案例操作,部分源码解析,客户端绑定服务端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 */@Overridepublic void show(IBinder windowToken) { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(0, windowToken).sendToTarget();}/** * schedule handleHide into the right thread */@Overridepublic 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方法
//远程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 消息机制还不太熟悉,可以查看我的这篇博客:
当创建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将不再弹出。
具体可以参考我的弹窗封装库:
自定义对话框,其中包括:自定义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 能够满足的需求
可以设置吐司的位置,偏移,吐司文字颜色,吐司背景颜色等等。简单的代码就可以实现你需要的多种场景。也可以设置定义布局的吐司。项目地址:
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
报错日志,是不是有点眼熟呀?更多可以看我的开源项目: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();复制代码
然后找找报错日志从哪里来的
子线程中吐司的正确做法,代码如下所示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.关于我的博客
我的个人站点:,
github:
知乎:
简书:
csdn:
喜马拉雅听书:
开源中国:
泡在网上的日子:
邮箱:yangchong211@163.com
阿里云博客: 239.headeruserinfo.3.dT4bcV
segmentfault头条: