本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
Android进阶之Dialog对应的Context必须是Activity吗?从源码详细分析
奔跑的男人 · 487浏览 · 发布于2021-08-27 +关注

创建Dialog的时候知道在Dialog的构造方法中需要一个上下文环境,而对这个“上下文”没有具体的概念结果导致程序报错,于是发现Dialog需要的上下文环境只能是activity。

前言

创建Dialog的时候知道在Dialog的构造方法中需要一个上下文环境,而对这个“上下文”没有具体的概念结果导致程序报错,

于是发现Dialog需要的上下文环境只能是activity。

所以接下来这篇文章将会从源码的角度来彻底的理顺这个问题;

一、Dialog创建失败

在Dialog的构造方法中传入一个Application的上下文环境。看看程序是否报错:

Dialog dialog = new Dialog(getApplication()); 
     TextView textView = new TextView(this); 
     textView.setText("使用Application创建Dialog"); 
     dialog.setContentView(textView); 
     dialog.show();

    运行程序,程序不出意外的崩溃了,我们来看下报错信息:

    Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application 
        at android.view.ViewRootImpl.setView(ViewRootImpl.java:517) 
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:301) 
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:215) 
        at android.view.WindowManagerImpl$CompatModeWrapper.addView(WindowManagerImpl.java:140)

      这段错误日志,有两点我们需要注意一下

      • 程序报了一个BadTokenException异常;

      • 程序报错是在ViewRootImpl的setView方法中;

      • 我们一定很疑惑BadTokenException到底是个啥,在说明这个之前我们首先需要了解Token,在了解了Token的概念之后,再结合ViewRootImpl的setView方法,就能理解BadTokenException这个到底是什么,怎么产生的;

      二、Token分析

      1、token详解

      Token直译成中文是令牌的意思,android系统中将其作为一种安全机制,其本质是一个Binder对象,在跨进程的通行中充当验证码的作用。比如:在activity的启动过程及界面绘制的过程中会涉及到ActivityManagerService,应用程序,WindowManagerService三个进程间的通信,此时Token在这3个进程中充当一个身份验证的功能,ActivityManagerService与WindowManagerService通过应用程序的activity传过来的Token来分辨到底是控制应用程序的哪个activity。具体来说就是:

      • 在启动Activity的流程当中,首先,ActivityManagerService会创建ActivityRecord由其本身来管理,同时会为这个ActivityRecord创建一个IApplication(本质上就是一个Binder)。

      • ActivityManagerService将这个binder对象传递给WindowManagerService,让WindowManagerService记录下这个Binder。

      • 当ActivityManagerService这边完成数据结构的添加之后,会返回给ActivityThread一个ActivityClientRecord数据结构,中间就包含了Token这个Binder对象。

      • ActivityThread这边拿到这个Token的Binder对象之后,就需要让WindowManagerService去在界面上添加一个对应窗口,在添加窗口传给WindowManagerService的数据中WindowManager.LayoutParams这里面就包含了Token。

      • 最终WindowManagerService在添加窗口的时候,就需要将这个Token的Binder和之前ActivityManagerService保存在里面的Binder做比较,验证通过说明是合法的,否则,就会抛出BadTokenException这个异常。

      • 到这里,我们就知道BadTokenException是怎么回事了,然后接下来分析为什么使用Application上下文会报BadTokenException异常,而Activity上下文则不会

      图片

      2、为什么非要一个Token

      因为在WMS那边需要根据这个Token来确定Window的位置(不是说坐标),如果没有Token的话,就不知道这个窗口应该放到哪个容器上了;

      因为非Activity的Context它的WindowManger没有ParentWindow,导致在WMS那边找不到对应的容器,也就是不知道要把Dialog的Window放置在何处。

      还有一个原因是没有SYSTEM_ALERT_WINDOW权限(当然要加权限啦,DisplayArea.Tokens的子容器,级别比普通应用的Window高,也就是会显示在普通应用Window的前面,如果不加权限控制的话,被滥用还得了)。

      在获得SYSTEM_ALERT_WINDOW权限并将Dialog的Window.type指定为SYSTEM_WINDOW之后能正常显示,是因为WMS会为SYSTEM_WINDOW类型的窗口专门创建一个WindowToken(这下就有容器了),并放置在DisplayArea.Tokens里面(这下知道放在哪里了);

      三、创建dialog流程分析

      1、activity的界面最后是通过ViewRootImpl的setView方法连接WindowManagerService,从而让WindowManagerService将界面绘制到手机屏幕上。而从上面的异常日志中其实也可以看出,Dialog的界面也是通过ViewRootImpl的setView连接WindowManagerService,从而完成界面的绘制的。

      我们首先来看Dialog的构造方法。不管一个参数的构造方法。两个参数的构造方法,最终都会调用到3个参数的构造方法:

      Dialog(@NonNull Context context, @StyleRes int themeResId, boolean  
      createContextThemeWrapper) { 
              ...... 
              //1.创建一个WindowManagerImpl对象 
              mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 
              //2.创建一个PhoneWindow对象 
              final Window w = new PhoneWindow(mContext); 
              mWindow = w; 
              //3.使dialog能够响应用户的事件 
              w.setCallback(this); 
              w.setOnWindowDismissedCallback(this); 
              //4.为window对象设置WindowManager 
              w.setWindowManager(mWindowManager, null, null); 
              w.setGravity(Gravity.CENTER); 
              mListenersHandler = new ListenersHandler(this); 
          }

        这段代码可以看出dialog的创建实质上和activity界面的创建没什么两样,都需要完成一个应用窗口Window的创建,和一个应用窗口视图对象管理者WindowManagerImpl的创建。

        然后Dialog同样有一个setContentView方法:

        public void setContentView(@LayoutRes int layoutResID) { 
                mWindow.setContentView(layoutResID); 
            } 
        依然是调用PhoneWindow的setContentView方法。再接着我们来看下dialog的show方法: 
        public void show() { 
                ...... 
                //1.得到通过setView方法封装好的DecorView  
                mDecor = mWindow.getDecorView(); 
                ...... 
               //2.得到创建PhoneWindow时已经初始化的成员变量WindowManager.LayoutParams 
                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 { 
                    //3.通过WindowManagerImpl添加DecorView到屏幕 
                    mWindowManager.addView(mDecor, l); 
                    mShowing = true; 
                    sendShowMessage(); 
                } finally { 
                } 
            }

          这段代码和activity的makeVisable方法类似,这里也不多说了,注释已经大概的写清楚了。然后调用WindowManagerImpl的addView方法:

          @Override 
              public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { 
                  applyDefaultToken(params); 
                  mGlobal.addView(view, params, mDisplay, mParentWindow); 
              } 
          接着调用了WindowManagerGlobal的addView方法: 
          public void addView(View view, ViewGroup.LayoutParams params, 
                      Display display, Window parentWindow) { 
                  ...... 
                  //1.将传进来的ViewGroup.LayoutParams类型的params转成  
          WindowManager.LayoutParams类型的wparams  
                  final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)  
          params; 
                 //2.如果WindowManagerImpl是在activity的方法中被创建则不为空 
                  if (parentWindow != null) { 
                      parentWindow.adjustLayoutParamsForSubWindow(wparams); 
                  } else { 
                  ...... 
                  } 
                  ViewRootImpl root; 
                  View panelParentView = null; 
                  synchronized (mLock) { 
                  ...... 
                      root = new ViewRootImpl(view.getContext(), display); 
                      view.setLayoutParams(wparams); 
                      //3.将视图对象view,ViewRootImpl以及wparams分别存入相应集合的对应位置 
                      mViews.add(view); 
                      mRoots.add(root); 
                      mParams.add(wparams); 
                  } 
                  // do this last because it fires off messages to start doing things 
                  try { 
                      //4.通过ViewRootImpl联系WindowManagerService将view绘制到屏幕上 
                      root.setView(view, wparams, panelParentView); 
                  } catch (RuntimeException e) { 
                      // BadTokenException or InvalidDisplayException, clean up. 
                      synchronized (mLock) { 
                          final int index = findViewLocked(view, false); 
                          if (index >= 0) { 
                              removeViewLocked(index, true); 
                          } 
                      } 
                      throw e; 
                  } 
              }

            //2.如果WindowManagerImpl是在activity的方法中被创建则不为空    
              if (parentWindow != null) { 
                       parentWindow.adjustLayoutParamsForSubWindow(wparams); 
                   } else { 
                   ...... 
                   }

              2、这里会首先判断一个类型为Window的parentWindow 是否为空,如果不为空会通过Window的adjustLayoutParamsForSubWindow方法调整一个类型为WindowManager.LayoutParams的变量wparams的一些属性值。应用程序请求WindowManagerService服务时会传入一个Token,其实那个Token就会通过Window的adjustLayoutParamsForSubWindow方法存放在wparams的token变量中,也就是说如果没有调用Window的adjustLayoutParamsForSubWindow方法就会导致wparams的token变量为空。然后我们接下来看一下wparams的token变量是如何赋值的:

              void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) { 
                      CharSequence curTitle = wp.getTitle(); 
                      if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW && 
                          wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) { 
                      ...... 
                      } else { 
                          if (wp.token == null) { 
                              wp.token = mContainer == null ? mAppToken : mContainer.mAppToken; 
                          } 
                      ...... 
                      } 
                      if (wp.packageName == null) { 
                          wp.packageName = mContext.getPackageName(); 
                      } 
                      if (mHardwareAccelerated) { 
                          wp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; 
                      }

                这里我们可以看到这段代码首先会做一个判断如果wp.type的值有没有位于WindowManager.LayoutParams.FIRST_SUB_WINDOW与WindowManager.LayoutParams.LAST_SUB_WINDOW之间,如果没有则会给wp.token赋值。wp.type代表窗口类型,有3种级别,分别为系统级,应用级以及子窗口级。而这里是判断是否为子窗口级级别。而Dialog的WindowManager.LayoutParams.type默认是应用级的,因此会走else分支,给wp.token赋值mAppToken。至于mAppToken是什么,我们待会再来分析。

                3、看WindowManagerGlobal的addView方法的,会调用ViewRootImpl的setView方法,我们来看一下,ViewRootImpl是如何连接WindowManagerService传递token的:

                public void setView(View view, WindowManager.LayoutParams attrs, View  
                panelParentView) { 
                        synchronized (this) { 
                            if (mView == null) { 
                                mView = view; 
                                try { 
                                    ...... 
                                    //1.通过binder对象mWindowSession调用WindowManagerService的接口请求 
                                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, 
                                            getHostVisibility(), mDisplay.getDisplayId(), 
                                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, 
                                            mAttachInfo.mOutsets, mInputChannel); 
                                } catch (RemoteException e) { 
                                    ...... 
                                    throw new RuntimeException("Adding window failed", e); 
                                } finally { 
                                    if (restore) { 
                                        attrs.restore(); 
                                    } 
                                } 
                                    ...... 
                                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?"); 
                                        //2.如果请求失败(token验证失败)则抛出BadTokenException异常 
                                        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 this type already  
                exists"); 
                                        case WindowManagerGlobal.ADD_PERMISSION_DENIED: 
                                            throw new WindowManager.BadTokenException( 
                                                    "Unable to add window " + mWindow + 
                                                    " -- permission denied for this window type"); 
                                        case WindowManagerGlobal.ADD_INVALID_DISPLAY: 
                                            throw new WindowManager.InvalidDisplayException( 
                                                    "Unable to add window " + mWindow + 
                                                    " -- the specified display can not be found"); 
                                      
                                
                                        
                					
                                
                                

                相关推荐

                android下vulkan与opengles纹理互通

                talkchan · 1178浏览 · 2020-11-23 10:37:39
                Android 使用RecyclerView实现轮播图

                奔跑的男人 · 2175浏览 · 2019-05-09 17:11:13
                微软发布新命令行工具 Windows Terminal

                吴振华 · 869浏览 · 2019-05-09 17:15:04
                Facebook 停止屏蔽部分区块链广告

                · 754浏览 · 2019-05-09 17:20:08
                加载中

                0评论

                评论
                分类专栏
                小鸟云服务器
                扫码进入手机网页