美文网首页Android基础知识Android面试程序编码
为什么Dialog不能用Application的Context

为什么Dialog不能用Application的Context

作者: goeasyway | 来源:发表于2016-07-08 21:33 被阅读10351次

    有网友在我的一篇和Context相关的面试题文章提到这个问题。我觉得一两句话,不好说清楚(我需要一些图表),所以用这篇文章来回答一下。

    先试一下用Application的上下文来创建Dialog,在调用它的show方法时程序会Crash,LogCat的异常信息如下:

    Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
                               at android.view.ViewRootImpl.setView(ViewRootImpl.java:685)
                               at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
                               at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
                               at android.app.Dialog.show(Dialog.java:316)
    

    从字面上也很容易理解“BadTokenException: Unable to add window -- token null is not for an application”,发生一个BadTokenException的异常,不能添加Window。

    在解释这个问题前,有必要先理清一些概念:

    Window: 定义窗口样式和行为的抽象基类,用于作为顶层的view加到WindowManager中,其实现类是PhoneWindow。
    每个Window都需要指定一个Type(应用窗口、子窗口、系统窗口)。Activity对应的窗口是应用窗口;PopupWindow,ContextMenu,OptionMenu是常用的子窗口;像Toast和系统警告提示框(如ANR)就是系窗口,还有很多应用的悬浮框也属于系统窗口类型。

    WindowManager:用来在应用与window之间的管理接口,管理窗口顺序,消息等。

    WindowManagerService:简称Wms,WindowManagerService管理窗口的创建、更新和删除,显示顺序等,是WindowManager这个管理接品的真正的实现类。它运行在System_server进程,作为服务端,客户端(应用程序)通过IPC调用和它进行交互。

    Token:这里提到的Token主是指窗口令牌(Window Token),是一种特殊的Binder令牌,Wms用它唯一标识系统中的一个窗口。

    下图显示了Activity的Window和Wms的关系:

    Activity有一个PhoneWindow,当我们调用setContentView时,其实最终结果是把我们的DecorView作为子View添加到PhoneWindow的DecorView中。而最终这个DecorView,过WindowMnagerImpl的addView方法添加到WMS中去的,由WMS负责管理和绘制(真正的绘制在SurfaceFlinger服务中)。

    Dialog的窗口属于什么类型

    跟Activity对应的窗口一样,Dialog有一个PhoneWindow的实例。Dialog 的类型是TYPE_APPLICATION,属于应用窗口类型。

    可以从Dialog的创建代码得到确认:

        Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
            // 忽略一些代码
            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    
            final Window w = new PhoneWindow(mContext);
            mWindow = w;
            w.setCallback(this);
            w.setOnWindowDismissedCallback(this);
            w.setWindowManager(mWindowManager, null, null);
            w.setGravity(Gravity.CENTER);
    
            mListenersHandler = new ListenersHandler(this);
        }
    

    注意w.setWindowManager(mWindowManager, null, null)这句,把appToken设置为null。这也是Dialog和Activity窗口的一个区别,Activity会将这个appToken设置为ActivityThread传过来的token。�

     public void setWindowManager(WindowManager wm, IBinder appToken, String appName)
    

    然后在Dialog的show方法中:

        public void show() {
            // 忽略一些代码
            mDecor = mWindow.getDecorView();
    
            WindowManager.LayoutParams l = mWindow.getAttributes();
            if ((l.softInputMode
                    & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
                WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
                nl.copyFrom(l);
                nl.softInputMode |=
                        WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
                l = nl;
            }
    
            try {
                mWindowManager.addView(mDecor, l);
                mShowing = true;
        
                sendShowMessage();
            } finally {
            }
        }
    

    mWindow是PhoneWindow类型,mWindow.getAttributes()默认获取到的Type为TYPE_APPLICATION。

    Dialog最终也是通过系统的WindowManager把自己的Window添加到WMS上。在addView前,Dialog的token是null(上面提到过的w.setWindowManager第二参数为空)。

    Dialog初化始时是通过Context.getSystemServer 来获取 WindowManager,而如果用Application或者Service的Context去获取这个WindowManager服务的话,会得到一个WindowManagerImpl的实例,这个实例里token也是空的。之后在Dialog的show方法中将Dialog的View(PhoneWindow.getDecorView())添加到WindowManager时会给token设置默认值还是null。

    如果这个Context是Activity,则直接返回Activity的mWindowManager,这个mWindowManager在Activity的attach方法被创建,Token指向此Activity的Token,mParentWindow为Activity的Window本身。如下的代码Activity重写了getSystemService这个方法:

        @Override
        public Object getSystemService(@ServiceName @NonNull String name) {
            if (getBaseContext() == null) {
                throw new IllegalStateException(
                        "System services not available to Activities before onCreate()");
            }
    
            if (WINDOW_SERVICE.equals(name)) {
                return mWindowManager;
            } else if (SEARCH_SERVICE.equals(name)) {
                ensureSearchManager();
                return mSearchManager;
            }
            return super.getSystemService(name);
        }
    

    系统对TYPE_APPLICATION类型的窗口,要求必需是Activity的Token,不是的话系统会抛出BadTokenException异常。Dialog 是应用窗口类型,Token必须是Activity的Token。

    问题的答案

    那为什么一定要是Activity的Token呢?我想使用Token应该是为了安全问题,通过Token来验证WindowManager服务请求方是否是合法的。如果我们可以使用Application的Context,或者说Token可以不是Activity的Token,那么用户可能已经跳转到别的应用的Activity界面了,但我们却可以在别人的界面上弹出我们的Dialog,想想就觉得很危险。

    如你跳到了微信界面了,这时在后台的某个应用里调用Dialog的show,那么微信的界面上会显示一个Dialog,这个Dialog可能会让用户输入密码什么的,而用户完全无法区分是不是微信弹出的。

    Even 原创
    简书链接:http://www.jianshu.com/users/f9fbc7a39b36/latest_articles
    转载请注明出处。
    对Android面试有兴趣的可以关注我的微信公众号:Android面试启示录

    Android面试启示录

    相关文章

      网友评论

      • TangKe:Token其实是用来告知WM需要把View添加到哪个Window上的。比如Toast,他就是通过系统的一个服务调用,让系统服务返回一个Token回来,然后拿到该Token,指定到WindowManager.LayoutParams.token里面,然后调用WM.addView显示出来,所以Toast任何Context都能显示,而且能脱离当前应用
      • 启航_2020_11_03:受教了,解决了自己之前的困惑:业务场景是在BroadcastReceiver中onReveive()方法里实现一个弹窗,默认入参的上下文是全局的,改成对应的Activity即可
      • snowIceg:感谢
      • AWeiLoveAndroid:真正的大佬:+1: 学习了
      • wervy:今天刚遇到这个问题,大佬,受教了
      • trycatchx:使用application 的context,dialog 有一种情况还是属于应用的!!跳转其他应用,dialog 还是会消失!!那就是使用theme 是dialog 的activity.
      • 68768b474bfc:文中说:token,Wms用它唯一标识系统中的一个窗口。

        但是除了activity以外的window都没有token,这些window如何标识
      • 22ee03b8dc8f:我感觉最后拿个解释不太靠谱

        system window是一样可以达到你说的跨应用伪装效果,谷歌一样没有禁止

        没理由sub window就只能在当前应用内打开是为了安全问题
      • 流穿枫:看了三遍才理清。 :joy:
      • lishiwei:以前从代码层面知道为什么。。今天算是从设计层面知道为什么了。。讲的太棒了。
      • why_92:666666
      • ab752740d9e1:有理有据,有代码有真相
      • ITIan:搜索这个问题,发现了这里,还不错,关注一下
      • HaiChecker:博主的每篇文章都让我提升了很多
      • 9afdce5ed1ae:博主每一篇文章对我都有提升
      • 会疼的小石头:写的好棒好棒
      • 挂云帆love:我听说是可以在service里弹出对话框的,例如当手机电量不足时,系统会弹出来一个对话框,此时的对话框就不是依赖于activity,楼主知道这是怎么做的吗?
        Alex_Cin:@WebView 不依赖于activity的对话框,可能不是dialog,可以用activity实现,也可以用windowmanage实现(需要权限)
        goeasyway:@wait_callback 嗯,和TOAST一样的。有些系统窗口还需要在Androidmanifest.xml声明权限
        2e1e210cfb6e:@WebView 这个应该是系统窗口
      • 捡淑:搜得斯奈 斯国一
      • one_cup:原来是这样,之前在学的时候以为是因为Application没有theme主题所以不能使用,肤浅了!
      • Chauncey_Chen:在项目中确实遇到问题。学习了,谢谢。。
      • loftcat:👍赞!
      • JokAr_::+1:
        JokAr_:@Freenovo :joy::joy::joy:,野生的
        e69fa67187f5:@_JokAr 发现一枚JokAr

      本文标题:为什么Dialog不能用Application的Context

      本文链接:https://www.haomeiwen.com/subject/djoljttx.html