登录

去注册

登录

注册

去登录

注册

每日一问 BadTokenException 你知道多少?

xiaoyang   2019-10-20   收藏

作为 Android 开发,这个异常一定都在自家崩溃平台上见过,那么

  1. 有哪些场景下会出现这个异常呢?
  2. 分别如何解决?
  3. 有无一些开源方案参考?

知道多少答多少哈,别冷场呀~

18

哪些场景下会出现这个异常?

源码分析:

AS中全局搜索BadTokenException,会在ViewRootImpl的setView方法中看到好几个抛出这个异常的代码:

         int res = mWindowSession.addToDisplay();
         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_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);

可以看到它是根据WindowSessionaddToDisplay方法的返回值做判断的,这个方法最终也是调用WMS(WindowManagerService)的addWindow方法。
来看看这个addWindow方法在什么情况下会返回以上7种Flag,首先是ADD_BAD_APP_TOKEN

        WindowToken token = displayContent.getWindowToken(attrs.token);
        if (token == null) {
            ......
            return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
        }
        else if (type == TYPE_TOAST) {
            if (token.windowType != TYPE_TOAST) {
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
        }
        else if (type == TYPE_QS_DIALOG) {
            if (token.windowType != TYPE_QS_DIALOG) {
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
        }
        else if (rootType == TYPE_WALLPAPER) {
            if (token.windowType != TYPE_WALLPAPER) {
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
        }
        else if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
            if (token.windowType != TYPE_ACCESSIBILITY_OVERLAY) {
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
        }

除了token为空会返回ADD_BAD_APP_TOKEN之外,还有一种情况就是当rootType!=windowType的时候也会返回ADD_BAD_APP_TOKEN
这两个type就是调用WindowManageraddView方法时传进去的LayoutParamstype,默认是TYPE_APPLICATION
所以后面那4个else if,也就是检查窗口类型是否匹配了。
那这个token什么时候为空呢?
从上面的代码可以看到,token是通过DisplayContentgetWindowToken方法来获取的(这些WindowToken都保存在一个DisplayContentHashMap<IBinder, WindowToken>实例中):

    WindowToken getWindowToken(IBinder binder) {
        return mTokenMap.get(binder);
    }

它是直接调用Mapget方法,如果返回null的话,很可能是被移除了,搜一下"mTokenMap.remove",会看到在removeWindowToken方法有调用:

    WindowToken removeWindowToken(IBinder binder) {
        final WindowToken token = mTokenMap.remove(binder);
        if (token != null) {
            token.setExiting();
        }
        return token;
    }

removeWindowToken方法何时被调用呢?
顺藤摸瓜:

DisplayContent.removeAppToken() ->
AppWindowContainerController.removeContainer() ->
ActivityStack.removeWindowContainer() ->
ActivityStack.removeActivityFromHistoryLocked() ->
ActivityStack.activityDestroyedLocked() ->
ActivityManagerService.activityDestroyed() ->
ActivityThread.handleDestroyActivity()

emmm,现在就很清晰了:
当Activity被Destroy的时候就会调用到removeWindowToken方法,把对应的WindowToken移除,
调用remove方法所传进去的key(IBinder),其实就是ActivityThread调用Activity的attach方法时传进来的,
也就是Activity.mToken了,也是这个Activity所对应的PhoneWindow里面的mAppToken
这个Activity所对应的WindowManager,它里面也会持有Activity的PhoneWindow,
当调用WindowManager的addView方法时,还会把PhoneWindow的mAppToken赋值给WindowManager.LayoutParams.token

那么看回上面的那句代码:

// attrs就是调用WindowManager的`addView`方法传进去的LayoutParams
WindowToken token = displayContent.getWindowToken(attrs.token);

哈哈,知道为什么会是null了吧,此时的attrs.token就是Activity的mToken,当这个Activity被Destroy之后,再通过它的WindowManager添加View,最终就会抛出BadTokenException,这也就对应了开头对这个异常的描述:" is your activity running?"。

感觉话有点多了,下面长话短说...


第二个,ADD_BAD_SUBWINDOW_TOKEN:
返回这个Flag有两种情况:

  1. token所对应的Window为空;
  2. 这个Window本身就是子窗口(添加Window时的Type值在FIRST_SUB_WINDOW与LAST_SUB_WINDOW之间);


第三个,ADD_NOT_APP_TOKEN:
Window类型为TYPE_APPLICATION(调用WindowManager.addView方法时不指定类型,默认就是这个),但是通过DisplayContent的getWindowToken方法获取到的实例,不是AppWindowToken的实例(AppWindowToken是WindowToken的子类,只有是AppWindowToken才能添加View)。
WindowToken何时为AppWindowToken?
AppWindowToken只有在AppWindowContainerController初始化,而AppWindowContainerController是在ActivityRecord创建时初始化,看到这里基本就可以确定:如果Window类型为TYPE_APPLICATION,则必须添加在Activity上。


第四个,ADD_APP_EXITING:
当AppWindowToken的removed被标记为true时就会返回这个Flag。
何时被标记为true?
细心的同学会发现,在前面贴出来的removeWindowToken(Activity被Destroy时调用)方法里会调用token的setExiting方法,这个removed就是在这时候被标记的,调用顺序为:

WindowToken.setExiting() ->
WindowToken.removeImmediately() ->
AppWindowToken.onRemovedFromDisplay()


第五个,ADD_DUPLICATE_ADD:
看名字就知道是重复添加的意思了。
除了同一个View不能添加两次,和不能同时添加两个Type为TYPE_APPLICATION_STARTING的View之外,还有一个就是,Toast窗口不能同时存在两个!


第六个,ADD_MULTIPLE_SINGLETON:
SDK28的源码里,WMS的addWindow方法不会返回这个。


第七个,ADD_PERMISSION_DENIED:
很明显就是没权限。
当Type指定为TYPE_PRIVATE_PRESENTATION时(私有),但是所在的显示器却不是私有的,就会返回这个Flag。


终于完了。。。这是我最长的一篇回答了。


来总结一下抛出BadTokenException的原因以及解决方案:

原因一共有7个:

  1. Activity已经Destroy了,还往里面添加View;

  2. 添加了2个Type为SUB_WINDOW的View;

  3. 使用非Activity的Context添加了Type为TYPE_APPLICATION的View;

  4. Activity在Destroy时还往里面添加View;

  5. 同一个View添加了2次,或者,添加了2个Type为TYPE_APPLICATION_STARTING的View;

  6. ADD_MULTIPLE_SINGLETON?不会发生这个的;

  7. 把Type为TYPE_PRIVATE_PRESENTATION的View,添加在非私有显示器上;

解决方法,就是避免以上7种做法就行了。

回复
陈小缘 : @鸿洋 

哈哈,没事啦,鸿神辛苦了~

2019-11-04 回复
鸿洋 : @陈小缘 

仔细看了下,二次回复做的比较轻量,用了太多标签,确实会乱~~ 暂时修复不了啦。

2019-11-02 回复
陈小缘 : @陈小缘 

补充:一些常见的场景就是:

1. 列表在“加载更多”的时候,退出了该Activity,从后台拉取数据成功时,Activity已经被Destroy了。(对应Flag: ADD_BAD  ...查看更多

1. 列表在“加载更多”的时候,退出了该Activity,从后台拉取数据成功时,Activity已经被Destroy了。(对应Flag: ADD_BAD_APP_TOKEN)


2. 在Service中使用WindowManager添加悬浮窗 或者,弹出对话框时,使用的是默认的TYPE_APPLICATION类型,即没有指定View类型为SYSTEM_WINDOW。(对应Flag: ADD_NOT_APP_TOKEN)


3. 像@cscxzxzc 同学所说的那样,在Activity中showDialog,使用了Application的Context。(对应Flag: ADD_NOT_APP_TOKEN)


4. 7.1.1版本,在调用Toast的show方法之后,主线程卡住的时间 > 该Toast的显示时长。(对应Flag: ADD_BAD_APP_TOKEN)

2019-11-01 回复
nanchen2251 : @陈小缘 

讲的比较到位。

2019-10-17 回复
3

来暖个场,见过在设置Dialog的时候遇到过,我记得挺清楚的(我靠希望wan android以后能定时保持一下输入的信息,刚刚放了个链接跳过去回来就没了)
1.下面这个demo中就报了这个错误

btn.setOnClickListener(new View.OnClickListener(){
        @Override
        public void onClick(View v) {
                new AlertDialog.Builder(getApplicationContext())
                                .setTitle("BadTokenDialog???")
                                .setMessage("No, I am OK !")
                                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {

                                        }
                                }).create()
                                .show();
                }
});

//Logcat
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
        at android.view.ViewRootImpl.setView(ViewRootImpl.java:543)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:259)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
        at android.app.Dialog.show(Dialog.java:286)

2.将AlertDialog.Builder(getApplicationContext())改成AlertDialog.Builder(MainActivity.this)就好了。
我上官方文档搜了下BadTokenException

发现它是个运行时异常,那解决方法就是捕捉异常并处理,或者修改代码来避免异常。
它介绍说是尝试添加的View的LayoutParams.token是无效的,我看了下这个LayoutParams.token

它只告诉说每一个Window对象都具有一个Token标识,一般系统会帮我们填写的。

我就准备去看一看这个token是怎么来的,

// /frameworks/base/core/java/android/view/WindowManager.java

2170        /**
2171         * Identifier for this window.  This will usually be filled in for
2172         * you.
2173         */
2174        public IBinder token = null;


2634        public LayoutParams(Parcel in) {
2635            ........
2653            ........
2654            token = in.readStrongBinder();
                }

在看一下 /frameworks/base/core/java/android/os/HwParcel.java

/**
468     * Reads a strong binder value from the parcel.
469     * @return binder object read from parcel or null if no binder can be read
470     * @throws IllegalArgumentException if the parcel has no more data
471     */
472    public native final IHwBinder readStrongBinder();

发现它是c++底层实现的返回Token

后来我查到一篇罗神写的博客,看一下大致了解了一点点,分享出来后我再去认真看一遍Android应用程序窗口(Activity)与WindowManagerService服务的连接过程分析

我这个Dialog的问题呢,我想的应该是Token越级了,一个Activity的Token,应该是不能使用Applicarion的Token的,否者是否可以实现在其他的APP中弹出一个Dialog了。

3.因为还只是个学生,开源的框架用得不多,这个就不知道了,上面的也不知道回答得对不对,不对请大佬们帮忙指正一下。

回复
cscxzxzc : @cscxzxzc 

呀!第二遍忘记上传截图了,对于大家说的Toast,我原来好像见过一篇文章就说是有个开源的方案替换Toast的。现在忘了

2019-10-16 回复
1

这个异常在需要操作Window(比如addView),但是这个Window是无效的时,就会报出来
比较常见的场景是:
1.显示Dialog或者PopupWindow,但对应的Activity已经销毁时,解决方法就是显示之前判断一下,如果已被销毁就不要显示
2.还是显示Dialog或者PopupWindow,但传入的Context为Application或Service等没有Window的context,且没有设置type为允许无window显示的
3.Android 7.1上面因为Toast引发的BadTokenException,解决方案就是修改对应的mTN,在handler中catch该异常
4.之前无聊看见的,一个比较少见的问题,在启动一个android:noHistory设置为true的activity时,如果在UI还没显示出来的时候(准确说是 ActivityThread.handleResumeActivity 之前)有地方耗时较长卡着了,这时候用户按了home键触发stop,则执行到 ActivityThread.handleResumeActivity 时,会报出这个错,原因是noHistory设置为true的activity触发stop时,会被AMS顺带着destory,client端处理超时会引发该activity被AMS强制销毁;而这个销毁的过程会把对应的token也一起销毁,当执行到handleResumeActivity时,显示ui却找不到token,就爆炸了。具体可以看这里

回复
1

这个异常主要发生在Android 7.x。Toast是系统层面的,不依赖前台页面,存在滥用问题。早期4.+时代做悬浮窗的时候就会把Window的类型设置为TYPE_TOAST。随着Google在Android系统版本上的不断迭代优化,终于在Android7.1.1的时候开发出了这个Bug。Android 7.1.1开始为了避免Toast被滥用,Google给Toast加上了token,这个token有个过期时间,应用在主线程处理其他任务而没能及时弹Toast,处理完任务回来弹Toast的时候,如果token过期了,WindowManager调用addView的时候就会抛出BadTokenException。解决方案可以参考Android 8.0的做法,Android 8.0在WindowManager调用addView的时候做了try-catch,这是从系统层面去解决这个问题。。。但是7.x系列只能开发者通过版本号判断,通过反射传入Handler,在dispatchMessage的时候去捕获这个异常了。
其实发展到现在,Toast的毛病挺多的。早期我们为了避免Toast一个一个排队的弹,会把Toast声明成单例的,每次只要setText然后show就行了(Weex里提供给H5的封装也是这种做法。。。然后在Oppo 9.0的机子上就出现了Toast弹过一次之后就弹不出了),但是这种做法在Android 9.0又会出弹过一个Toast之后Toast弹不出的问题...因为Android 9.0做了优化,你就算每次都调用Toast.makeText().show()也只会显示一个Toast,不会有排队弹的效果。最坑的还是关闭通知权限后弹不出Toast(小米系统做了优化,关闭通知权限也还能弹,要看到这个问题要用非小米系统的Android机)

回复
1

这个在api25的android手机可能会出现,toast要show但是token过期的问题。

回复
0

token失效会导致吗

回复
0

dialog 会出现。

回复
willwaywang6 : @willwaywang6 

Toast也会出现,使用这个开源方案解决:https://github.com/PureWriter/ToastCompat

2019-10-16 回复
0

不知道。挽尊

回复

删除留言

确认删除留言,会导致相关评论丢失?

取消 确定