另类 BadTokenException 问题分析和解决

既然不是客户端那么就是在服务端了,那么 Activity 对应的这个 Token 是怎么生成的呢?

  • 启动 Activity 之前系统端 AMS 会创建 ActivityRecord,并在构造函数内部实例化了一个 Token,这个 token 保证了唯一性,从而保证了 Activity 在 AMS 端的唯一标识,包括 WMS 及其它服务都基于这个唯一标识进行信息同步和区分:

  • 服务端 AMS 在通知客户端实例化 Activity 过程,会传入 Token 标识给客户端进程,及后期便于将服务端与客户端(ActivityRecord 与 ActivityClientRecord)进行同步管理:

  • 同时 AMS 在创建完 ActivityRecord 之后,也要通知 WMS 去为当前 Activity 创建一个 WindowToken,便于将当前的 Activity 与之对应的 Window 进行关联,依然传入 Token 标识做 Key 映射,调用过程:

  • 在 WMS 内部,会根据传入的 appToken 去查找是否已经创建过 WindowToken,如果没有则实例化一个 WindowToken,并将 token 作为标识。

  • 在回到客户端,App 进程根据 Token 作为 key 将当前的 ActivityClientRecord 与 token 建立映射,存入 map 表中。

在搞清楚 token 的由来和使用场景之后,下面就进入正题,看一下为啥刚创建的 token 为啥,就提示 Wms 端被移除了呢?那又是怎么移除的呢?

Activity 的 Destory 过程:

Activity 的生命周期,主要包括下面几个过程:Create,Pause,Stop,Destory;进一步分析系统源码可以知道,在 AMS 向客户端进程对应 Activity 发送了 Destory 通知并完成或发送之后一直没有执行,发送超时之后,系统服务 AMS 才会通知 WMS 将当前 Activity 的 Window 移除掉;详见:ActivityStack.java

回到当前问题,从进程崩溃现场可知,主线程正在创建 Activity(LaunchActivity),此时主线程消息队列所有消息都被 Block,没有机会执行 onDestory,那么猜测有一种可能,即系统在向客户端进程发送 scheduleDestory 之后,触发了 Timeout,从而在服务端强制触发了 Destory 操作并将 WindowToken 移除(感觉系统太不负责任了)。

但是受限于系统对 App 权限管控,无法从 App 层面获取系统 event 日志,否则从 event 日志可以清楚的看到 activity 生命周期的切换过程。那么从 App 层是否有其它办法确认系统是否已经对当前 Activity 执行了 Destory 请求呢?答案是有的。

消息调度:

我们之前为了更好的分析 ANR 问题,并针对 App 因权限获取信息不足,上线了主线程消息调度监控,简单来说就是对发生 ANR 问题之前一段时间,耗时较长的调度消息进行监控并统计,目的是为了更好的监控 ANR 发生之前一段时间主线程的状态,包括:

  1. 主线程都执行了哪些耗时的消息

  2. 当前正在消息已处理时长

  3. 获取缓存在消息队列未被调度的消息及其被 pending 时长

参考下图:

有了上述消息调度监控,我们便可以清晰看到 pending 消息队列是否有我们预期的未调度消息,如:H.DESTROY_ACTIVITY。我们将 ANR 的消息调度监控扩展到了 Crash 场景,以便于分析这类时序类问题,提供有更多更有效的参考信息。

问题分析:

=====

有了上面大量的知识铺垫和信息扩展,直接结合下面问题实例进行分析,通过我们 Crash 画像可以看到:当前进程启动时长为 22S,并处于后台。为啥处于后台?基本是用户刚打开这个应用,由于其他原因,如来电?语音电话?Home 键?将其切回到后台(可以思考一下如果处于前台会不会有该类问题?)但是当前应用状态仍处于 LaunchActivity 阶段,后台场景也为该问题埋下伏笔。

我们拿到了当前 Case 对于的消息调度历史监控数据和 Pending 队列的数据,可以看到当前正在执行的消息正是 HandleLaunchActivity,和 Crash 堆栈现场问题,再看当前消息调度耗时已超过 8S,属于非正常启动

“current_message”: {

“currentMessageCost”: 8454, // 当前消息耗时超过8S

“currentMessageCpu”: 4120,

“currentTick”: 27,

“message”: “\u003e\u003e\u003e\u003e\u003e Dispatching to Handler (android.app.ActivityThread$H) {17b8b5a} null: 100”

},

再进一步搜索消息队列,查看消息队列是否有未处理的 PAUSE_ACTIVITY,及关键的 DESTROY_ACTIVITY 消息,在 pending 消息队列中,找到了与当前正在创建 Activity 对于的 Destory 消息对象(“obj”:“android.os.BinderProxy@4506268”),并且该消息在消息队列 pending 时间已经超过 19S!!!已超过了服务端 AMS 请求 Activity.destory 设置的 10S 超时等待,也就是说服务端已经在发生了 timeOut,并将当前 Activity 及 Token 强制移除;

“pending_messages”: [

{

“arg1”: 1,

“arg2”: 0,

“id”: 10,

“obj”: “android.os.BinderProxy@4506268”, //该对象主线程正在创建的Activity对象里面的ActivityToken

“target”: “Handler (android.app.ActivityThread$H) {17b8b5a}”,

“what”: 109, // 系统已经向客户端发了Destory消息,指挥系统测会去销毁WMS内部维护的WindowStatToken。

“when”: -19845 //发送时间已经长达19S+,这段时间服务端早把对应ActivityToken对应的WindowStat给移除了

},

{

“arg1”: 0,

“arg2”: 0,

“callback”: “com.ss.android.common.util.MultiProcessSharedProvider$1@8ef5a87”,

“id”: 11,

“target”: “Handler (android.os.Handler) {6e8ddb4}”,

“what”: 0,

“when”: -18708

},

]

经过上面的分析,并结合消息调度监控及直接数据,我们该类问题有了一个比较清晰的流程,即:在应用退到后台,因为设备环境变化,如 pending Stop Activity 过多,或横竖屏变化,导致服务端 AMS 主动向当前 Activity 发送 Destory 请求,并设置超时监听,如果请求顺利完成,会通知 AMS 并接下来通知 WMS 移除 windowToken,但是如果客户端返回过晚,或没有来得及执行(如当前 BadCase),那么将会在 AMS 端触发 Timeout,强行通知 WMS 移除 windowToken,但是客户端并知道当前 Activity 在服务端对应的 Token 已经被移除,继续按照正常流程向 WMS 发送 addView 请求时,被 WMS 抛出 BadToken 异常,整个流程如下:

分析结论:

=====

综上分析,这类问题主要发生在后台场景,即:Activty 启动过程被用户切换到后台,但因创建 Activity 过程出现 BadCase 导致耗时较长,叠加一些系统环境改变和超时机制,导致系统向当前 Activity 强制发起 Stop 或 Destory 操作,进一步超时后,服务端 AMS 将 WMS 侧的 WindowToken 移除。

从问题分布来看,耗时问题严重的 Activity 实例化过程更容易触发这类问题,如存在 IPC 调用,尽管本地测试 IPC 调用耗时很少,但是到了线上,各种复杂的用户场景不能保证每次 IPC 调用都是高效的,这就可能出现某次初始化过程很久,用户不愿等,退到后台后再遇到系统环境的一些变化,一定概率发生上述问题,如头条内部的一个业务 Activity,贡献了这类问题 90%以上崩溃量。

解决方案:

=====

在分析了问题原因之后,我们再来说说该如何解决或规避此类问题,毕竟 Crash 问题还是很影响用户体验的。解决该类问题主要有 2 个思路:一个是从正面解决,即优化 LaunchActivty 期间对应业务耗时问题,提高性能稳定性,减少 IO,IPC,资源同步等等;部分业务逻辑异步化;将部分数据缓存处理,将会大大减少该类问题发生;

但是对应一个复杂的 App 来说,很难做的彻底,并且随着业务迭代,可能这类问题还会死灰复燃,另一个角度是:对于该类问题进行兜底,以将影响降至最低。

业务侧兜底:

重载部分问题 Activity 的 onResume 方法,在 Activity 第一次执行 onResume 时,判断一下当前 Activity 是否处于后台以及本次 onCreate 到 onResume 耗时是否过长,如果满足这 2 个条件,那么有一定概率触发此类问题;可以在此 Activity 的 onResume 调用结束处,判断消息队列缓存是否已经存在当前 Activity 的 H.DESTROY_ACTIVITY,如果有该消息,则说明 AMS 已经将 WMS 缓存的 windowToken 移除,此时可在客户端主动调用当前 Activity 的 finish 接口,将 Activity 内部的 mFinished 置为 True:

在 performResumeActivity 之后,系统在 addView 之前,会进行一些状态判断,其中就包括 mFinished 对象,而此时 mFinished 可能已经被我们主动置为 True,因此即可跳过 PeformResumeActivity 结束后的 wm.addView 逻辑,避免 Crash 问题发生;

final void handleResumeActivity(IBinder token,

boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

ActivityClientRecord r = mActivities.get(token);

// TODO Push resumeArgs into the activity for consideration

r = performResumeActivity(token, clearHide, reason);

// 主动发出finish之后,当前a.mFinished则被标记为true,内部逻辑直接跳过。

if (r.window == null && !a.mFinished && willBeVisible) {

r.window = r.activity.getWindow();

View decor = r.window.getDecorView();

decor.setVisibility(View.INVISIBLE);

ViewManager wm = a.getWindowManager();

WindowManager.LayoutParams l = r.window.getAttributes();

a.mDecor = decor;

if (a.mVisibleFromClient) {

if (!a.mWindowAdded) {

a.mWindowAdded = true;

wm.addView(decor, l); //在此处与WMS IPC通信过程发生异常

}

}

}

通用解决方案:

=======

除了上面提到的指定业务侧进行兜底之外,还有一种方案可以业务无侵入的方式进行兜底解决,这样做的优势在于大大减少业务的耦合,无需业务适配,即:通过代理 ActivityManager 去监听 willActivityBeVisible 调用,并在监听该接口调用过程去判断当前 Token 在服务侧 AMS 是否存在;如果不存在则说明服务端 AMS/WMS 已经销毁(移除)当前 Activity 的 Token 对象,同理在此主动调用当前 Token 对应 Activity 的 finish()请求,之后相关 addView 相关逻辑不被执行,以解决此类问题。

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
[外链图片转存中…(img-VadO0XdX-1715863802933)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值