咱们好,信任咱们在使用Dialog时,都有一个非常基本的认知:便是Dialog的context只能是Activity,而不能是Application,不然会导致弹窗溃散:

一文洞彻:Application为啥不能作为Dialog的context?

这个Exception简直属于是每个Android开发初学者都会碰到的,但是。

前几天研讨项目代码发现 Application作为Dialogcontext居然不会溃散?!!这句话说出来和本篇文章标题严峻不符哈,这不是赤裸裸的打脸了吗。先别急,请咱们跟着我的脚步,信任阅览完本篇文章就能够解答现在你心目中最大的两个疑惑:

  1. 如标题所言,为啥Application无法作为Dialog的context并导致溃散?
  2. 项目中为啥又发现,Application作为Dialog的context能够正常显现弹窗?

一. 窗口(包含Activity和Dialog)怎么显现的?

这儿怕有些童鞋不了解窗口(包含Activity和Dialog的)的显现流程,先简略的介绍下:

不管是Activity界面的显现仍是DIalog的窗口显现,都会调用到WindowManagerImpl#addView()办法,这个办法经过一连续调用,会走到ViewRootImpl#setView()办法中。

在这个办法中,咱们终究会调用到IWindowSession#addToDisplayAsUser()办法,这个办法是一个跨进程的调用,经过一番折腾,终究会执行到WMS的addWindow()办法。

在这个办法中会将窗口的信息进行保存管理,而且关于窗口的信息进行校验,比方上面的溃散信息:“BadTokenException: Unable to add window”便是由于在这个办法中查验失败导致的;别的也是在这个办法中将窗口和Surface、Layer制作树立起了连接(这句话说的可能不标准,主要对这块了解不多,懂得大佬能够谈论分享下)。

接着开端在ViewRootImpl#setView()执行requestLayout()办法,开端进行烘托制作等。

有了上面的简略介绍,接下来咱们就开端先剖析为啥Application作为Dialog的context会反常。

二. 窗口离不开的WindowManagerImpl

上面也说了,窗口只需显现,就得凭借WindowManagerImpl#addView()办法,而WindowManagerImpl创立流程在ApplicationActivity的差异,便是Application作为Dialogcontext会反常的中心原因

咱们就从下面办法作为入口进行剖析:

context.getSystemService(WINDOW_SERVICE)

1. Application下WindowManagerImpl的创立

关于Application而言,getSystemService()办法的调用,终究会走到父类ContextWrapper中:

一文洞彻:Application为啥不能作为Dialog的context?

而这个mBase特点对应的类为ContextImpl目标,对应ContextImpl#getSystemService():

一文洞彻:Application为啥不能作为Dialog的context?

对应SystemServiceRegistry#getSystemService

一文洞彻:Application为啥不能作为Dialog的context?

SYSTEM_SERVICE_FETCHERS是一个Map调集,对应的key为服务的名称,value为服务的完成办法:

一文洞彻:Application为啥不能作为Dialog的context?

Android会在SystemServiceRegistry初始化的时分将各种服务以及服务的完成办法注册到这个调集中:

一文洞彻:Application为啥不能作为Dialog的context?

一文洞彻:Application为啥不能作为Dialog的context?

接下来看下咱们关怀的WindowManager服务的注册办法:

一文洞彻:Application为啥不能作为Dialog的context?

到了这儿,咱们就理解了,调用context.getSystemService(WINDOW_SERVICE)会返回一个WindowManagerImpl目标,中心点就在于WindowManagerImpl的结构函数,能够看到结构函数只传入了一个ContextImpl目标,咱们看下其结构办法:

一文洞彻:Application为啥不能作为Dialog的context?

本篇文章重要的当地来了:经过这种办法创立的WindowManagerImpl目标,其mParentWindow特点是null的

2. Activity下WindowManagerImpl的创立

Activity重写了getSystemService()办法:

一文洞彻:Application为啥不能作为Dialog的context?

而mWindowManager特点的赋值是产生在Activity#attach()办法中:

一文洞彻:Application为啥不能作为Dialog的context?

这个mWindow特点对应的类型为Window类型(其仅有完成类为咱们耳熟能详的PhoneWindow,其创立时机和Activity创立的时机是一起的),走进去看下:

一文洞彻:Application为啥不能作为Dialog的context?

一文洞彻:Application为啥不能作为Dialog的context?

经过一层层的调用,终究咱们的WindowManager是经过WindowManagerImpl#createLocalWindowManager创立的,而且参数传入的是当前的Window目标,即PhoneWindow。

一文洞彻:Application为啥不能作为Dialog的context?

能够看到,该办法终究帮助咱们创立了WindowManagerImpl目标,要害点是其mParentWindow特点的值为上面传入的PhoneWindow,不为null

小结:

Activity获取到的WindManager服务,即WindowManagerImpl的mParentWindow特点不为空,而Application获取的mParentWindow特点为null。

文章最初咱们简略介绍了窗口的显现流程,一起又知道完成窗口添加的要害类WindowManagerImpl的来头,有了这些铺垫,接下来咱们就对窗口的显现进行一个比较深入的剖析。

三. 深入探究窗口的显现流程

这儿咱们就从WindowManagerGlobal#addView()办法说起,它是WindowManagerImpl#addView()办法的真实完成者。

WindowManagerImpl#addView():

一文洞彻:Application为啥不能作为Dialog的context?

WindowManagerGlobal#addView():

一文洞彻:Application为啥不能作为Dialog的context?

这一剖析,就进入到了本篇文章最重要的一个办法的剖析,如上面红框所示。

前面咱们有讲过,关于Application获取的WindowManagerImpl,其mParentWindow特点为null,而Activity对应的mParentWindow不为null。

  1. 假如当前为Activity的窗口,或者凭借Activity作为Context显现的Dialog窗口,其会走入到办法adjustLayoutParamsForSubWindow()中,对应的完成类为Window

一文洞彻:Application为啥不能作为Dialog的context?

type为窗口的类型,关于Activity的窗口仍是关于Dialog的窗口,其对应类型为都为2(TYPE_APPLICATION),所以终究都会走到红框中的位置,终究给window对应的layoutparam目标的token特点赋值为mAppToken

这个mAppToken能够简略理解为窗口的一种凭证,它是AMS在startActivity流程的时分被初始化的,然后传递给使用侧,终究再用来WMS进行窗口查验的其间在AMS的startActivity流程中,会将这个AppToken作为key,并结构一个WindowToken目标作为value,写入到 DisplayContent#mTokenMap调集中,这部分具体的源码剖析能够参阅文章:Android高工面试(难度:四星):为什么不能使用 Application Context 显现 Dialog?

  1. 假如当前为application作为context显现的Dialog,mParentWindow为null,那就走不到adjustLayoutParamsForSubWindow()办法中,天然其Window#LayoutParam#token特点便是null。

咱们再次回到WindowManagerGlobal#addView()办法中,接下来会走到ViewRootImpl#setView()办法中,这个办法里终究会调用下面办法完成窗口真实的添加:

一文洞彻:Application为啥不能作为Dialog的context?

其间这个mWindowSession对应是一个Binder目标,对应类型为IWindowSession,其真实的完成坐落system_server侧的Session类,所以这儿会产生跨进程通讯,并将window的LayoutParam类型参数进行传入,咱们持续看下Session#addToDiaplayAsUser办法:

一文洞彻:Application为啥不能作为Dialog的context?

mService对应的完成类WindowManagerService,所以咱们看下该类的addWindow办法:

# WindowManagerService
final HashMap<IBinder, WindowState> mWindowMap = new HashMap<>();
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
            int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls) {
            WindowState parentWindow = null;
            final int type = attrs.type;
            //1. 
     		if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
                parentWindow = windowForClientLocked(null, attrs.token, false);
              //...
            }
        	//2. 
            final boolean hasParent = parentWindow != null;
            WindowToken token = displayContent.getWindowToken(
                    hasParent ? parentWindow.mAttrs.token : attrs.token);
        	//3.
            if (token == null) {
                if (!unprivilegedAppCanCreateTokenWith(parentWindow, callingUid, type,
                        rootType, attrs.token, attrs.packageName)) {
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
            }
    		final WindowState win = new WindowState(this, session, client, token, parentWindow,
                    appOp[0], attrs, viewVisibility, session.mUid, userId,
                    session.mCanAddInternalSystemWindow);
}
# DiaplayConent
private final HashMap<IBinder, WindowToken> mTokenMap = new HashMap();
WindowToken getWindowToken(IBinder binder) {
    return mTokenMap.get(binder);
}

上面的代码是经过精简后的。

  1. 前面有提到,Dialog的窗口类型为2,所以不满足if的条件,天然parentWindow无法赋值,即为null;

  2. 这儿hasParent天然便是false,调用办法getWindowToken()传入的参数便是使用侧Window#LayoutParam#token特点,其间凭借前面剖析,假如Application作为Dialog的context,这个token值是null;

    看下getWindowToken()办法,它会将上面的传入token作为key,从DisplayContent#mTokenMap这个调集中获取值,什么时分写入值呢:前面有提到过,在startActivity的流程中,会向这个调集中写入值。而这个传入的token便是之前startActivity流程中,写入到DisplayContent#mTokenMap这个调集中的key,所以天然是能够获取到对应的value,即WindowToken类型特点token不为null,天然走不到3处标记的条件分支中,窗口校验经过。

  3. 而Application作为Dialog的context时,传入的token是null,天然是无法获取到值,WindowToken 类型特点token为null,走到if分支中,会返回WindowManagerGlobal.ADD_BAD_APP_TOKEN ,当使用侧检测到返回值为这个时,就会呈现文章一最初说的BadTokenException反常

到了这儿,信任你就理解了,为啥Application作为Dialog的context会导致溃散,要害的剖析便是上面的内容;

四. 不让Application作为Dialog的context溃散?

依据上面的剖析结果,Application作为Dialog的context溃散的真实原因便是使用侧传过来的LayoutParam#token目标是null的,已然这样,那咱们在使用侧给Dialog的Window#LayoutParam#token特点赋值为Activity的Window#LayoutParam#token特点,就能够避免这场悲惨剧产生了,能够看到下面能正常显现弹窗:

一文洞彻:Application为啥不能作为Dialog的context?

但是仍是不建议咱们这样做哈,究竟假如在Dialog中使用到了这个Application的context进行Activity的跳转等其他未知行为,估计就会呈现其他的幺蛾子了哈。

五. 总结

本篇文章涉及到的源码有点多,重点在于以下几个当地:

  1. Activity和Application获取WindowManager在使用侧服务的区别;
  2. 将窗口添加到WMS侧,Activity和Application下WindowManagerImpl传参token的区别;
  3. WMS中对应窗口类型以及传入的token是否为null进行的一番查验,现已查验不经过导致使用侧产生BadTokenException反常;

期望本篇文章能对你有所帮助,有什么需要沟通的也欢迎下谈论中留言,感谢阅览。

参阅文章

Android高工面试(难度:四星):为什么不能使用 Application Context 显现 Dialog?