详解BadTokenException报错解决方法

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

总结

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

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

(0)

相关推荐

  • SpringBoot启动yaml报错的解决

    SpringBoot启动yaml报错 报错找不到org.yaml里的一个方法 10:45:54.742 [main] ERROR org.springframework.boot.SpringApplication - Application run failed java.lang.NoSuchMethodError: org.yaml.snakeyaml.nodes.ScalarNode.getScalarStyle()Lorg/yaml/snakeyaml/DumperOptions$Sc

  • Android报错Error:Could not find com.android.tools.build:gradle:4.1解决办法

    看字面意思,这个问题是Gradle没有对应版本.在搜索引擎没有找到方法之后,尝试自己解决. 有一点很重要,先保证自己的Android Studio是最新的稳定版本! 因为版本更新会修复很多bug,说不定遇到报错就是某个bug引起的. Could not find com.android.tools.build:gradle:3.0.0. 首先,看报错,大概是长这样的: Error:Could not find com.android.tools.build:gradle:4.1. Searche

  • springBoot集成Elasticsearch 报错 Health check failed的解决

    springBoot集成Elasticsearch 报错 Health check failed 今天集成Elasticsearch 时启动报错 报错信息如下: 2018-11-01 20:52:51.310 INFO [hstao-supersearch,,,] 8528 --- [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8009 2018-11-01 20:52:51.314 INFO [hstao

  • cocos2d-2.0-x-2.0.3 交叉编译到android报错解决

    我用的是cocos2d-2.0-x-2.0.3 之前弄了一天也没成功 今天来了下载了最新的ndk8 更新了sdk 又重新是了一遍 居然成功了,不知道是工具的版本问题还是哪一步出错误了,在这里整理一下: 首先各个工具都下下来配置好,然后将cygwin中的.bash_profile这个文件打开 在最后加上ndk的路径 NDK_ROOT=/cygdrive/c/android-ndk-r8d export NDK_ROOT 2.找到cocos2dx中的create-android-project.ba

  • Android崩溃异常捕获方法

    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面.而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里.但平时使用的时候给你闹崩溃,那你就欲哭无泪了. 那么今天主要讲一下如何去捕捉系统出现的Unchecked异常.何为Unchecked异常呢,换句话说就是指非受检异常,它不能用try-catch来显示捕捉. 我们先从Exception讲起.Exception分为两类:一种是CheckedException,一种是UncheckedException

  • 解决springboot报错找不到自动注入的service问题

    springboot报错找不到自动注入的service 找了不少资料,最后发现是每个service接口(虽然已经使用了@service注解)的实现类都需要加上@service注解. 如果是使用junit生成的测试类,发现无法自动注入服务的话 在test启动类前加上 @RunWith(SpringRunner.class) @SpringBootTest @ComponentScan("com.XXX.service") 这是因为test包和启动类不在同一个包或子包下,无法自动注入 Sp

  • 解决android报错:Intel HAXM is required to run this AVD

    今天,简单讲解Android 启动模拟器时,提示错误: Intel HAXM is required to run this AVD. VT-x is disabled in BIOS. Enable VT-x in your BIOS security settings (refer to documentation for your computer)的问题. 这个问题其实是Android studio是否下载了Download Intel x86 Emulator Accelerator

  • 详解BadTokenException报错解决方法

    线上出现了如上的 crash,第一解决反应是在 show dialog 之前做个 isFinish 和 isDestroyed 判断,当我翻开代码正要解决时,我惊了,原来已经做过了如上的判断检测,示例伪代码如下: public void showDialog(Activity activity){ new OkHttp().call(new Callback(){ void onSucess(Response resp){ if(activity!=null && !activity.is

  • 详解vue-cli项目在IE浏览器打开报错解决方法

    首先要知道VUE官网已表明仅支持IE8以上版本(不包括IE8),css弹性布局flex各浏览器支持如下: 假如项目用到弹性布局,则项目仅支持IE10以上版本. 另外兼容其他低版本的,请另寻它法. 网上说的本人试了都是没用的,下面是亲自试验的解决方法. 1. 问题 :ie浏览器打开vue2.0项目空白,控制台报错 vuex requires a Promise polyfill in this browser; 解决方法: 安装babel-polyfill npm install babel-po

  • Python连接Oracle之环境配置、实例代码及报错解决方法详解

    Oracle Client 安装 1.环境 日期:2019年8月1日 公司已经安装好Oracle服务端 Windows版本:Windows10专业版 系统类型:64位操作系统,基于x64的处理器 Python版本:Python 3.6.4 :: Anaconda, Inc. 2.下载网址 https://www.oracle.com/database/technologies/instant-client/downloads.html 3.解压至目录 解压后(这里放D盘) 4.配置环境变量 控制

  • IDEA安装lombok插件设置Enable Annotation Processing后编译依然报错解决方法

    IDEA导入的项目中有依赖lombok的get set注解,build项目时报错:找不到get/set方法. 查找网上资料,安装lombok插件,如图: 安装好插件后,重启IDEA后还是编译报错,又在设置中勾选了Enable Annotation Processing 如下图: 完成了所有这些设置后编译还是报错找不到get/set方法 经过了很多次尝试后 (更换JDK1.8.清空项目缓存等等)发现项目中引用的lombok jar包是1.16版本的,下载了官网的最新jar包并替换后重新编译,编译通

  • 详解git合并冲突解决方法

    1.git merge冲突了,根据提示找到冲突的文件,解决冲突 如果文件有冲突,那么会有类似的标记 2.修改完之后,执行git add 冲突文件名 3.git commit 注意:没有-m选项 进去类似于vim的操作界面,把conflict相关的行删除掉 4.直接push就可以了,因为刚刚已经执行过相关merge操作了 相关的操作如下 冲突产生 [root@Monitor Demo]# git branch #当前在master分支下 * master psr/psr-01 psr/psr-02

  • Mockito mock Kotlin Object类方法报错解决方法

    比如我创建一个Kotlin Object类:ObjectMethod package com.baichuan.example.unit_test object ObjectMethod { fun doSomething() { println("this is ObjectMethod#doSomething") } @JvmStatic fun doSomethingWithJvmStatic() { println("this is ObjectMethod#doSo

  • Python编程源码报错解决方法总结经验分享

    目录 一.前言 二.解决过程 三.总结 一.前言 最近本都是开开心心的打开电脑写一些祖传BUG 但一个报错阻碍了我写BUG的进度! 这年代还有能阻碍我写BUG的报错??? 二.解决过程 一个新项目要做token认证,所以拷了原来的项目代码 没错,高级CV工程师就是我!!! 加入了一些token配置信息后,启动项目准备调试. 启动很成功没有任何报错. 然后启动前端项目开始一天的写BUG时间! 结果登录的时候就报错了!! 把之前项目的代码拷过来还会报错? 有点奇怪,但都是小场面! 开始调试 结果一看

  • Node.js安装、环境变量配置、报错解决方法

    目录 1.下载Node.js 2.安装Node.js 3.验证Node.js 4.Node.js环境配置 5. Node.js测试 6. 补充 Node.js 就是运行在服务端的 JavaScript. Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台. Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境,基于 Google 的 V8 引擎,V8 引擎执行 Javascript 的速度非常快,性能非常好. 1.下载Node.js 下载地

  • 详解vue中引入stylus及报错解决方法

    前提条件是已经有了vue项目,如果没有,请先建立,具体方法看这里https://cn.vuejs.org/v2/guide/installation.html 安装stylus 好了,建立好项目后我们来安装stylus npm install stylus stylus-loader --save-dev 这样就安装上了stylus. 接下来就可以使用了,使用方式分两种.一种是在.vue文件的style块中使用,一种是引用.styl文件的形式 在.vue文件的style块中使用 这个很简单,只要

  • 详解mysql8.0创建用户授予权限报错解决方法

    问题一: 会报错的写法: GRANT ALL PRIVILEGES ON *.*  'root'@'%' identified by '123123' WITH GRANT OPTION; 以下是正确的写法: grant all privileges on *.* to 'root'@'%' ; 可见,在授权的语句中需要去掉 IDENTIFIED BY 'password'; 单独授予某种权限的写法: GRANT SELECT ON oilsystem.input TO 'u5'@'localh

随机推荐