WindowManager$BadTokenException-解決方案

簡介

上一篇分析了WindowManager$BadTokenException發(fā)生的原因,帶大家一起通過分析WindowManager源碼搪桂,更加深入的了解了WindowManager添加window的過程透敌,以及在使用WindowManager添加自己的window或者View的時候,怎么去避免發(fā)生異常踢械,接下來酗电,繼續(xù)深入分析WindowManager源碼,帶大家一起尋找裸燎,解決平時使用WindowManager出現(xiàn)的各種異常的辦法顾瞻。

\color{blue}{建議:}本文章講解,建立在上一篇文章的基礎(chǔ)之上德绿,所以閱讀本文章前荷荤,請先閱讀上一篇文章,地址如下:
WindowManager$BadTokenException(WindowManager源碼分析

源碼版本

在沒有特別說明的情況下移稳,源碼版本如下:

  • sdk:android-28
  • Android系統(tǒng)源碼:Android8.0

window類型

Window有三種類型蕴纳,分別是應(yīng)用Window,子Window和系統(tǒng)Window个粱。

  • 應(yīng)用類Window對應(yīng)著一個Activity古毛。

  • 子Window不能單獨(dú)存在,它需要附屬在特定的父Window中都许,比如Dialog就是一個子Window稻薇。

  • 系統(tǒng)Window是需要聲明權(quán)限才能創(chuàng)建的Window,比如Toast和系統(tǒng)狀態(tài)欄這些都是系統(tǒng)Window胶征。

    Window是分層的塞椎,每個Window都有對應(yīng)的z-ordered,層級大的會覆蓋    在層級小的Window上睛低。在三類    
    Window中案狠,應(yīng)用Window的層級范圍是1~99,子Window的層級范圍是1000~1999钱雷,系統(tǒng)Window的層級范 
    圍是2000~2999骂铁。很顯然系統(tǒng)Window的層級是最大的,而且系統(tǒng)層級有很多值罩抗,一般我們可以選用 
    TYPE_SYSTEM_ERROR或者TYPE_SYSTEM_OVERLAY拉庵,另外重要的是要記得在清單文件中聲明權(quán)限。
    

系統(tǒng)Window

由于源碼比較多套蒂,只講解關(guān)鍵或者不容易判斷的代碼名段,其它可以自行查看阱扬。

  • 函數(shù) :addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow)
    源碼文件:WindowManagerGlobal.java
  1. root = new ViewRootImpl(view.getContext(), display);
    創(chuàng)建ViewRootImpl泣懊,看一下里面創(chuàng)建的幾個關(guān)鍵的對象
    (1) mWindow = new W(this);
    (2) mWindowSession = WindowManagerGlobal.getWindowSession();
  • 函數(shù):setView(View view, WindowManager.LayoutParams attrs, View panelParentView)
    源碼文件:ViewRootImpl.java
  1. 參數(shù)變化:
    (1) attrs
    mWindowAttributes.copyFrom(attrs);
    if (mWindowAttributes.packageName == null) {
        mWindowAttributes.packageName = mBasePackageName;
    }
    attrs = mWindowAttributes;
    
  • 函數(shù):addWindow(Session session, IWindow client, int seq,WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets,InputChannel outInputChannel)
  1. 參數(shù)
    (1)session: mWindowSession
    (2)client:mWindow
    (3)attrs:mWindowAttributes包含了傳入時WindowManager.LayoutParams參數(shù)的所有屬性

  2. 權(quán)限檢測: int res = mPolicy.checkAddPermission(attrs, appOp)
    函數(shù):checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp)
    源碼路徑:frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
    (1)如果不在window類型里伸辟,返回?zé)o效類型 —— ADD_INVALID_TYPE

     if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)
              || (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
              || (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {
          return WindowManagerGlobal.ADD_INVALID_TYPE;
      }
    

    (2)不是系統(tǒng)窗體類型(即應(yīng)用window和子window)和高于最后一個系統(tǒng)window類型的,直接返回 —— ADD_OKAY,不再進(jìn)行權(quán)限檢測。

     if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
          // Window manager will make sure these are okay.
          return ADD_OKAY;
      }
    

    (3) 如果不是系統(tǒng)彈窗window砚亭,除了一下類型在刺,者需要INTERNAL_SYSTEM_WINDOW權(quán)限(系統(tǒng)app才能申請的權(quán)限)

      //WindowManager.java
      public static boolean isSystemAlertWindowType(int type) {
          switch (type) {
              case TYPE_PHONE:
              case TYPE_PRIORITY_PHONE:
              case TYPE_SYSTEM_ALERT:
              case TYPE_SYSTEM_ERROR:
              case TYPE_SYSTEM_OVERLAY:
              case TYPE_APPLICATION_OVERLAY:
                  return true;
          }
          return false;
      }
    
      if (!isSystemAlertWindowType(type)) {
          switch (type) {
              case TYPE_TOAST:
                  outAppOp[0] = OP_TOAST_WINDOW;
                  return ADD_OKAY;
              case TYPE_DREAM:
              case TYPE_INPUT_METHOD:
              case TYPE_WALLPAPER:
              case TYPE_PRESENTATION:
              case TYPE_PRIVATE_PRESENTATION:
              case TYPE_VOICE_INTERACTION:
              case TYPE_ACCESSIBILITY_OVERLAY:
              case TYPE_QS_DIALOG:
                  // The window manager will check these.
                  return ADD_OKAY;
          }
          return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
                  == PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;
      }
    
      if (appInfo == null || (type != TYPE_APPLICATION_OVERLAY && appInfo.targetSdkVersion >= O)) {
          return (mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
                  == PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
      }
    

    (4) 檢測app是否申明或者在設(shè)備里面以及打開了權(quán)限

      final int mode = mAppOpsManager.checkOpNoThrow(outAppOp[0], callingUid, attrs.packageName);
      switch (mode) {
          case AppOpsManager.MODE_ALLOWED:
          case AppOpsManager.MODE_IGNORED:
              return ADD_OKAY;
          case AppOpsManager.MODE_ERRORED:
              if (appInfo.targetSdkVersion < M) {
                  return ADD_OKAY;
              }
              return ADD_PERMISSION_DENIED;
          default:
            //默認(rèn)需要SYSTEM_ALERT_WINDOW權(quán)限
              return (mContext.checkCallingOrSelfPermission(SYSTEM_ALERT_WINDOW)
                      == PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
        ```
    
  3. 類型檢測
    (1)callingUid,type
    \color{blue}{注意:}現(xiàn)在是在系統(tǒng)進(jìn)程里面睹耐,window添加過程其實(shí)是跨進(jìn)程的,這句話意思是獲取調(diào)用進(jìn)程的uid,即我們app所在的進(jìn)程uid
    final int callingUid = Binder.getCallingUid();
    //這個是我們傳入的window類型
    final int type = attrs.type;
    (2) mWindowMap

        final WindowState win = new WindowState(this, session, client, token, parentWindow,
                 appOp[0], seq, attrs, viewVisibility, session.mUid,
                 session.mCanAddInternalSystemWindow);
        //client:mWindow
        mWindowMap.put(client.asBinder(), win);
    
  4. 異常:
    (1) Unable to add window -- window mWindow has already been added

         if (mWindowMap.containsKey(client.asBinder())) {
             return WindowManagerGlobal.ADD_DUPLICATE_ADD;
         }
    

意思是同一個window添加多次振湾,但是通過addview方法,都會重新創(chuàng)建ViewRootImpl對象亡脸,然后重新創(chuàng)建mWindow押搪,所以應(yīng)該不會報這個錯。
(2) Unable to add window -- token attrs.token is not valid; is your activity running?

     ```
        if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
            parentWindow = windowForClientLocked(null, attrs.token, false);
            if (parentWindow == null) {
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
            if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
                    && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
        }
    ```

子window必須依賴于父window浅碾,并且父window不能是子window類型

  1. 檢測window類型合法性

         AppWindowToken atoken = null;
         //系統(tǒng)window類型大州,parentWindow是null
         final boolean hasParent = parentWindow != null;
        //獲取token,點(diǎn)擊方法垂谢,里面是和hashmap集合厦画,里面存儲的是activity在啟動的時候,創(chuàng)建的token滥朱,所  
        //以在這里獲取到的是null根暑,因?yàn)閍ttrs里面taken為null,如果自己構(gòu)造一個徙邻,其實(shí)也應(yīng)該是null
         WindowToken token = displayContent.getWindowToken( hasParent ? parentWindow.mAttrs.token : attrs.token);
         // If this is a child window, we want to apply the same type checking rules as the
         // parent window type.
         final int rootType = hasParent ? parentWindow.mAttrs.type : type;
         boolean addToastWindowRequiresToken = false;
    

經(jīng)過上面的分析排嫌,我平時通過獲取windowmanager,然后添加view的操作鹃栽,應(yīng)該都會進(jìn)入token == null這個條件中躏率,知道怎么才會報異常,那么接下來就知道怎么去應(yīng)對了民鼓。當(dāng)然薇芝,不同的Android系統(tǒng)版本,邏輯是有差異的丰嘉,總得來說夯到,系統(tǒng)版本越高,控制得越嚴(yán)格饮亏。具體的解決方案耍贾,請繼續(xù)往下看阅爽,我會在最后講解,如果只是想知道解決辦法荐开,可以直接拉到最后查看付翁。

\color{red}{疑問:} 為什么Toast不會報異常:

  • Toast簡單的源碼分析
    Toast其實(shí)也是用的windowmanager添加我們view實(shí)現(xiàn)的,而且type是TYPE_TOAST晃听,但是它為什么不會出現(xiàn)之前說的哪些異常呢百侧,其實(shí)toast最大的不同就是,在toast添加window之前會先和windowmanagerservice進(jìn)行通信能扒,然后會返回一個binder對象(即token)佣渴,然后在addwindow的時候一起帶過去。下面就一起看看:
    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

函數(shù):
源碼路徑:enqueueToast(String pkg, ITransientNotification callback, int duration) frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            ....
            synchronized (mToastQueue) {
                ...
                    // If the package already has a toast, we update its toast
                    // in the queue, we don't move it to the end of the queue.
                    if (index >= 0) {
                        ...
                    } else {
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                    }
                    keepProcessAliveIfNeededLocked(callingPid);
                    ...
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                record.callback.show(record.token);
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
                ...
        }
    }

關(guān)鍵代碼: mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);

  mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
  //rameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
  LocalServices.addService(WindowManagerInternal.class, new LocalService());
  LocalService是WindowManagerService的內(nèi)部類

   @Override
    public void addWindowToken(IBinder token, int type, int displayId) {
        WindowManagerService.this.addWindowToken(token, type, displayId);
    }

@Override
public void addWindowToken(IBinder binder, int type, int displayId) {
    if (!checkCallingPermission(MANAGE_APP_TOKENS, "addWindowToken()")) {
        throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
    }

    synchronized(mWindowMap) {
        final DisplayContent dc = mRoot.getDisplayContentOrCreate(displayId);
        WindowToken token = dc.getWindowToken(binder);
        if (token != null) {
            ...
            return;
        }
        if (type == TYPE_WALLPAPER) {
            new WallpaperWindowToken(this, binder, true, dc,
                    true /* ownerCanManageAppTokens */);
        } else {
            new WindowToken(this, binder, type, true, dc, true /* ownerCanManageAppTokens */);
        }

上面會將toast的token添加到DisplayContent里面初斑,所以在上面獲取的token的時候就不為null辛润,這樣就不會出現(xiàn)進(jìn)入token == null時的異常,由于toast是維護(hù)在一個隊(duì)列里面见秤,下一個顯示前砂竖,上一個時已經(jīng)被移除,所以不會出現(xiàn)同時顯示兩個懸浮窗的情況秦叛,自然不會出現(xiàn)之前的說的異常晦溪。所以,可以模仿toast的隊(duì)列挣跋,來防止同時彈處兩個懸浮窗而導(dǎo)致的崩潰三圆。

解決方案

我們來看一下,可以使用哪些系統(tǒng)類型避咆,這里只列舉常用的系統(tǒng)類型舟肉,類型實(shí)在太多了。根據(jù)if (!isSystemAlertWindowType(type)) 這個判斷查库,其實(shí)我們可以把類型鎖定到這些上:

  TYPE_PHONE路媚,TYPE_PRIORITY_PHONE,TYPE_SYSTEM_ALERT樊销,TYPE_SYSTEM_ERROR整慎,TYPE_SYSTEM_OVERLAY,TYPE_APPLICATION_OVERLAY围苫,TYPE_TOAST

這里面只有TYPE_TOAST不需要 "SYSTEM_ALERT_WINDOW"權(quán)限裤园,但是在不同的版本會有不同的限制。

  • 用戶已經(jīng)授予了 "SYSTEM_ALERT_WINDOW"權(quán)限
  1. 系統(tǒng)版本 >= O
    這種情況下上面的所以類型按理都是可以使用的剂府,但是TYPE_TOAST在targetSdkVersion >= 26時拧揽,是不能直接添加window的,而且在sdk 26后,google推薦使用TYPE_APPLICATION_OVERLAY淤袜,所以在有權(quán)限和系統(tǒng)版本在O或者以上時痒谴,可以用TYPE_APPLICATION_OVERLAY。
  2. 系統(tǒng)版本 < O
    這個時候就不能用TYPE_APPLICATION_OVERLAY铡羡,那么其實(shí)我們可以用TYPE_SYSTEM_ALERT积蔚。

\color{blue}{綜上:}有權(quán)限的情況下,其實(shí)還是比較好處理的蓖墅,這樣就不會出現(xiàn)用TYPE_TOAST時出現(xiàn)的異常

  • 用戶沒有授予了 "SYSTEM_ALERT_WINDOW"權(quán)限
    這種情況下库倘,沒辦法了,就不能用上面的系統(tǒng)類型了论矾,那我們可以用TYPE_TOAST,這個類型是不需要特殊權(quán)限的杆勇,使用TYPE_TOAST有兩種方法贪壳,直接有系統(tǒng)的Toast類,二是還是像上面那樣蚜退,只是類型指定為TYPE_TOAST闰靴。
  1. 問題:
    使用TYPE_TOAST,在android8.0及以上钻注,是不能直接添加window蚂且,在Android8.0以下的某些版本,在上一個沒有移除前幅恋,是不能繼續(xù)添加下一個的杏死,當(dāng)然可以模仿toast的方式,但是捆交,這個需要hook系統(tǒng)的方法淑翼,所以可能存在兼容性問題,使用Toast品追,但是Toast顯示時間有限制玄括,而且默認(rèn)是不接收觸摸事件的,當(dāng)然可以通過反射去修改肉瓦。

分析完遭京,感覺大腦已經(jīng)缺氧了,這里說一解決思路:
\color{blue}{思路:}

  • 有SYSTEM_ALERT_WINDOW權(quán)限泞莉,請看上面哪雕。
  • 沒有SYSTEM_ALERT_WINDOW權(quán)限。
    1. 如果需求比較簡單戒财,其實(shí)可以使用系統(tǒng)的Toast
    2. android O以下热监,可以使用TYPE_TOAST,但是要保證上一個window移除后才能添加下一個饮寞。
    3. 通過反射修改Toast屬性孝扛,比如顯示時長列吼,接收觸摸事件,這樣有沒有權(quán)限或者不同的版本苦始,都是ok的寞钥,當(dāng)然得注意一下Android9.0。
    4. 模仿Toast陌选,自己創(chuàng)建token對象理郑,hook系統(tǒng)類(比如WindowManagerService,LocalServices等系統(tǒng)類咨油,獲取WindowManagerService對象)您炉,加入到對應(yīng)的數(shù)組里面
    5. 在addview處加try{}catch{},其實(shí)Toast在addview的時候都使用了try{}catch{}的
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市役电,隨后出現(xiàn)的幾起案子赚爵,更是在濱河造成了極大的恐慌,老刑警劉巖法瑟,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冀膝,死亡現(xiàn)場離奇詭異,居然都是意外死亡霎挟,警方通過查閱死者的電腦和手機(jī)窝剖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酥夭,“玉大人赐纱,你說我怎么就攤上這事〔衫桑” “怎么了千所?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蒜埋。 經(jīng)常有香客問我淫痰,道長,這世上最難降的妖魔是什么整份? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任待错,我火速辦了婚禮,結(jié)果婚禮上烈评,老公的妹妹穿的比我還像新娘火俄。我一直安慰自己,他們只是感情好讲冠,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布瓜客。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谱仪。 梳的紋絲不亂的頭發(fā)上玻熙,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音疯攒,去河邊找鬼嗦随。 笑死,一個胖子當(dāng)著我的面吹牛敬尺,可吹牛的內(nèi)容都是我干的枚尼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼砂吞,長吁一口氣:“原來是場噩夢啊……” “哼署恍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起呜舒,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤锭汛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后袭蝗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡般婆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年到腥,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蔚袍。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡乡范,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出啤咽,到底是詐尸還是另有隱情晋辆,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布宇整,位于F島的核電站瓶佳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鳞青。R本人自食惡果不足惜霸饲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望臂拓。 院中可真熱鬧厚脉,春花似錦、人聲如沸胶惰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至中捆,卻和暖如春鸯匹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背轨香。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工忽你, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人臂容。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓科雳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親脓杉。 傳聞我的和親對象是個殘疾皇子糟秘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344