~ read.

Android 中Context的不同类型

Dialog 的 Context 能不能是 Application 的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) 

从字面上也很容易理解adTokenException: Unable to add window -- token null is not for an application” ,发生一个adTokenException异常,不能添加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的关系:
window

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,想想就觉得很危险。

再添加一下Application,Activity,Service,ContentProvider等中的Context 适用范围:
context----

大家注意看到有一些NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?下面一个一个解释:
数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。
数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。
数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)
注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。
在表格里重点看Activity和Application,可以看到,和UI相关的方法基本都不建议或者不可使用Application,并且,前三个操作基本不可能在Application中出现。实际上,只要把握住一点,凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。
能使用Application的context的时候尽量使用Application的,减少内存开支。
在Application的Context中还有getApplication和getApplicationContext

getApplication返回结果为Application,且不同的Activity和Service返回的Application均为同一个全局对象,在ActivityThread内部有一个列表专门用于维护所有应用的application
final ArrayList mAllApplications = new ArrayList();
getApplicationContext返回的也是Application对象,只不过返回类型为Context,看看它的实现

public Context getApplicationContext() {  
    return (mPackageInfo != null) ?  
            mPackageInfo.getApplication() : mMainThread.getApplication();  
}  

上面代码中mPackageInfo是包含当前应用的包信息、比如包名、应用的安装目录等,原则上来说,作为第三方应用,包信息mPackageInfo不可能为空,在这种情况下,getApplicationContext返回的对象和getApplication是同一个。但是对于系统应用,包信息有可能为空,具体就不深入研究了。从这种角度来说,对于第三方应用,一个应用只存在一个Application对象,且通过getApplication和getApplicationContext得到的是同一个对象,两者的区别仅仅是返回类型不同。