WindowManager$BadTokenException(WindowManager源码分析)

简介:

本文主要讲解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还没有来得及移除。

  1. removeView实现异步的方式是什么?
  2. 同步和异步表现的差异在哪里呢?
  3. 为什么在同步和异步移除的情况,得到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);
            }
        }
    }

关键点:

  1. view.assignParent(null);
    这个方法就是将View的父容器置null,通过该View的getParent()可以获取该父容
    器的对象。
  2. 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:

  1. 垃圾回收相关操作,比如清除数据和消息,移除回调。

  2. 通过Session的remove()删除Window,这同样是一个IPC过程,最终会调用WindowManagerService的removeWindow()。

  3. 调用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);
  1. mViews 存储的是所有Window对应的View
  2. mRoots 存储的是所有Window对应的ViewRootImpl
  3. mParams 存储的是所有Window对应的布局参数
  4. 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(...)过程分析

  1. 第一个异常:
throw new IllegalStateException("View " + view
         + " has already been added to the window manager.");
  1. 抛出条件:
    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系统开始的,没有去查看。

里面还有很多异常,大家可以自己去查看。如果写得有不对的地方,欢迎留言。

你可能感兴趣的:(WindowManager$BadTokenException(WindowManager源码分析))