详解BadTokenException报错解决方法
作者:让开,我要吃人了 发布时间:2021-11-27 06:18:20
线上出现了如上的 crash,第一解决反应是在 show dialog 之前做个 isFinish 和 isDestroyed 判断,当我翻开代码正要解决时,我惊了,原来已经做过了如上的判断检测,示例伪代码如下:
public void showDialog(Activity activity){
new OkHttp().call(new Callback(){
void onSucess(Response resp){
if(activity!=null && !activity.isFinishing() && !activity.isDestroed()){
new Dialog().show()
}
}
})
}
这该如何是好,正常的判断解决不了 badToken 问题,在焦灼之际重新回顾一下 framework 的源码,AMS 分发 onDestroy 生命周期在 ActivityRecord 类(基于 Android 10 源码):
1、第一个红框调用 ApplicationThread binder 代理的 scheduleTransaction 方法,回执的生命周期为 DestroyActivityItem,scheduleTransaction 方法将包裹着 DestroyActivityItem 的 ClientTransaction 分发给 ActivityThread , ActivityThread 的父类会处理 scheduleTransaction ,并将 ClientTransaction 切换到主线程进行进行 Activity 的生命周期调度。为什么要把这个过程理清,后面解决部分会 hook 该过程
2、第二个红框是 Destroy 生命周期超时处理,超时时间为 10s,如果分发给应用进程的 onDestroy 10s 内处理未结束,AMS 也会在超时的时候,将该 Activity 标记为已销毁,并通知 WMS 删除该 Activity 的 token。
通过这两点,我们可以推理出我们应用当时处于什么环境:
AMS 已经将销毁的指令告诉应用进程了,但应用进程一直在处理自己的事情,未处理 Destroy 生命周期(从业务代码 > isDestroyed> = false 可知),然后 AMS 的 10s 超时机制到了,并通知 WMS 移除 token,然后我们的业务代码异步请求网络完成,判断 isFinish 和 isDestroyed 都是有效的,然后就顺理成章的执行了 show dialog 操作,发生了该异常。
我们可以画个简单的图:
解决办法1
既然是 AMS 发的 destroy 消息被主线程的其他任务阻塞导致一直没执行,那么,我们可以在 show dialog 的时候去检查一下主线程的 MessageQueue,遍历一下所有的 Message,看看里面有没有 Destroy Message,如果有的话,说明当前会发生 badToken 异常。
查看了下 MessageQueue 的 mMessages 字段,发现该字段被标注为 UnsupportedAppUsage
注解,看起来不支持给 app 调用,先不管,我们先 hook 一番,代码就不贴了,后面给出示例代码,一顿操作猛如虎,发现是可以通过反射拿到 Message 的,然后接下来就可以通过递归遍历 Message next,取出所有的 Message。
在拿到 Message 的同时,我们要怎么识别出这是个 Destroy Message 呢?
这要看不同的系统版本:
Android P 之前(不包括 P),destroy message 是通过给 Message.what = DESTROY_ACTIVITY 来进行分发的,DESTROY_ACTIVITY = 109,那么我们就可以判断,只要 Message 中的 what 为 109 即可判断当前是 Destroy Message。
Android P 之后(包括 P),AMS 的生命周期分发改了,不再是通过调用 ApplicationThread 的某个方法,然后根据 DESTROY_ACTIVITY 这种数值型来分发,而是全部统一走 ApplicationThread 的 scheduleTransaction 方法,生命周期标识是存放在参数 ClientTransaction 中,在切换到主线程时,会执行 ClientTransaction 的 getLifecycleStateRequest 方法,拿到 ActivityLifecycleItem,ActivityLifecycleItem 的子类很多,其中就有 DestroyActivityItem ,我们只需要判断 Message 中是否有 DestroyActivityItem 即可
部分示例代码如下:
fun isOnDestroyMsgExit(): Boolean {
val msg = hookMessage()
return nextMessage(::isOnDestroyMsgExit, msg)
}
private fun isOnDestroyMsgExit(msg: Message): Boolean {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
if (msg.what == EXECUTE_TRANSACTION && msg.obj != null) {
val clazz = msg.obj::class.java
if (TextUtils.equals(clazz.name, "android.app.servertransaction.ClientTransaction")) {
val method = clazz.getDeclaredMethod("getLifecycleStateRequest")
method.isAccessible = true
val obj = method.invoke(msg.obj)
if (obj!=null){
val clazzName = obj::class.java.name
if (TextUtils.equals(clazzName,"android.app.servertransaction.DestroyActivityItem") ){
return true
}
}
}
}
} else {
return msg.what == DESTROY_ACTIVITY
}
return false
}
demo 验证如下,destroy message 被成功拿到:
那么我们的业务代码的判断就可以改造成:
public void showDialog(Activity activity){
new OkHttp().call(new Callback(){
void onSucess(Response resp){
if(activity!=null
&& !activity.isFinishing()
&& !activity.isDestroed()
// 多加一条判断,判断当前消息队列中没有 destroy message
&& !BadTokenUtils.isOnDestroyMsgExit()
){
new Dialog().show()
}
}
})
}
这种方式有个缺点,大量的 hook message 会造成应用的不稳定性。
解决方法2
业务代码是在请求网络成功的时候进行的 dialog 展示,这时候又有人问了,这是在子线程,怎么能 show dialog 呢?其实不然,ViewRoomImpl 检验线程,是判断创建 ViewRootImpl 时的线程与 requestLayout 的线程一致,是一样的话,即可直接操作。
但这一点提醒到了我,我们能否将 show dialog 的逻辑放到主线程来做,MessageQueue 已经有了 destroy 消息,如果我们再发一个 show dialog message 的话,那肯定是排在 destroy message 后面的(Message 会根据 when 来整理链表),那么,先处理的 destroy message 会使 isDestroyed 为 true,这样,我们的判断就生效了,示例图如下:
代码则变为:
public void showDialog(Activity activity){
new OkHttp().call(new Callback(){
void onSucess(Response resp){
// 先判断一次
if(activity!=null && !activity.isFinishing() && !activity.isDestroed() ){
// 切到主线程,post 一个 message 给 MQ
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
// 再判断一次
if(activity!=null && !activity.isFinishing() && !activity.isDestroed() ){
new Dialog().show()
}
}
});
}
});
}
缺点:runOnUiThread 只对异步线程有效,因为在主线程会被直接执行,并不会插入一条 message,解决办法也有,如果当前是在主线程的话,可以通过 handler 的方式发送一条 message,如 Handler(Looper.getMainLooper()).post()
来源:https://blog.csdn.net/weixin_55362248/article/details/119986854


猜你喜欢
- 一、继承引言继承关系可以对不同模块的依赖版本做统一管理,因为子模块中的依赖基本都继承于父模块,父模块中指定哪个版本,子模块就继承哪个版本,可
- 前言:线程池是一个非常重要的知识点,也是池化技术的一个典型应用,相信很多人都有使用线程池的经历,但是对于线程池的实现原理大家都了解吗?本篇文
- DataSource在数据库应用中,客户端与数据库服务端建立的连接对象(Connection)是宝贵的资源,每次请求数据库都创建连接,使用完
- 1.可见性通常,我们无法保证执行读操作的线程能看到其他线程写入的值,因为每个线程都由自己的缓存机制。为了确保多个线程之间对内存写入操作的可见
- 本文研究的主要是java fastdfs客户端使用实例的相关内容,具体实现如下。什么是FastDFS?FastDFS是用c语言编写的一款开源
- 目录一、事出有因二、解决方案困境三、柳暗花明,终级解决方案第一种实现方案第二种实现方案第三种实现方案四、引发的思考一、事出有因最近有一个场景
- 一、Steam的优势java8中Stream配合Lambda表达式极大提高了编程效率,代码简洁易懂(可能刚接触的人会觉得晦涩难懂),不需要写
- 本文主要从两个方面对Android Volley框架的使用方法进行讲解,具体内容如下一、网络请求1.get方式请求数据// 1 创建一个请求
- 在很多的Android项目中都需要用户登录、注册。这样的话在开发中做好保护用户密码的工作就显得尤为重要。这里我把自己的密码保护方法记录下来。
- 一、JVM 类加载机制JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。1. 加载:加载是类
- 前言项目中有使用到水印效果,如下图所示。在实现过程中,最终选用ItemDecoration来实现,其中有两大步骤:自定义Drawable来完
- Thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, P
- 本文为大家分享了Android使用线程获取网络图片的具体代码,供大家参考,具体内容如下AndroidManifest.xml &n
- 本篇文章所涉及到的demo练习 使用的cloud 2021.0.3+ springboot2.6.8一、概述简介官网:https://doc
- 一、什么是桥接模式桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又
- DataTable可以通过RowStatus来判断状态是否发生了改变。但是有些时候我们希望在行状态即使为Modified的情况下也不要提示内
- 前言最近做了个滑动选择的小控件,拿出来给大家分享一下,先上图运行效果实现步骤这里分解为3个动作:Down、Move、Up来进行分析,博主文采
- dynamic是C#里面的动态类型,可在未知类型的情况访问对应的属性,非常灵活和方便。使用Json.Net可以把一个Json字符串转换成一个
- 一.SQLite的介绍1.SQLite简介SQLite是一款轻型的数据库,是遵守ACID的关联式数据库管理系统,它的设计目标是嵌入 
- 1.运行程序时, AddOrEditBook1.BooksType = GetTypeName(model.BookType_ID); 出现