这里我们紧张剖析所有 App 都会碰着的另一类 BadTokenException 问题,这类问题会在 App 体量变大之后,暴露的愈发明显,如下图这类问题在公司内部很多产品都是 TopCase。
Crash 堆栈表现如下:流程剖析:
和 Toast 和 Dialog 场景的 BadToken 一样,在开始剖析该类问题之前,首先要弄清楚 Activity 的这个 Token 是怎么产生的,它的浸染是什么,都会在哪些场景用到?
Token,顾名思义便是暗号,识别号,既然是一个暗号,那么就须要担保全局唯一,才便于全体系统去识别和管理。翻看源码可以创造,这个 Token 并不是在客户端进程天生的;

既然不是客户端那么便是在做事端了,那么 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 发生之前一段韶光主线程的状态,包括:
主线程都实行了哪些耗时确当前正在已处理时长获取缓存在行列步队未被调度的及其被 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) { ....... // TODO Push resumeArgs into the activity for consideration r = performResumeActivity(token, clearHide, reason); if (r != null) { final Activity a = r.activity; ...... boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } //如果调用了finish,则a.mFinished遍历被置为True,则addView将不会被实行,后续干系逻辑也被跳过 if (r.window == null && !a.mFinished && willBeVisible) { if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); //非常在这里发生 } else { } } ...... } }}
更多分享
“���”引发的线上事件
字节跳动在 Go 网络库上的实践
InfoQ 专访头条搜索:从推举到搜索,如何构建搜索技能的另一种可能?
更多问题
后续我们将分享更多Android系统本身及厂商定制引起的稳定性问题及办理方案,欢迎大家连续关注。
Android 平台架构团队字节跳动 Android 平台架构团队以做事今日头条产品为主,同时帮忙公司其他产品,在产品性能、稳定性等用户体验,研发流程,架构方向上不断优化和深入探索,以知足产品快速迭代的同时,保持较高的用户体验。我们长期招聘 Android 平台架构方向的同学,涉及用户根本体验优化,研发流程,产品架构优化,详细可拜会 https://job.toutiao.com/s/K3yPgx,感兴趣可以联系邮箱 xulei.sky@bytedance.com 。
欢迎关注字节跳动技能团队