简介:
本文主要讲解WindowManager里的addView(View view, ViewGroup.LayoutParams params),removeView(View view),removeViewImmediate(View view)三个方法的实现原理,以及通过分析系统源码,解决我们在平常开发过程中使用WindowManager遇到的各种异常崩溃问题,本文因修改项目中的WindowManager$BadTokenException类型bug而来,所以以此命名。
源码版本
- sdk:android-28
- Android系统源码:Android8.0
解决方案请看第二篇文章:
WindowManager$BadTokenException-解决方案
功能:
通过WindowManager实现一个悬浮在屏幕最上方的悬浮View,提示用户文件上传完成,以及点击查看文件内容。
//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
看到这个异常,很多开发者立马能够想到是同一个View,在没有移除情况下,又执行了addView操作,于是,在添加之前判断该View是否已经有父容器了,有,者先移除该View:
final ViewParent parent = mToastView.getParent();
if (parent != null) {
mWdm.removeView(mToastView);
}
改好,心想如此简单,脸上洋溢着开心的笑容,提交,打包,热更,完事,但是没想到,线上还是会报上面的异常。
于是,翻翻WindowManager里的Api,看到还有removeViewImmediate(View view)这个方法,通过文档,了解到这个方法与removeView(View view)不同在于前者是同步,后者是异步的,于是想到可能是removeView(View view)异步导致,执行添加之前View还没有来得及移除。
- removeView实现异步的方式是什么?
- 同步和异步表现的差异在哪里呢?
- 为什么在同步和异步移除的情况,得到View的父容器都是null,为什么用removeView会报异常呢?
带着疑问继续向向看吧!
于是采用同步移除的方式,测试几波,发现ok了,这下应该没事了吧,打包,热更,吃饭,哈哈哈哈哈!
场景2:
突然第二天发现这个功能偶尔还是会出现崩溃,不同的是异常信息:
Unable to add window -- window android.view.ViewRootImpl$W@c84a531 has already been added
眉头紧锁,着了,还是报什么View已经添加了,难道同步移除也不行,但是报的是ViewRootImpl已经添加了啊,这下完了,到底完没完,继续下看。
补充:
也许发现,在不同的Android版本,手机,targetSdkVersion值,也会有不一样的现象,比如在targetSdkVersion指定在Android O及以上,当WindowManager.LayoutParams指定的类型为WindowManager.LayoutParams.TYPE_TOAST(官方已建议弃用),会直接崩溃,报以下错误:
Unable to add window -- token null is not valid; is your activity running?
而在O以下却不会呢,这是为什么呢?带着疑问一起向下看吧!
removeView,removeViewImmediate异同
- WindowManager.java实现类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() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx);
}});
}
private static void registerService(String serviceName, Class serviceClass,ServiceFetcher 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);
}
关键就在这个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);
}
}
}
关键点:
- 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立即调用doDie(),否则通过handler发送一个MSG_DIE消息,然后立即返回,这时候View并没有完成真正的删除操作,原来同步和异步是这么实现的
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:
垃圾回收相关操作,比如清除数据和消息,移除回调。
通过Session的remove()删除Window,这同样是一个IPC过程,最终会调用WindowManagerService的removeWindow()。
调用View的dispatchDetachedFromWindow(),进而调用onDetachedFromWindow(),onDetachedFromWindowInternal()。
onDetachedFromWindow()对于大家来说一定不陌生,我们可以在这个方法内部做一些资源回收工作,比如终止动画、停止线程等。
最后再调用WindowManagerGlobal的doRemoveView()方法刷新数据,包括mRoots、mParams、mViews和mDyingViews,将当前Window所关联的对象从集合中删除。
- 重要函数: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系统在启动时,会在SystemServer里面注册很多系统需要的服务,像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;
}
- 关键代码:WindowManagerGlobal.getInstance().doRemoveView(this);
- mViews 存储的是所有Window对应的View
- mRoots 存储的是所有Window对应的ViewRootImpl
- mParams 存储的是所有Window对应的布局参数
- mDyingViews 待删除的View列表
void doRemoveView(ViewRootImpl root) {
synchronized (mLock) {
final int index = mRoots.indexOf(root);
//如果找到对应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.
}
...
//创建 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;
}
}
}
- 关键函数: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);
}
...
//设置view的父容器
view.assignParent(this);
...
}
}
}
- 关键代码:
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];
// 权限检测,有兴趣的可以自己点进去看看
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表示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,另外重要的是要记得在清单文件中声明权限。
*/
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) {
//应用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.可以看出,当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系统版本,表现不一样,在我的Android5.1上测试,是可以同时添加多个Window,通过查看5.1系统源码,发现在5.1上没有上面的判断逻辑,具体从那个Android系统开始的,没有去查看。
里面还有很多异常,大家可以自己去查看。如果写得有不对的地方,欢迎留言。