Android BadTokenException異常解決案例詳解

線上出現(xiàn)了如上的 crash,第一解決反應(yīng)是在 show dialog 之前做個 isFinish 和 isDestroyed 判斷,當(dāng)我翻開代碼正要解決時,我驚了,原來已經(jīng)做過了如上的判斷檢測,示例偽代碼如下:
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 分發(fā) onDestroy 生命周期在 ActivityRecord 類(基于 Android 10 源碼):

1、第一個紅框調(diào)用 ApplicationThread binder 代理的 scheduleTransaction 方法,回執(zhí)的生命周期為 DestroyActivityItem,scheduleTransaction 方法將包裹著 DestroyActivityItem 的 ClientTransaction 分發(fā)給 ActivityThread , ActivityThread 的父類會處理 scheduleTransaction ,并將 ClientTransaction 切換到主線程進(jìn)行進(jìn)行 Activity 的生命周期調(diào)度。為什么要把這個過程理清,后面解決部分會 hook 該過程
2、第二個紅框是 Destroy 生命周期超時處理,超時時間為 10s,如果分發(fā)給應(yīng)用進(jìn)程的 onDestroy 10s 內(nèi)處理未結(jié)束,AMS 也會在超時的時候,將該 Activity 標(biāo)記為已銷毀,并通知 WMS 刪除該 Activity 的 token。
通過這兩點(diǎn),我們可以推理出我們應(yīng)用當(dāng)時處于什么環(huán)境:
AMS 已經(jīng)將銷毀的指令告訴應(yīng)用進(jìn)程了,但應(yīng)用進(jìn)程一直在處理自己的事情,未處理 Destroy 生命周期(從業(yè)務(wù)代碼 > isDestroyed> = false 可知),然后 AMS 的 10s 超時機(jī)制到了,并通知 WMS 移除 token,然后我們的業(yè)務(wù)代碼異步請求網(wǎng)絡(luò)完成,判斷 isFinish 和 isDestroyed 都是有效的,然后就順理成章的執(zhí)行了 show dialog 操作,發(fā)生了該異常。
我們可以畫個簡單的圖:

解決辦法1
既然是 AMS 發(fā)的 destroy 消息被主線程的其他任務(wù)阻塞導(dǎo)致一直沒執(zhí)行,那么,我們可以在 show dialog 的時候去檢查一下主線程的 MessageQueue,遍歷一下所有的 Message,看看里面有沒有 Destroy Message,如果有的話,說明當(dāng)前會發(fā)生 badToken 異常。
查看了下 MessageQueue 的 mMessages 字段,發(fā)現(xiàn)該字段被標(biāo)注為 UnsupportedAppUsage 注解,看起來不支持給 app 調(diào)用,先不管,我們先 hook 一番,代碼就不貼了,后面給出示例代碼,一頓操作猛如虎,發(fā)現(xiàn)是可以通過反射拿到 Message 的,然后接下來就可以通過遞歸遍歷 Message next,取出所有的 Message。
在拿到 Message 的同時,我們要怎么識別出這是個 Destroy Message 呢?
這要看不同的系統(tǒng)版本:
- Android P 之前(不包括 P),destroy message 是通過給 Message.what = DESTROY_ACTIVITY 來進(jìn)行分發(fā)的,DESTROY_ACTIVITY = 109,那么我們就可以判斷,只要 Message 中的 what 為 109 即可判斷當(dāng)前是 Destroy Message。
- Android P 之后(包括 P),AMS 的生命周期分發(fā)改了,不再是通過調(diào)用 ApplicationThread 的某個方法,然后根據(jù) DESTROY_ACTIVITY 這種數(shù)值型來分發(fā),而是全部統(tǒng)一走 ApplicationThread 的 scheduleTransaction 方法,生命周期標(biāo)識是存放在參數(shù) ClientTransaction 中,在切換到主線程時,會執(zhí)行 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 驗(yàn)證如下,destroy message 被成功拿到:

那么我們的業(yè)務(wù)代碼的判斷就可以改造成:
public void showDialog(Activity activity){
new OkHttp().call(new Callback(){
void onSucess(Response resp){
if(activity!=null
&& !activity.isFinishing()
&& !activity.isDestroed()
// 多加一條判斷,判斷當(dāng)前消息隊(duì)列中沒有 destroy message
&& !BadTokenUtils.isOnDestroyMsgExit()
){
new Dialog().show()
}
}
})
}
這種方式有個缺點(diǎn),大量的 hook message 會造成應(yīng)用的不穩(wěn)定性。
解決方法2
業(yè)務(wù)代碼是在請求網(wǎng)絡(luò)成功的時候進(jìn)行的 dialog 展示,這時候又有人問了,這是在子線程,怎么能 show dialog 呢?其實(shí)不然,ViewRoomImpl 檢驗(yàn)線程,是判斷創(chuàng)建 ViewRootImpl 時的線程與 requestLayout 的線程一致,是一樣的話,即可直接操作。
但這一點(diǎn)提醒到了我,我們能否將 show dialog 的邏輯放到主線程來做,MessageQueue 已經(jīng)有了 destroy 消息,如果我們再發(fā)一個 show dialog message 的話,那肯定是排在 destroy message 后面的(Message 會根據(jù) when 來整理鏈表),那么,先處理的 destroy message 會使 isDestroyed 為 true,這樣,我們的判斷就生效了,示例圖如下:

代碼則變?yōu)椋?/strong>
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()
}
}
});
}
});
}
缺點(diǎn):runOnUiThread 只對異步線程有效,因?yàn)樵谥骶€程會被直接執(zhí)行,并不會插入一條 message,解決辦法也有,如果當(dāng)前是在主線程的話,可以通過 handler 的方式發(fā)送一條 message,如 Handler(Looper.getMainLooper()).post()
總結(jié)
大部分場景都能通過 isFinish 和 isDestroyed 判斷來解決,但對于主線程做耗時任務(wù)導(dǎo)致 destroy message 沒有被正確處理情況,還是得回歸到應(yīng)用穩(wěn)定性治理層面,雖然能解決 badToken 問題,但本質(zhì)上應(yīng)用卡頓問題依然存在.
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Eclipse開發(fā)環(huán)境導(dǎo)入android sdk的sample中的源碼
初學(xué)Android編程,Android SDK中提供的Sample代碼自然是最好的學(xué)習(xí)材料,需要的朋友可以參考下2012-12-12
Android如何實(shí)現(xiàn)壓縮和解壓縮文件
這篇文章主要介紹了Android實(shí)現(xiàn)壓縮和解壓文件的實(shí)例代碼,涉及到批量壓縮文件夾,解壓縮一個文件等方面的知識點(diǎn),本文介紹的非常詳細(xì),具有參考借鑒價值,感興趣的朋友一起看下吧2016-05-05
Android Zxing生成二維碼經(jīng)典案例分享
這篇文章主要為大家分享了Android Zxing生成二維碼經(jīng)典案例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11
學(xué)習(xí)Android開發(fā)之RecyclerView使用初探
Android開發(fā)學(xué)習(xí)之路的第一課RecyclerView使用初探,感興趣的小伙伴們可以參考一下2016-07-07
Android實(shí)現(xiàn)拖動小球跟隨手指移動效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)拖動小球跟隨手指移動效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-03-03
Android TouchListener實(shí)現(xiàn)拖拽刪實(shí)例代碼
這篇文章主要介紹了Android TouchListener實(shí)現(xiàn)拖拽刪實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02
Android跳轉(zhuǎn)到系統(tǒng)聯(lián)系人及撥號或短信界面
現(xiàn)在開發(fā)中的功能需要直接跳轉(zhuǎn)到撥號、聯(lián)系人、短信界面等等,查找了很多資料,自己整理了一下特此分享到腳本之家平臺供大家參考2016-12-12
android開發(fā)教程之實(shí)現(xiàn)listview下拉刷新和上拉刷新效果
這篇文章主要介紹了android實(shí)現(xiàn)listview下拉刷新和上拉刷新效果,Android的ListView上拉下拉刷新,原理都一樣,在Touch事件中操作header/footer的paddingTop屬性,需要的朋友可以參考下2014-02-02

