我的编程空间,编程开发者的网络收藏夹
学习永远不晚

详解BadTokenException报错解决方法

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

详解BadTokenException报错解决方法

image.jpg

线上出现了如上的 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 源码):

image.jpg

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 操作,发生了该异常。

我们可以画个简单的图:

image.jpg

解决办法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 被成功拿到:

image.jpg

那么我们的业务代码的判断就可以改造成:


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,这样,我们的判断就生效了,示例图如下:

image.jpg

代码则变为:


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()

总结

大部分场景都能通过 isFinish 和 isDestroyed 判断来解决,但对于主线程做耗时任务导致 destroy message 没有被正确处理情况,还是得回归到应用稳定性治理层面,虽然能解决 badToken 问题,但本质上应用卡顿问题依然存在.

到此这篇关于详解BadTokenException报错解决方法的文章就介绍到这了,更多相关解决BadTokenException内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

详解BadTokenException报错解决方法

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Django-报错解决方法

无法使用Django新建项目:'django-admin.py’不是内部或外部命令找到site-packages/django/bin(如 D:\Program Files\Anaconda3\Lib\site-packages\djang
2023-01-30

【Flask】报错解决方法:Assert

运行Flask时出现了一个错误, AssertionError: View function mapping is overwriting an existing endpoint function: main.user  直译就是视图方法
2023-01-31

pyqt5安装报错的解决方案详解

pyqt5安装报错的解决方案详解,附具体代码示例引言:PyQt5是一款强大而流行的Python GUI图形界面开发工具。安装PyQt5时,常常会遇到一些报错,这给开发者带来了不少困扰。本文将详细介绍几种常见的PyQt5安装报错及相应的解决
pyqt5安装报错的解决方案详解
2024-01-19

php $_get报错的解决方法

这篇文章主要介绍php $_get报错的解决方法,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!php $_get报错的解决办法:使用【array_key_exists(key, 数组)】函数来进行判断,其中参数1为要
2023-06-09

git报错:OpenSSLSSL_read:Connectionwasreset,errno10054解决方法

这篇文章主要介绍了git报错:OpenSSLSSL_read:Connectionwasreset,errno10054解决方法,涉及git配置信息及缓存相关操作技巧,需要的朋友可以参考下
2023-05-15

SpringDataJpa的@Query注解报错的解决方法

这篇文章将为大家详细讲解有关SpringDataJpa的@Query注解报错的解决方法,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。SpringDataJpa @Query注解报错publi
2023-06-21

php中include报错的解决方法

这篇文章将为大家详细讲解有关php中include报错的解决方法,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。php include报错的解决办法:1、检查include文件的位置是否正确;2、检测被包含
2023-06-14

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录