您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關Android中BadTokenException異常怎么辦的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
線上出現了如上的 crash,第一解決反應是在 show dialog 之前做個 isFinish 和 isDestroyed 判斷,當我翻開代碼正要解決時,我驚了,原來已經做過了如上的判斷檢測,示例偽代碼如下:
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 分發 onDestroy 生命周期在 ActivityRecord 類(基于 Android 10 源碼):
1、第一個紅框調用 ApplicationThread binder 代理的 scheduleTransaction 方法,回執的生命周期為 DestroyActivityItem,scheduleTransaction 方法將包裹著 DestroyActivityItem 的 ClientTransaction 分發給 ActivityThread , ActivityThread 的父類會處理 scheduleTransaction ,并將 ClientTransaction 切換到主線程進行進行 Activity 的生命周期調度。為什么要把這個過程理清,后面解決部分會 hook 該過程
2、第二個紅框是 Destroy 生命周期超時處理,超時時間為 10s,如果分發給應用進程的 onDestroy 10s 內處理未結束,AMS 也會在超時的時候,將該 Activity 標記為已銷毀,并通知 WMS 刪除該 Activity 的 token。
通過這兩點,我們可以推理出我們應用當時處于什么環境:
AMS 已經將銷毀的指令告訴應用進程了,但應用進程一直在處理自己的事情,未處理 Destroy 生命周期(從業務代碼 > isDestroyed> = false 可知),然后 AMS 的 10s 超時機制到了,并通知 WMS 移除 token,然后我們的業務代碼異步請求網絡完成,判斷 isFinish 和 isDestroyed 都是有效的,然后就順理成章的執行了 show dialog 操作,發生了該異常。
我們可以畫個簡單的圖:
既然是 AMS 發的 destroy 消息被主線程的其他任務阻塞導致一直沒執行,那么,我們可以在 show dialog 的時候去檢查一下主線程的 MessageQueue,遍歷一下所有的 Message,看看里面有沒有 Destroy Message,如果有的話,說明當前會發生 badToken 異常。
查看了下 MessageQueue 的 mMessages 字段,發現該字段被標注為 UnsupportedAppUsage 注解,看起來不支持給 app 調用,先不管,我們先 hook 一番,代碼就不貼了,后面給出示例代碼,一頓操作猛如虎,發現是可以通過反射拿到 Message 的,然后接下來就可以通過遞歸遍歷 Message next,取出所有的 Message。
在拿到 Message 的同時,我們要怎么識別出這是個 Destroy Message 呢?
這要看不同的系統版本:
Android P 之前(不包括 P),destroy message 是通過給 Message.what = DESTROY_ACTIVITY 來進行分發的,DESTROY_ACTIVITY = 109,那么我們就可以判斷,只要 Message 中的 what 為 109 即可判斷當前是 Destroy Message。
Android P 之后(包括 P),AMS 的生命周期分發改了,不再是通過調用 ApplicationThread 的某個方法,然后根據 DESTROY_ACTIVITY 這種數值型來分發,而是全部統一走 ApplicationThread 的 scheduleTransaction 方法,生命周期標識是存放在參數 ClientTransaction 中,在切換到主線程時,會執行 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 驗證如下,destroy message 被成功拿到:
那么我們的業務代碼的判斷就可以改造成:
public void showDialog(Activity activity){ new OkHttp().call(new Callback(){ void onSucess(Response resp){ if(activity!=null && !activity.isFinishing() && !activity.isDestroed() // 多加一條判斷,判斷當前消息隊列中沒有 destroy message && !BadTokenUtils.isOnDestroyMsgExit() ){ new Dialog().show() } } }) }
這種方式有個缺點,大量的 hook message 會造成應用的不穩定性。
業務代碼是在請求網絡成功的時候進行的 dialog 展示,這時候又有人問了,這是在子線程,怎么能 show dialog 呢?其實不然,ViewRoomImpl 檢驗線程,是判斷創建 ViewRootImpl 時的線程與 requestLayout 的線程一致,是一樣的話,即可直接操作。
但這一點提醒到了我,我們能否將 show dialog 的邏輯放到主線程來做,MessageQueue 已經有了 destroy 消息,如果我們再發一個 show dialog message 的話,那肯定是排在 destroy message 后面的(Message 會根據 when 來整理鏈表),那么,先處理的 destroy message 會使 isDestroyed 為 true,這樣,我們的判斷就生效了,示例圖如下:
代碼則變為:
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() } } }); } }); }
缺點:runOnUiThread 只對異步線程有效,因為在主線程會被直接執行,并不會插入一條 message,解決辦法也有,如果當前是在主線程的話,可以通過 handler 的方式發送一條 message,如 Handler(Looper.getMainLooper()).post()
感謝各位的閱讀!關于“Android中BadTokenException異常怎么辦”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。