WindowManager$BadTokenException-解決方案

簡介

上一篇分析了WindowManager$BadTokenException發生的原因,帶大家一起通過分析WindowManager源碼,更加深入的瞭解了WindowManager添加window的過程,以及在使用WindowManager添加自己的window或者View的時候,怎麼去避免發生異常,接下來,繼續深入分析WindowManager源碼,帶大家一起尋找,解決平時使用WindowManager出現的各種異常的辦法。

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

源碼版本

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

  • sdk:android-28
  • Android系統源碼:Android8.0

window類型

Window有三種類型,分別是應用Window,子Window和系統Window。

  • 應用類Window對應着一個Activity。

  • 子Window不能單獨存在,它需要附屬在特定的父Window中,比如Dialog就是一個子Window。

  • 系統Window是需要聲明權限才能創建的Window,比如Toast和系統狀態欄這些都是系統Window。

    Window是分層的,每個Window都有對應的z-ordered,層級大的會覆蓋    在層級小的Window上。在三類    
    Window中,應用Window的層級範圍是1~99,子Window的層級範圍是1000~1999,系統Window的層級範 
    圍是2000~2999。很顯然系統Window的層級是最大的,而且系統層級有很多值,一般我們可以選用 
    TYPE_SYSTEM_ERROR或者TYPE_SYSTEM_OVERLAY,另外重要的是要記得在清單文件中聲明權限。
    

系統Window

由於源碼比較多,只講解關鍵或者不容易判斷的代碼,其它可以自行查看。

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

  2. 權限檢測: int res = mPolicy.checkAddPermission(attrs, appOp)
    函數:checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp)
    源碼路徑:frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
    (1)如果不在window類型裏,返回無效類型 —— 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)不是系統窗體類型(即應用window和子window)和高於最後一個系統window類型的,直接返回 —— ADD_OKAY,不再進行權限檢測。

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

    (3) 如果不是系統彈窗window,除了一下類型,者需要INTERNAL_SYSTEM_WINDOW權限(系統app才能申請的權限)

      //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是否申明或者在設備裏面以及打開了權限

      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:
            //默認需要SYSTEM_ALERT_WINDOW權限
              return (mContext.checkCallingOrSelfPermission(SYSTEM_ALERT_WINDOW)
                      == PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
        ```
    
  3. 類型檢測
    (1)callingUid,type
    \color{blue}{注意:}現在是在系統進程裏面,window添加過程其實是跨進程的,這句話意思是獲取調用進程的uid,即我們app所在的進程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方法,都會重新創建ViewRootImpl對象,然後重新創建mWindow,所以應該不會報這個錯。
(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;
         //系統window類型,parentWindow是null
         final boolean hasParent = parentWindow != null;
        //獲取token,點擊方法,裏面是和hashmap集合,裏面存儲的是activity在啓動的時候,創建的token,所  
        //以在這裏獲取到的是null,因爲attrs裏面taken爲null,如果自己構造一個,其實也應該是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;
    

經過上面的分析,我平時通過獲取windowmanager,然後添加view的操作,應該都會進入token == null這個條件中,知道怎麼纔會報異常,那麼接下來就知道怎麼去應對了。當然,不同的Android系統版本,邏輯是有差異的,總得來說,系統版本越高,控制得越嚴格。具體的解決方案,請繼續往下看,我會在最後講解,如果只是想知道解決辦法,可以直接拉到最後查看。

\color{red}{疑問:} 爲什麼Toast不會報異常:

  • Toast簡單的源碼分析
    Toast其實也是用的windowmanager添加我們view實現的,而且type是TYPE_TOAST,但是它爲什麼不會出現之前說的哪些異常呢,其實toast最大的不同就是,在toast添加window之前會先和windowmanagerservice進行通信,然後會返回一個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
        }
    }

函數:
源碼路徑: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) {
                ...
        }
    }

關鍵代碼: 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的內部類

   @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,這樣就不會出現進入token == null時的異常,由於toast是維護在一個隊列裏面,下一個顯示前,上一個時已經被移除,所以不會出現同時顯示兩個懸浮窗的情況,自然不會出現之前的說的異常。所以,可以模仿toast的隊列,來防止同時彈處兩個懸浮窗而導致的崩潰。

解決方案

我們來看一下,可以使用哪些系統類型,這裏只列舉常用的系統類型,類型實在太多了。根據if (!isSystemAlertWindowType(type)) 這個判斷,其實我們可以把類型鎖定到這些上:

  TYPE_PHONE,TYPE_PRIORITY_PHONE,TYPE_SYSTEM_ALERT,TYPE_SYSTEM_ERROR,TYPE_SYSTEM_OVERLAY,TYPE_APPLICATION_OVERLAY,TYPE_TOAST

這裏面只有TYPE_TOAST不需要 "SYSTEM_ALERT_WINDOW"權限,但是在不同的版本會有不同的限制。

  • 用戶已經授予了 "SYSTEM_ALERT_WINDOW"權限
  1. 系統版本 >= O
    這種情況下上面的所以類型按理都是可以使用的,但是TYPE_TOAST在targetSdkVersion >= 26時,是不能直接添加window的,而且在sdk 26後,google推薦使用TYPE_APPLICATION_OVERLAY,所以在有權限和系統版本在O或者以上時,可以用TYPE_APPLICATION_OVERLAY。
  2. 系統版本 < O
    這個時候就不能用TYPE_APPLICATION_OVERLAY,那麼其實我們可以用TYPE_SYSTEM_ALERT。

\color{blue}{綜上:}有權限的情況下,其實還是比較好處理的,這樣就不會出現用TYPE_TOAST時出現的異常

  • 用戶沒有授予了 "SYSTEM_ALERT_WINDOW"權限
    這種情況下,沒辦法了,就不能用上面的系統類型了,那我們可以用TYPE_TOAST,這個類型是不需要特殊權限的,使用TYPE_TOAST有兩種方法,直接有系統的Toast類,二是還是像上面那樣,只是類型指定爲TYPE_TOAST。
  1. 問題:
    使用TYPE_TOAST,在android8.0及以上,是不能直接添加window,在Android8.0以下的某些版本,在上一個沒有移除前,是不能繼續添加下一個的,當然可以模仿toast的方式,但是,這個需要hook系統的方法,所以可能存在兼容性問題,使用Toast,但是Toast顯示時間有限制,而且默認是不接收觸摸事件的,當然可以通過反射去修改。

代碼找個時間寫,分析完,感覺大腦已經缺氧了,這裏說一樣思路:
\color{blue}{思路:}

  • 有SYSTEM_ALERT_WINDOW權限,請看上面。
  • 沒有SYSTEM_ALERT_WINDOW權限。
    1. 如果需求比較簡單,其實可以使用系統的Toast
    2. android O以下,可以使用TYPE_TOAST,但是要保證上一個window移除後才能添加下一個。
    3. 通過反射修改Toast屬性,比如顯示時長,接收觸摸事件,這樣有沒有權限或者不同的版本,都是ok的,當然得注意一下Android9.0。
    4. 模仿Toast,自己創建token對象,hook系統類(比如WindowManagerService,LocalServices等系統類,獲取WindowManagerService對象),加入到對應的數組裏面
    5. 在addview處加try{}catch{},其實Toast在addview的時候都使用了try{}catch{}的

最後

最近工作一直很忙,抽週末寫文章,分析完已經是晚上,而且感覺大腦已經缺氧,所以代碼我會最近抽時間寫,剛開始寫文章,如果寫得不好,還請多多建議,如果需要,可以博客地址下面留言,如果有人已經寫好,可以互相交流,如有寫得不對的地方,歡迎留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章