簡介:
本文主要講解WindowManager里的addView(View view, ViewGroup.LayoutParams params),removeView(View view),removeViewImmediate(View view)三個方法的實現(xiàn)原理乡翅,以及通過分析系統(tǒng)源碼,解決我們在平常開發(fā)過程中使用WindowManager遇到的各種異常崩潰問題,本文因修改項目中的WindowManager$BadTokenException類型bug而來闲勺,所以以此命名。
源碼版本
- sdk:android-28
- Android系統(tǒng)源碼:Android8.0
解決方案請看第二篇文章:
WindowManager$BadTokenException-解決方案
功能:
通過WindowManager實現(xiàn)一個懸浮在屏幕最上方的懸浮View芽狗,提示用戶文件上傳完成把将,以及點擊查看文件內(nèi)容焰枢。
//WindowManager實例獲取:
mContext.getApplicationContext().getSystemService(Context.WINDOW_SERVICE)
//添加view
mWdm.addView(mToastView, mParams);
//移除View
mWdm.removeViewImmediate(mToastView);
場景1:
在使用WindowManager時候徽级,可能會遇到這樣的異常:
View xxxxx@167788 has already been added to the window manager
看到這個異常气破,很多開發(fā)者立馬能夠想到是同一個View,在沒有移除情況下餐抢,又執(zhí)行了addView操作现使,于是,在添加之前判斷該View是否已經(jīng)有父容器了旷痕,有碳锈,者先移除該View:
final ViewParent parent = mToastView.getParent();
if (parent != null) {
mWdm.removeView(mToastView);
}
改好,心想如此簡單欺抗,臉上洋溢著開心的笑容售碳,提交,打包绞呈,熱更贸人,完事,但是沒想到佃声,線上還是會報上面的異常艺智。
于是,翻翻WindowManager里的Api圾亏,看到還有removeViewImmediate(View view)這個方法十拣,通過文檔,了解到這個方法與removeView(View view)不同在于前者是同步召嘶,后者是異步的父晶,于是想到可能是removeView(View view)異步導(dǎo)致,執(zhí)行添加之前View還沒有來得及移除弄跌。
- removeView實現(xiàn)異步的方式是什么甲喝?
- 同步和異步表現(xiàn)的差異在哪里呢?
- 為什么在同步和異步移除的情況铛只,得到View的父容器都是null埠胖,為什么用removeView會報異常呢?
帶著疑問繼續(xù)向向看吧淳玩!
于是采用同步移除的方式直撤,測試幾波,發(fā)現(xiàn)ok了蜕着,這下應(yīng)該沒事了吧谋竖,打包红柱,熱更,吃飯蓖乘,哈哈哈哈哈锤悄!
場景2:
突然第二天發(fā)現(xiàn)這個功能偶爾還是會出現(xiàn)崩潰,不同的是異常信息:
Unable to add window -- window android.view.ViewRootImpl$W@c84a531 has already been added
眉頭緊鎖嘉抒,著了零聚,還是報什么View已經(jīng)添加了,難道同步移除也不行些侍,但是報的是ViewRootImpl已經(jīng)添加了啊隶症,這下完了,到底完沒完,繼續(xù)下看岗宣。
補充:
也許發(fā)現(xiàn)蚂会,在不同的Android版本,手機(jī)狈定,targetSdkVersion值颂龙,也會有不一樣的現(xiàn)象,比如在targetSdkVersion指定在Android O及以上纽什,當(dāng)WindowManager.LayoutParams指定的類型為WindowManager.LayoutParams.TYPE_TOAST(官方已建議棄用),會直接崩潰躲叼,報以下錯誤:
Unable to add window -- token null is not valid; is your activity running?
而在O以下卻不會呢芦缰,這是為什么呢让蕾?帶著疑問一起向下看吧誉裆!
removeView,removeViewImmediate異同
- WindowManager.java實現(xiàn)類WindowManagerImpl.java
1.分解操作:
mContext.getApplicationContext().getSystemService(Context.WINDOW_SERVICE)
//android.app.ContextImpl
@Override
public String getSystemServiceName(Class<?> serviceClass) {
return SystemServiceRegistry.getSystemServiceName(serviceClass);
}
//android.app.SystemServiceRegistry
static{
...
registerService(Context.WINDOW_SERVICE, WindowManager.class, new
CachedServiceFetcher<WindowManager>() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx);
}});
}
private static <T> void registerService(String serviceName, Class<T> serviceClass,ServiceFetcher<T> serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
- removeView耀鸦,removeViewImmediate
//android.view.WindowManagerImpl
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
@Override
public void removeViewImmediate(View view) {
mGlobal.removeView(view, true);
}
關(guān)鍵就在這個boolean變量了(...省略的都是相同或者不重要的代碼)
//android.view.WindowManagerGlobal
private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
...
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}
關(guān)鍵點:
- view.assignParent(null);
這個方法就是將View的父容器置null,通過該View的getParent()可以獲取該父容
器的對象。 - boolean deferred = root.die(immediate);
//android.view.ViewRootImpl
boolean die(boolean immediate) {
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
...
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
如果immediate為true立即調(diào)用doDie()奄容,否則通過handler發(fā)送一個MSG_DIE消息,然后立即返回,這時候View并沒有完成真正的刪除操作,原來同步和異步是這么實現(xiàn)的
void doDie() {
checkThread();
if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
if (mAdded) {
dispatchDetachedFromWindow();
}
...
}
mAdded = false;
}
WindowManagerGlobal.getInstance().doRemoveView(this);
}
dispatchDetachedFromWindow:
垃圾回收相關(guān)操作迫横,比如清除數(shù)據(jù)和消息,移除回調(diào)呛讲。
通過Session的remove()刪除Window衡瓶,這同樣是一個IPC過程关面,最終會調(diào)用WindowManagerService的removeWindow()。
調(diào)用View的dispatchDetachedFromWindow(),進(jìn)而調(diào)用onDetachedFromWindow(),onDetachedFromWindowInternal()佃迄。
onDetachedFromWindow()對于大家來說一定不陌生普碎,我們可以在這個方法內(nèi)部做一些資源回收工作套啤,比如終止動畫、停止線程等随常。
最后再調(diào)用WindowManagerGlobal的doRemoveView()方法刷新數(shù)據(jù),包括mRoots萄涯、mParams绪氛、mViews和mDyingViews,將當(dāng)前Window所關(guān)聯(lián)的對象從集合中刪除涝影。
- 重要函數(shù):dispatchDetachedFromWindow
void dispatchDetachedFromWindow() {
...
mView.assignParent(null);
mView = null;
mAttachInfo.mRootView = null;
...
try {
mWindowSession.remove(mWindow);
} catch (RemoteException e) {
}
...
}
- 移除window - mWindowSession.remove(mWindow);
mWindowSession.remove(mWindow);
--------------
@Override
public void remove(IWindow window) {
mService.removeWindow(this, window);
}
//android.view.WindowManagerGlobal
public static IWindowSession getWindowSession() {
...
InputMethodManager imm = InputMethodManager.getInstance();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(...);
return sWindowSession;
}
}
- windowManager
IWindowManager windowManager = getWindowManagerService();
--------------
public static IWindowManager getWindowManagerService() {
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));
...
}
return sWindowManagerService;
}
}
- ServiceManager.getService("window"))
Android系統(tǒng)在啟動時枣察,會在SystemServer里面注冊很多系統(tǒng)需要的服務(wù),像AMS燃逻,PMS序目,WMS等
路徑:framewor/base/setvices/java/com.android.server/SystemServer
private void startOtherServices() {
WindowManagerService wm = null;
...
wm = WindowManagerService.main(context, inputManager,mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL, !mFirstBoot, mOnlyCore, new PhoneWindowManager());
ServiceManager.addService(Context.WINDOW_SERVICE, wm);
...
}
- WindowManagerService.openSession
@Override
public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client,
IInputContext inputContext) {
if (client == null) throw new IllegalArgumentException("null client");
if (inputContext == null) throw new IllegalArgumentException("null inputContext");
Session session = new Session(this, callback, client, inputContext);
return session;
}
- 關(guān)鍵代碼:WindowManagerGlobal.getInstance().doRemoveView(this);
- mViews 存儲的是所有Window對應(yīng)的View
- mRoots 存儲的是所有Window對應(yīng)的ViewRootImpl
- mParams 存儲的是所有Window對應(yīng)的布局參數(shù)
- mDyingViews 待刪除的View列表
void doRemoveView(ViewRootImpl root) {
synchronized (mLock) {
final int index = mRoots.indexOf(root);
//如果找到對應(yīng)view的索引
if (index >= 0) {
mRoots.remove(index);
mParams.remove(index);
final View view = mViews.remove(index);
mDyingViews.remove(view);
}
}
if (ThreadedRenderer.sTrimForeground && ThreadedRenderer.isAvailable()) {
doTrimForeground();
}
}
addView(...)過程分析
- 第一個異常:
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
- 拋出條件:
if (index >= 0 && ! mDyingViews.contains(view));
//android.view.WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
...
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
...
//創(chuàng)建 new ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
- 關(guān)鍵函數(shù):ViewRootImpl - setView
//android.view.ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
...
mAttachInfo.mRootView = view;
...
mAdded = true;
try {
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
} catch (RemoteException e) {
...
} finally {
...
}
...
// 一大波異常來襲
if (res < WindowManagerGlobal.ADD_OKAY) {
...
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- another window of type "
+ mWindowAttributes.type + " already exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified display can not be found");
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified window type "
+ mWindowAttributes.type + " is not valid");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
...
//設(shè)置view的父容器
view.assignParent(this);
...
}
}
}
- 關(guān)鍵代碼:
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
//framewor/base/setvices/java/com.android.server/wm/Session
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
//framewor/base/setvices/java/com.android.server/wm/WindowManagerService
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
int[] appOp = new int[1];
// 權(quán)限檢測伯襟,有興趣的可以自己點進(jìn)去看看
int res = mPolicy.checkAddPermission(attrs, appOp);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
...
synchronized(mWindowMap) {
/** final WindowState win = new WindowState(this, session, client, token, parentWindow,
mWindowMap.put(client.asBinder(), win);
win = mWindow = new W(this);
public ViewRootImpl(Context context, Display display) {
...
mWindow = new W(this);
...
}
*/
appOp[0], seq, attrs, viewVisibility, session.mUid,
session.mCanAddInternalSystemWindow);
//異常1
if (mWindowMap.containsKey(client.asBinder())) {
Slog.w(TAG_WM, "Window " + client + " is already added");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
/**
window類型type:
type表示W(wǎng)indow的類型猿涨,Window有三種類型,分別是應(yīng)用Window姆怪,子
Window和系統(tǒng)Window叛赚。
應(yīng)用類Window對應(yīng)著一個Activity澡绩。子Window不能單獨存在,它需要附屬在特定的父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)限。
*/
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
parentWindow = windowForClientLocked(null, attrs.token, false);
if (parentWindow == null) {
Slog.w(TAG_WM, "Attempted to add window with token that is not a window: "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
//子Window
if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
&& parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
Slog.w(TAG_WM, "Attempted to add window with token that is a sub-window: "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
}
if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
Slog.w(TAG_WM, "Attempted to add private presentation window to a non-private display. Aborting.");
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
}
...
if (token == null) {
//應(yīng)用Window
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (rootType == TYPE_WALLPAPER) {
Slog.w(TAG_WM, "Attempted to add wallpaper window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
....
if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
parentWindow)) {
Slog.w(TAG_WM, "Attempted to add a toast window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
...
if (type == TYPE_TOAST) {
if (!getDefaultDisplayContentLocked().canAddToastWindowForUid(callingUid)) {
Slog.w(TAG_WM, "Adding more than one toast window for UID at a time.");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
...
}
// From now on, no exceptions or errors allowed!
res = WindowManagerGlobal.ADD_OKAY;
if (mCurrentFocus == null) {
mWinAddedSinceNullFocus.add(win);
}
...
win.attach();
mWindowMap.put(client.asBinder(), win);
...
boolean imMayMove = true;
win.mToken.addWindow(win);
...
return res;
}
- 異常分析:
if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
parentWindow)) {
Slog.w(TAG_WM, "Attempted to add a toast window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
從注釋// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.可以看出盛杰,當(dāng)targetSdkVersion大于N MR1挽荡,type == TYPE_TOAST 情況下是會直接崩潰的,報以下異常:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
- 異常分析:
if (type == TYPE_TOAST) {
if (!getDefaultDisplayContentLocked().canAddToastWindowForUid(callingUid)) {
Slog.w(TAG_WM, "Adding more than one toast window for UID at a time.");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
...
}
如果mParams的類型是TYPE_TOAST即供,對于同一個uid定拟,在同一個時刻只能添加一個toast類型的Window,如果在上一個Window還沒有移除時逗嫡,又去添加新Window的操作青自,直接崩潰報以下異常:
Unable to add window -- window android.view.ViewRootImpl$W@c84a531 has already been added
但是不同Android系統(tǒng)版本铁追,表現(xiàn)不一樣仑荐,在我的Android5.1上測試,是可以同時添加多個Window,通過查看5.1系統(tǒng)源碼腺办,發(fā)現(xiàn)在5.1上沒有上面的判斷邏輯抹锄,具體從那個Android系統(tǒng)開始的逆瑞,沒有去查看。
里面還有很多異常伙单,大家可以自己去查看获高。如果寫得有不對的地方,歡迎留言吻育。