WindowManager$BadTokenException-解決方案
簡介
上一篇分析了WindowManager$BadTokenException發生的原因,帶大家一起通過分析WindowManager原始碼,更加深入的瞭解了WindowManager新增window的過程,以及在使用WindowManager新增自己的window或者View的時候,怎麼去避免發生異常,接下來,繼續深入分析WindowManager原始碼,帶大家一起尋找,解決平時使用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
-
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) 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)session: mWindowSession
(2)client:mWindow
(3)attrs:mWindowAttributes包含了傳入時WindowManager.LayoutParams引數的所有屬性
-
許可權檢測: 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; ```
-
型別檢測
(1)callingUid,type
現在是在系統程序裡面,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);
-
異常:
(1) Unable to add window -- windowmWindow 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型別
-
檢測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系統版本,邏輯是有差異的,總得來說,系統版本越高,控制得越嚴格。具體的解決方案,請繼續往下看,我會在最後講解,如果只是想知道解決辦法,可以直接拉到最後檢視。
為什麼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"許可權
-
系統版本 >= O
這種情況下上面的所以型別按理都是可以使用的,但是TYPE_TOAST在targetSdkVersion >= 26時,是不能直接新增window的,而且在sdk 26後,google推薦使用TYPE_APPLICATION_OVERLAY,所以在有許可權和系統版本在O或者以上時,可以用TYPE_APPLICATION_OVERLAY。 -
系統版本 < O
這個時候就不能用TYPE_APPLICATION_OVERLAY,那麼其實我們可以用TYPE_SYSTEM_ALERT。
有許可權的情況下,其實還是比較好處理的,這樣就不會出現用TYPE_TOAST時出現的異常
-
使用者沒有授予了 "SYSTEM_ALERT_WINDOW"許可權
這種情況下,沒辦法了,就不能用上面的系統型別了,那我們可以用TYPE_TOAST,這個型別是不需要特殊許可權的,使用TYPE_TOAST有兩種方法,直接有系統的Toast類,二是還是像上面那樣,只是型別指定為TYPE_TOAST。
-
問題:
使用TYPE_TOAST,在android8.0及以上,是不能直接新增window,在Android8.0以下的某些版本,在上一個沒有移除前,是不能繼續新增下一個的,當然可以模仿toast的方式,但是,這個需要hook系統的方法,所以可能存在相容性問題,使用Toast,但是Toast顯示時間有限制,而且預設是不接收觸控事件的,當然可以通過反射去修改。
程式碼找個時間寫,分析完,感覺大腦已經缺氧了,這裡說一樣思路:
- 有SYSTEM_ALERT_WINDOW許可權,請看上面。
-
沒有SYSTEM_ALERT_WINDOW許可權。
- 如果需求比較簡單,其實可以使用系統的Toast
- android O以下,可以使用TYPE_TOAST,但是要保證上一個window移除後才能新增下一個。
- 通過反射修改Toast屬性,比如顯示時長,接收觸控事件,這樣有沒有許可權或者不同的版本,都是ok的,當然得注意一下Android9.0。
- 模仿Toast,自己建立token物件,hook系統類(比如WindowManagerService,LocalServices等系統類,獲取WindowManagerService物件),加入到對應的數組裡面
- 在addview處加try{}catch{},其實Toast在addview的時候都使用了try{}catch{}的
最後
最近工作一直很忙,抽週末寫文章,分析完已經是晚上,而且感覺大腦已經缺氧,所以程式碼我會最近抽時間寫,剛開始寫文章,如果寫得不好,還請多多建議,如果需要,可以部落格地址下面留言,如果有人已經寫好,可以互相交流,如有寫得不對的地方,歡迎留言。