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)文章
Android 自定義圓形帶刻度漸變色的進(jìn)度條樣式實(shí)例代碼
這篇文章主要介紹了Android 自定義圓形帶刻度漸變色的進(jìn)度條的實(shí)例代碼,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-11-11Android ListView與getView調(diào)用卡頓問題解決辦法
這篇文章主要介紹了Android ListView與getView調(diào)用卡頓問題解決辦法的相關(guān)資料,這里提供實(shí)例及解決辦法幫助大家解決這種問題,需要的朋友可以參考下2017-08-08Android使用Gradle依賴配置compile、implementation與api的區(qū)別介紹
這篇文章主要給大家介紹了關(guān)于Android使用Gradle依賴配置compile、implementation與api區(qū)別的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起看看吧2018-09-09詳解Android中Glide與CircleImageView加載圓形圖片的問題
本篇文章主要介紹了詳解Android中Glide與CircleImageView加載圓形圖片的問題,具有一定的參考價值,有興趣的可以了解一下2017-09-09Android之自定義實(shí)現(xiàn)BaseAdapter(通用適配器二)
這篇文章主要為大家詳細(xì)介紹了Android之自定義實(shí)現(xiàn)BaseAdapter通用適配器第二篇,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-08-08Android編程使用AlarmManager設(shè)置鬧鐘的方法
這篇文章主要介紹了Android編程使用AlarmManager設(shè)置鬧鐘的方法,結(jié)合具體實(shí)例分析了Android基于AlarmManager實(shí)現(xiàn)鬧鐘功能的設(shè)置、取消、顯示等相關(guān)操作技巧,需要的朋友可以參考下2017-03-03