詳解BadTokenException報(bào)錯(cuò)解決方法
線上出現(xiàn)了如上的 crash,第一解決反應(yīng)是在 show dialog 之前做個(gè) isFinish 和 isDestroyed 判斷,當(dāng)我翻開代碼正要解決時(shí),我驚了,原來(lái)已經(jīng)做過了如上的判斷檢測(cè),示例偽代碼如下:
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、第一個(gè)紅框調(diào)用 ApplicationThread binder 代理的 scheduleTransaction 方法,回執(zhí)的生命周期為 DestroyActivityItem,scheduleTransaction 方法將包裹著 DestroyActivityItem 的 ClientTransaction 分發(fā)給 ActivityThread , ActivityThread 的父類會(huì)處理 scheduleTransaction ,并將 ClientTransaction 切換到主線程進(jìn)行進(jìn)行 Activity 的生命周期調(diào)度。為什么要把這個(gè)過程理清,后面解決部分會(huì) hook 該過程
2、第二個(gè)紅框是 Destroy 生命周期超時(shí)處理,超時(shí)時(shí)間為 10s,如果分發(fā)給應(yīng)用進(jìn)程的 onDestroy 10s 內(nèi)處理未結(jié)束,AMS 也會(huì)在超時(shí)的時(shí)候,將該 Activity 標(biāo)記為已銷毀,并通知 WMS 刪除該 Activity 的 token。
通過這兩點(diǎn),我們可以推理出我們應(yīng)用當(dāng)時(shí)處于什么環(huán)境:
AMS 已經(jīng)將銷毀的指令告訴應(yīng)用進(jìn)程了,但應(yīng)用進(jìn)程一直在處理自己的事情,未處理 Destroy 生命周期(從業(yè)務(wù)代碼 > isDestroyed> = false 可知),然后 AMS 的 10s 超時(shí)機(jī)制到了,并通知 WMS 移除 token,然后我們的業(yè)務(wù)代碼異步請(qǐng)求網(wǎng)絡(luò)完成,判斷 isFinish 和 isDestroyed 都是有效的,然后就順理成章的執(zhí)行了 show dialog 操作,發(fā)生了該異常。
我們可以畫個(gè)簡(jiǎn)單的圖:
解決辦法1
既然是 AMS 發(fā)的 destroy 消息被主線程的其他任務(wù)阻塞導(dǎo)致一直沒執(zhí)行,那么,我們可以在 show dialog 的時(shí)候去檢查一下主線程的 MessageQueue,遍歷一下所有的 Message,看看里面有沒有 Destroy Message,如果有的話,說明當(dāng)前會(huì)發(fā)生 badToken 異常。
查看了下 MessageQueue 的 mMessages 字段,發(fā)現(xiàn)該字段被標(biāo)注為 UnsupportedAppUsage
注解,看起來(lái)不支持給 app 調(diào)用,先不管,我們先 hook 一番,代碼就不貼了,后面給出示例代碼,一頓操作猛如虎,發(fā)現(xiàn)是可以通過反射拿到 Message 的,然后接下來(lái)就可以通過遞歸遍歷 Message next,取出所有的 Message。
在拿到 Message 的同時(shí),我們要怎么識(shí)別出這是個(gè) Destroy Message 呢?
這要看不同的系統(tǒng)版本:
- Android P 之前(不包括 P),destroy message 是通過給 Message.what = DESTROY_ACTIVITY 來(lái)進(jìn)行分發(fā)的,DESTROY_ACTIVITY = 109,那么我們就可以判斷,只要 Message 中的 what 為 109 即可判斷當(dāng)前是 Destroy Message。
- Android P 之后(包括 P),AMS 的生命周期分發(fā)改了,不再是通過調(diào)用 ApplicationThread 的某個(gè)方法,然后根據(jù) DESTROY_ACTIVITY 這種數(shù)值型來(lái)分發(fā),而是全部統(tǒng)一走 ApplicationThread 的 scheduleTransaction 方法,生命周期標(biāo)識(shí)是存放在參數(shù) ClientTransaction 中,在切換到主線程時(shí),會(huì)執(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() } } }) }
這種方式有個(gè)缺點(diǎn),大量的 hook message 會(huì)造成應(yīng)用的不穩(wěn)定性。
解決方法2
業(yè)務(wù)代碼是在請(qǐng)求網(wǎng)絡(luò)成功的時(shí)候進(jìn)行的 dialog 展示,這時(shí)候又有人問了,這是在子線程,怎么能 show dialog 呢?其實(shí)不然,ViewRoomImpl 檢驗(yàn)線程,是判斷創(chuàng)建 ViewRootImpl 時(shí)的線程與 requestLayout 的線程一致,是一樣的話,即可直接操作。
但這一點(diǎn)提醒到了我,我們能否將 show dialog 的邏輯放到主線程來(lái)做,MessageQueue 已經(jīng)有了 destroy 消息,如果我們?cè)侔l(fā)一個(gè) show dialog message 的話,那肯定是排在 destroy message 后面的(Message 會(huì)根據(jù) when 來(lái)整理鏈表),那么,先處理的 destroy message 會(huì)使 isDestroyed 為 true,這樣,我們的判斷就生效了,示例圖如下:
代碼則變?yōu)椋?/p>
public void showDialog(Activity activity){ new OkHttp().call(new Callback(){ void onSucess(Response resp){ // 先判斷一次 if(activity!=null && !activity.isFinishing() && !activity.isDestroed() ){ // 切到主線程,post 一個(gè) message 給 MQ activity.runOnUiThread(new Runnable() { @Override public void run() { // 再判斷一次 if(activity!=null && !activity.isFinishing() && !activity.isDestroed() ){ new Dialog().show() } } }); } }); }
缺點(diǎn):runOnUiThread 只對(duì)異步線程有效,因?yàn)樵谥骶€程會(huì)被直接執(zhí)行,并不會(huì)插入一條 message,解決辦法也有,如果當(dāng)前是在主線程的話,可以通過 handler 的方式發(fā)送一條 message,如 Handler(Looper.getMainLooper()).post()
總結(jié)
大部分場(chǎng)景都能通過 isFinish 和 isDestroyed 判斷來(lái)解決,但對(duì)于主線程做耗時(shí)任務(wù)導(dǎo)致 destroy message 沒有被正確處理情況,還是得回歸到應(yīng)用穩(wěn)定性治理層面,雖然能解決 badToken 問題,但本質(zhì)上應(yīng)用卡頓問題依然存在.
到此這篇關(guān)于詳解BadTokenException報(bào)錯(cuò)解決方法的文章就介紹到這了,更多相關(guān)解決BadTokenException內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Android報(bào)錯(cuò)Error:Could not find com.android.tools.build:gradle:4.1解決辦法
- 解決android報(bào)錯(cuò):Intel HAXM is required to run this AVD
- cocos2d-2.0-x-2.0.3 交叉編譯到android報(bào)錯(cuò)解決
- Android崩潰異常捕獲方法
- SpringBoot啟動(dòng)yaml報(bào)錯(cuò)的解決
- 解決springboot報(bào)錯(cuò)找不到自動(dòng)注入的service問題
- springBoot集成Elasticsearch 報(bào)錯(cuò) Health check failed的解決
相關(guān)文章
Android優(yōu)質(zhì)索尼滾動(dòng)相冊(cè)
這篇文章主要介紹了Android優(yōu)質(zhì)索尼滾動(dòng)相冊(cè),桌面小部件滾動(dòng)相冊(cè),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09Android4.4+ 實(shí)現(xiàn)半透明狀態(tài)欄(Translucent Bars)
這篇文章主要為大家詳細(xì)介紹了Android4.4+ 實(shí)現(xiàn)半透明狀態(tài)欄,對(duì)狀態(tài)欄(Status Bar)和下方導(dǎo)航欄(Navigation Bar)進(jìn)行半透明處理,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09Android自定義EditText實(shí)現(xiàn)登錄界面
這篇文章主要為大家詳細(xì)介紹了Android自定義EditText實(shí)現(xiàn)登錄界面,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Android編程實(shí)現(xiàn)添加低電流提醒功能的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)添加低電流提醒功能的方法,涉及Android廣播監(jiān)聽及電源監(jiān)控等相關(guān)操作技巧,需要的朋友可以參考下2017-09-09Android獲取手機(jī)位置的實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android獲取手機(jī)位置的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android進(jìn)階之使用時(shí)間戳計(jì)算時(shí)間差
這篇文章主要為大家詳細(xì)介紹了Android進(jìn)階之使用時(shí)間戳計(jì)算時(shí)間差,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Android 監(jiān)聽網(wǎng)絡(luò)狀態(tài)方法詳解
這篇文章主要介紹了Android 監(jiān)聽網(wǎng)絡(luò)狀態(tài)方法詳解的相關(guān)資料,需要的朋友可以參考下2017-07-07Android編程簡(jiǎn)單獲取網(wǎng)絡(luò)上的圖片
這篇文章主要介紹了Android編程簡(jiǎn)單獲取網(wǎng)絡(luò)上的圖片,結(jié)合實(shí)例形式分析了Android獲取網(wǎng)絡(luò)圖片及加載顯示的相關(guān)操作步驟與注意事項(xiàng),需要的朋友可以參考下2016-10-10