簡介
上一篇分析了WindowManager$BadTokenException發(fā)生的原因,帶大家一起通過分析WindowManager源碼搪桂,更加深入的了解了WindowManager添加window的過程透敌,以及在使用WindowManager添加自己的window或者View的時候,怎么去避免發(fā)生異常踢械,接下來酗电,繼續(xù)深入分析WindowManager源碼,帶大家一起尋找裸燎,解決平時使用WindowManager出現(xiàn)的各種異常的辦法顾瞻。
本文章講解,建立在上一篇文章的基礎(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
- 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
- 參數(shù)變化:
(1) attrsmWindowAttributes.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)
參數(shù)
(1)session: mWindowSession
(2)client:mWindow
(3)attrs:mWindowAttributes包含了傳入時WindowManager.LayoutParams參數(shù)的所有屬性-
權(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_TYPEif (!((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; ```
-
類型檢測
(1)callingUid,type
現(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) mWindowMapfinal 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 -- window mWindow has already been addedif (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類型
-
檢測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ù)往下看阅爽,我會在最后講解,如果只是想知道解決辦法荐开,可以直接拉到最后查看付翁。
為什么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)限
- 系統(tǒng)版本 >= O
這種情況下上面的所以類型按理都是可以使用的剂府,但是TYPE_TOAST在targetSdkVersion >= 26時拧揽,是不能直接添加window的,而且在sdk 26后,google推薦使用TYPE_APPLICATION_OVERLAY淤袜,所以在有權(quán)限和系統(tǒng)版本在O或者以上時痒谴,可以用TYPE_APPLICATION_OVERLAY。 - 系統(tǒng)版本 < O
這個時候就不能用TYPE_APPLICATION_OVERLAY铡羡,那么其實(shí)我們可以用TYPE_SYSTEM_ALERT积蔚。
有權(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闰靴。
- 問題:
使用TYPE_TOAST,在android8.0及以上钻注,是不能直接添加window蚂且,在Android8.0以下的某些版本,在上一個沒有移除前幅恋,是不能繼續(xù)添加下一個的杏死,當(dāng)然可以模仿toast的方式,但是捆交,這個需要hook系統(tǒng)的方法淑翼,所以可能存在兼容性問題,使用Toast品追,但是Toast顯示時間有限制玄括,而且默認(rèn)是不接收觸摸事件的,當(dāng)然可以通過反射去修改肉瓦。
分析完遭京,感覺大腦已經(jīng)缺氧了,這里說一解決思路:
- 有SYSTEM_ALERT_WINDOW權(quán)限泞莉,請看上面哪雕。
- 沒有SYSTEM_ALERT_WINDOW權(quán)限。
- 如果需求比較簡單戒财,其實(shí)可以使用系統(tǒng)的Toast
- android O以下热监,可以使用TYPE_TOAST,但是要保證上一個window移除后才能添加下一個饮寞。
- 通過反射修改Toast屬性孝扛,比如顯示時長列吼,接收觸摸事件,這樣有沒有權(quán)限或者不同的版本苦始,都是ok的寞钥,當(dāng)然得注意一下Android9.0。
- 模仿Toast陌选,自己創(chuàng)建token對象理郑,hook系統(tǒng)類(比如WindowManagerService,LocalServices等系統(tǒng)類咨油,獲取WindowManagerService對象)您炉,加入到對應(yīng)的數(shù)組里面
- 在addview處加try{}catch{},其實(shí)Toast在addview的時候都使用了try{}catch{}的