微信号:guolin_blog

介绍:Android技术分享平台,在这里不仅可以学到各种Android相关的最新技术,还可以将你自己的技术总结分享给其他人,每周定期更新.

崩溃率排行榜第三,WindowManager$BadTokenException报错分析

2019-01-11 08:02 奔跑的平头哥



今日科技快讯


近日上海徐汇交警部门召集辖区快递、外卖企业负责人进行集中约谈,对前期发现积累的400余起“骑手”交通违法信息进行集中通报。美团、饿了么、必胜客宅急送、韵达、申通等企业区域负责人都到会,各负责人的座位前方有该企业骑手交通违法记录。其中美团、饿了么成“违法大户”。仅2018年12月6日至2019年1月5日,美团违法538起,饿了么违法699起。


作者简介


明天就是周六啦,提前祝大家周末愉快!

不知道大家还记不记得,几个月前我开直播讲Kotlin的时候提到了Android崩溃率排名前几的几个异常,其中第一是NullPointerException,第二是OutOfMemoryError,而第三就是WindowManager$BadTokenException了。

本篇来自 奔跑的平头哥 的投稿,和大家分享了WindowManager$BadTokenException这一错误的具体分析,希望对大家有所帮助!

奔跑的平头哥的博客地址:

https://www.jianshu.com/u/b4d21f49c053


简介


本文主要讲解WindowManager里的addView(View view, ViewGroup.LayoutParams params),removeView(View view),removeViewImmediate(View view)三个方法的实现原理,以及通过分析系统源码,解决我们在平常开发过程中使用WindowManager遇到的各种异常崩溃问题,本文因修改项目中的WindowManager$BadTokenException类型bug而来,所以以此命名。

源码版本

  • sdk:android-28

  • Android系统源码:Android8.0

功能

通过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<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);
    }

关键就在这个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 == nullthrow new IllegalArgumentException("null client");
    if (inputContext == nullthrow 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.");

2. 抛出条件:
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系统开始的,没有去查看。

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


 
郭霖 更多文章 高级Android开发进阶之路,你需要掌握的几个关键技术! 我是如何准备 Android 技术面试的 Java并发之synchronized深度解析 Android插件化初体验 一看就懂,Git的初步学习
猜您喜欢 “湾”道超车:催生粤港澳大湾区教育合作的“化学反应” 为什么2016年整个互联网行业前端工程师还是供不应求? UCloud ·感恩2014·献给正在奋斗中的运维工程师 分享图片 百度产品体验评测(九)| 如何进行用户反馈分析