溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android中BadTokenException異常怎么辦

發(fā)布時間:2021-08-31 10:28:45 來源:億速云 閱讀:129 作者:小新 欄目:開發(fā)技術(shù)

這篇文章給大家分享的是有關(guān)Android中BadTokenException異常怎么辦的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

Android中BadTokenException異常怎么辦

線上出現(xiàn)了如上的 crash,第一解決反應是在 show dialog 之前做個 isFinish 和 isDestroyed 判斷,當我翻開代碼正要解決時,我驚了,原來已經(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 源碼):

Android中BadTokenException異常怎么辦

1、第一個紅框調(diào)用 ApplicationThread binder 代理的 scheduleTransaction 方法,回執(zhí)的生命周期為 DestroyActivityItem,scheduleTransaction 方法將包裹著 DestroyActivityItem 的 ClientTransaction 分發(fā)給 ActivityThread , ActivityThread 的父類會處理 scheduleTransaction ,并將 ClientTransaction 切換到主線程進行進行 Activity 的生命周期調(diào)度。為什么要把這個過程理清,后面解決部分會 hook 該過程

2、第二個紅框是 Destroy 生命周期超時處理,超時時間為 10s,如果分發(fā)給應用進程的 onDestroy 10s 內(nèi)處理未結(jié)束,AMS 也會在超時的時候,將該 Activity 標記為已銷毀,并通知 WMS 刪除該 Activity 的 token。

通過這兩點,我們可以推理出我們應用當時處于什么環(huán)境:

AMS 已經(jīng)將銷毀的指令告訴應用進程了,但應用進程一直在處理自己的事情,未處理 Destroy 生命周期(從業(yè)務代碼 > isDestroyed> = false 可知),然后 AMS 的 10s 超時機制到了,并通知 WMS 移除 token,然后我們的業(yè)務代碼異步請求網(wǎng)絡完成,判斷 isFinish 和 isDestroyed 都是有效的,然后就順理成章的執(zhí)行了 show dialog 操作,發(fā)生了該異常。

我們可以畫個簡單的圖:

Android中BadTokenException異常怎么辦

解決辦法1

既然是 AMS 發(fā)的 destroy 消息被主線程的其他任務阻塞導致一直沒執(zhí)行,那么,我們可以在 show dialog 的時候去檢查一下主線程的 MessageQueue,遍歷一下所有的 Message,看看里面有沒有 Destroy Message,如果有的話,說明當前會發(fā)生 badToken 異常。

查看了下 MessageQueue 的 mMessages 字段,發(fā)現(xiàn)該字段被標注為 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 來進行分發(fā)的,DESTROY_ACTIVITY = 109,那么我們就可以判斷,只要 Message 中的 what 為 109 即可判斷當前是 Destroy Message。

  • Android P 之后(包括 P),AMS 的生命周期分發(fā)改了,不再是通過調(diào)用 ApplicationThread 的某個方法,然后根據(jù) DESTROY_ACTIVITY 這種數(shù)值型來分發(fā),而是全部統(tǒng)一走 ApplicationThread 的 scheduleTransaction 方法,生命周期標識是存放在參數(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 驗證如下,destroy message 被成功拿到:

Android中BadTokenException異常怎么辦

那么我們的業(yè)務代碼的判斷就可以改造成:

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 會造成應用的不穩(wěn)定性。

解決方法2

業(yè)務代碼是在請求網(wǎng)絡成功的時候進行的 dialog 展示,這時候又有人問了,這是在子線程,怎么能 show dialog 呢?其實不然,ViewRoomImpl 檢驗線程,是判斷創(chuàng)建 ViewRootImpl 時的線程與 requestLayout 的線程一致,是一樣的話,即可直接操作。

但這一點提醒到了我,我們能否將 show dialog 的邏輯放到主線程來做,MessageQueue 已經(jīng)有了 destroy 消息,如果我們再發(fā)一個 show dialog message 的話,那肯定是排在 destroy message 后面的(Message 會根據(jù) when 來整理鏈表),那么,先處理的 destroy message 會使 isDestroyed 為 true,這樣,我們的判斷就生效了,示例圖如下:

Android中BadTokenException異常怎么辦

代碼則變?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()   
                    }
                 }
              });
           }
    });
}

缺點:runOnUiThread 只對異步線程有效,因為在主線程會被直接執(zhí)行,并不會插入一條 message,解決辦法也有,如果當前是在主線程的話,可以通過 handler 的方式發(fā)送一條 message,如 Handler(Looper.getMainLooper()).post()

感謝各位的閱讀!關(guān)于“Android中BadTokenException異常怎么辦”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI