上一道题目,关于 BadTokenException,很多同学指出 Toast 可能会出现这个问题,我们需要修复一下。
我们自己的代码,没问题,可以修复,假设这个 toast 在 SDK里面调用的,我们也没有源码,怎么处理呢?
有什么思路吗?
更多问答 >>
-
2019-10-28 21:13
-
2019-11-03 23:50
-
2019-11-08 23:06
-
每日一问 | 控件不都是矩形么?遇到多边形,这个怎么绘制,事件分发怎么处理嘞?
2019-11-13 01:08 -
每日一问 | Kotlin 中不需要写“ ; ”,但是有个场景意外?
2019-11-22 00:12 -
2019-10-20 23:46
-
2019-10-15 23:26
-
玩Android更新记录 [from 2019-10-02]
2019-10-08 21:44 -
2019-09-25 21:58
-
2019-09-23 01:01
可以用移花接木之法:
只是我们的手法与大家熟知的手法不一样,因为现在的场景是在SDK中,无法直接修改它的代码,所以用反射替换掉Handler,也是于事无补的。除非,你能拿到NotificationManagerService的实例,替换掉它的mHandler,这个Handler的handleMessage方法中有接收一个what为MESSAGE_TIMEOUT的消息,收到此消息后,会取消掉 刚刚已经通知给Toast.TN那边显示的Toast的View 所对应的Token。没错,也是通过上一个问题中提到的removeWindowToken方法来从Map中移除对应的<Binder, WindowToken>键值对了。问题就出在这里,先来看一下NotificationManagerService的showNextToastLocked方法:record.callback,就是Toast.TN的实例,TN的这个show方法中,会send一条带有token的message到它的mHandler,mHandler收到消息之后,就会调用大家熟知的那个会报错的方法:handleShow。
现在看回上面的showNextToastLocked方法,可以看到在回调了show方法之后,还调用了一个scheduleTimeoutLocked方法,这个方法会给NotificationManagerService中的mHandler发一个延时消息,延时的时长就是刚刚调用show方法的Toast的实际时长,这条消息的what,就是上面说的MESSAGE_TIMEOUT。好,再回到刚刚说的那个会报错的handleShow方法,如果这时候,主线程有很多任务,等待处理的时间超出了当前要显示的Toast的时长(这时候对应的token已经被移除了),当调用WindowManager的addView方法准备把这个Toast的View显示出来时,WindowSession的addToDisplay方法就不能根据这个token找到WindowToken了,所以会返回一个ADD_BAD_APP_TOKEN的Flag,也就是"xxx is not valid; is your activity running?"这条描述了。emmm,那现在会萌生一个想法:替换掉NotificationManagerServiced的mHandler,在收到MESSAGE_TIMEOUT消息之后,不立即remove,而是给主线程post这个remove任务,这样的话,就能保证Toast能在token被remove之前show出来了,也就避免了BadTokenException。
但实际上,你是拿不到NotificationManagerService的实例的,只能拿到它的代理类。所以这个想法,就让它石沉大海吧。正确的解决方法:
我们说的移花接木,既不是替换掉Toast.TN中的mHandler,也不是替换NotificationManagerService的mHandler,更不是替换掉原生的Toast,而是替换掉Application的WindowManager!为什么呢?先来看看那个会报错的handleShow方法:看到没有?那个WindowManager它是通过context的getSystemService来获取的,而这个context实例,就是Application!
它还把View的Context当作第二选择,以防万一。不过在日常开发中,我们使用Toast的时候,无非就借助Activity、Fragment、Service的Context来显示。Fragment的Context也是Activity,所以Toast的View的Context,就只有Activity和Service了。而这两个类的getApplicationContext方法:在非不可预测的特殊情况下,是不会为空的,mPackageInfo和mMainThread这两个类里面,都没有把Application引用置空的代码。
那现在就等于是调用Application的getSystemService方法来获取WindowManager了,我们可以在Application中重写这个方法(必须在manifest里面使用),并且判断版本和对象类型,确定是WindowManager之后,返回一个WindowManger的包装类,就像这样:
可以看到,我们把WindowManger的全部方法都重写了一遍,把具体的逻辑都交给了super.getSystemService返回的WindowManager实例,只是我们在调用它的addView方法时,像其他版本那样,外面加了try catch。
这样的话,当发生BadTokenException时,也不会crash了。这个方法还有个好处就是,其他地方的代码不需要做任何修改,就算SDK里面的方法有使用Toast,发生异常时我们也一样能catch到。
如果只是想处理Toast的操作,其他操作不想加包装的话,可以根据调用栈来判断方法名,可以加上这个条件:Thread.currentThread().stackTrace[3].methodName = ...查看更多
如果只是想处理Toast的操作,其他操作不想加包装的话,可以根据调用栈来判断方法名,可以加上这个条件:Thread.currentThread().stackTrace[3].methodName == "handleShow"(java请用equals),这样就能只针对Toast,其他不包装了。
有个小问题,某些rom会乱改系统的各种Manager,往里面增加各种各样的方法,这些方法我们自己写的Manager肯定是没有实现的,然后会出现各种各样的crash ...查看更多
有个小问题,某些rom会乱改系统的各种Manager,往里面增加各种各样的方法,这些方法我们自己写的Manager肯定是没有实现的,然后会出现各种各样的crash
不会,WindowManager是一个接口,我们把里面的方法都交给原来的WindowManager来执行就OK了。
你都已经new 了一个新的WindowManager了,怎么交给原来的去实现
不是 "交给原来的去实现",是在新实现的方法中调用旧实例的对应方法,这也就是装饰者模式的应用: 调用新的WindowManager的方法,会间接调用原来旧的实例方法,只是加了一个包装而已。 ...查看更多
不是 "交给原来的去实现",是在新实现的方法中调用旧实例的对应方法,这也就是装饰者模式的应用: 调用新的WindowManager的方法,会间接调用原来旧的实例方法,只是加了一个包装而已。
问题是你根本不知道会有这个方法,怎么交给真正的manager实现
我说一下你就明白了:厂商往WindowManager里添加抽象方法,然后在自己的impl里去实现,这里没有问题;你自己写了一个WindowManager作为代理类,重写了你知道的所有方法并交给原来的来 ...查看更多
我说一下你就明白了:厂商往WindowManager里添加抽象方法,然后在自己的impl里去实现,这里没有问题;你自己写了一个WindowManager作为代理类,重写了你知道的所有方法并交给原来的来实现,但厂商加的方法你是不知道的,所以没有去重写,如果系统要调用这个方法,会发现压根找不到有方法体的方法(真正的实现在原来的impl里),就会抛一个AbstractMethodError。
确实会有你说的这种情况出现,不过我们可以做一些检测,以确保本次的调用是来自于Toast.TN.handleShow方法,比如根据调用栈的类名、方法名来判断。 这样的话,只要厂商不手贱删掉WindowM ...查看更多
确实会有你说的这种情况出现,不过我们可以做一些检测,以确保本次的调用是来自于Toast.TN.handleShow方法,比如根据调用栈的类名、方法名来判断。 这样的话,只要厂商不手贱删掉WindowManager的addView方法,就算它往里面添加了一些其他的新方法,也是安全的。 除非厂商它同时也在那个handleShow方法中调用了新方法,但是我想没有厂商会那么做,再加上WindowManager这个接口也没什么可修改的。 如果这种低几率的事情也有可能发生的话,那么网上那么多通过反射系统API的库,也要掂量着使用了吧。 再说回来,替换WindowManager这个方法是目前我能想到的成本、入侵程度最低且相对完美的解决方法了。
厂商在修改API时,应该也会充分考虑到这个修改对于APP是否有兼容性问题的,不然的话,吃亏的反而是厂商自己。
明白了~ 另外其实可以用动态代理的方式动态生成一个实现类,只处理addView方法,其他方法可以无缝交给原来的manager来执行
赞~
缘神,上面的代码可以用kotlin的类代理简化代码,你看一下我的评论,直接评论代码不能格式化
可以使用kotlin的类代理简化代码:
赞~! 学到了。
hook toast,然后拦截“enqueueToast”,替换成自己的handler就可以了。部分华为手机需要拦截“enqueueToastEx”
try一下
目前是想到两种方案
第一种方案比较稳定一些,第二种稍微差点
可以用反射,或者把原生的toast都换成自定义view也可以