2021-11-11 为什么不能使用 Application Context 显示 Dialog?

转载

目录

为什么不能使用 Application Context 显示 Dialog?

谁创建了 Token?

WMS 是如何拿到 Token 的?

WMS 是如何校验 Token 的?

为什么不能使用 Application Context 显示 Dialog?

在上一篇文章扒一扒 Context中遗留了一个问题:

为什么不能使用 Application Context 显示 Dialog ?

写个简单的测试代码,如下:

Dialog dialog=newDialog(getApplicationContext());dialog.show();复制代码

运行时会得到这样一个错误:

Causedby:android.view.WindowManager$BadTokenException:Unabletoaddwindow--tokennullisnot valid;isyour activity running?at android.view.ViewRootImpl.setView(ViewRootImpl.java:951)at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:387)at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:96)at android.app.Dialog.show(Dialog.java:344)at luyao.android.context.ContextActivity.showDialog(ContextActivity.java:31)复制代码

看到报错信息中的token,不知道你还记不记得上篇文章中介绍过的Activity.attach()方法:

finalvoidattach(Context context,ActivityThread aThread,Instrumentation instr,IBinder token,int ident,Application application,Intent intent,ActivityInfo info,CharSequence title,Activity parent,String id,NonConfigurationInstances lastNonConfigurationInstances,Configuration config,String referrer,IVoiceInteractor voiceInteractor,Window window,ActivityConfigCallback activityConfigCallback){// 回调 attachBaseContext()attachBaseContext(context);......// 创建 PhoneWindowmWindow=newPhoneWindow(this,window,activityConfigCallback);......// 第二个参数是 mTokenmWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken,mComponent.flattenToString(),(info.flags&ActivityInfo.FLAG_HARDWARE_ACCELERATED)!=0);......}复制代码

Activity 被创建之后会调用 attach() 方法,做了这么几件事:

创建了 PhoneWindow 对象mWondow

给当前 window 绑定mToken

......

这里的 IBinder 对象mToken很重要。它是一个 Binder 对象,可以在 app 进程,system_server 进程之间进行传递。和我们通常所说的 Token 一样,这里也可以把它看做是一种特殊的令牌,用来标识 Window ,在对 Window 进行视图操作的时候就可以做一些校验工作。

所以,Activity 对应的 Window/WMS 都是持有这个 mToken 的。结合之前 Application 创建 Dialog 的报错信息,我们可以大胆猜测Application Context 创建 Dialog 的过程中,并没有实例化类似的 token。

回到 Dialog 的构造函数中,

Dialog(@NonNullContextcontext,@StyleResintthemeResId,booleancreateContextThemeWrapper){......// 获取 WindowManagermWindowManager=(WindowManager)context.getSystemService(Context.WINDOW_SERVICE);finalWindoww=newPhoneWindow(mContext);mWindow=w;......}复制代码

根据传入的 Context 调用getSystemService(Context.WINDOW_SERVICE)方法来得到 WindowManager 对象mWindowManager,最终会通过mWindowManager.addWindow()来显示 dialog 。

如果传入的 Context 是 Activity,返回的是在Activity.attach()方法中创建的mWindowManager对象,这个时候mToken也已经绑定。

>Activity.java@OverridepublicObjectgetSystemService(@ServiceName@NonNullString name){......if(WINDOW_SERVICE.equals(name)){returnmWindowManager;// 在 attach() 方法中已经实例化}elseif(SEARCH_SERVICE.equals(name)){ensureSearchManager();returnmSearchManager;}returnsuper.getSystemService(name);}复制代码

如果传入的 Context 是 Application,最终调用的是父类ContextImpl的方法。

@OverridepublicObjectgetSystemService(String name){returnSystemServiceRegistry.getSystemService(this,name);}复制代码

SystemServiceRegistry.getSystemService(this, name)拿到的是已经提前初始化完成并缓存下来的系统服务,并没有携带任何的 Token。

registerService(Context.WINDOW_SERVICE,WindowManager.class,newCachedServiceFetcher<WindowManager>(){@OverridepublicWindowManagercreateService(ContextImplctx){returnnewWindowManagerImpl(ctx);}});复制代码

所以,Android 不允许 Activity 以外的 Context 来创建和显示普通的 Dialog 。这里的普通指的是文章开头示例代码中的普通 Dialog,并非 Toast,System Dialog 等等。Android 系统为了安全考虑,不想在 App 进入后台之后仍然可以弹出 Dialog,这样就会产生可以在其他 App 中弹窗的场景,造成一定的安全隐患。虽然通过 Dialog Theme 的 Activity 仍然可以实现这一需求,但是 Google 也在加强对后台启动 Activity 的限制

写到这里,问题似乎已经得到了解答。但是其实我们对于整个 Token 流程还是一片雾水的。试着想一下下面几个问题。

mToken 是在什么时机,什么地方创建的?

WMS 是如何拿到 mToken 的?

WMS 是如何校验 token 的?

......

真正掌握了这些问题之后,才能形成一个完整的知识闭环,但伴随而来的,是逃避不了的,枯燥乏味的Read the fucking AOSP 。

谁创建了 Token?

先来看看 Token 到底是个什么样的类。

>ActivityRecord.javastaticclassTokenextendsIApplicationToken.Stub{privatefinalWeakReference<ActivityRecord>weakActivity;privatefinalStringname;Token(ActivityRecordactivity,Intentintent){weakActivity=newWeakReference<>(activity);name=intent.getComponent().flattenToShortString();}......}复制代码

Token是ActivityRecord的静态内部类,它持有外部 ActivityRecord 的弱引用。继承自IApplicationToken.Stub,是一个 Binder 对象。它在 ActivityRecord 的构造函数中初始化。

>ActivityRecord.javaActivityRecord(ActivityManagerService _service,ProcessRecord _caller,int _launchedFromPid,int _launchedFromUid,String _launchedFromPackage,Intent _intent,String _resolvedType,ActivityInfo aInfo,Configuration _configuration,ActivityRecord _resultTo,String _resultWho,int _reqCode,boolean _componentSpecified,boolean _rootVoiceInteraction,ActivityStackSupervisor supervisor,ActivityOptions options,ActivityRecord sourceRecord){service=_service;// 初始化 appTokenappToken=newToken(this,_intent);......}复制代码

一个ActivtyRecord代表一个 Activity 实例, 它包含了 Activity 的所有信息。在 Activity 的启动过程中,当执行到ActivityStarter.startActivity()时,会创建待启动的 ActivityRecord 对象,也间接创建了 Token 对象。

>ActivityStarter.javaprivateintstartActivity(IApplicationThreadcaller,Intentintent,IntentephemeralIntent,StringresolvedType,ActivityInfoaInfo,ResolveInforInfo,IVoiceInteractionSessionvoiceSession,IVoiceInteractorvoiceInteractor,IBinderresultTo,StringresultWho,intrequestCode,intcallingPid,intcallingUid,StringcallingPackage,intrealCallingPid,intrealCallingUid,intstartFlags,SafeActivityOptionsoptions,booleanignoreTargetSecurity,booleancomponentSpecified,ActivityRecord[]outActivity,TaskRecordinTask,booleanallowPendingRemoteAnimationRegistryLookup,PendingIntentRecordoriginatingPendingIntent){......// 构建 ActivityRecord,其中会初始化 tokenActivityRecordr=newActivityRecord(mService,callerApp,callingPid,callingUid,callingPackage,intent,resolvedType,aInfo,mService.getGlobalConfiguration(),resultRecord,resultWho,requestCode,componentSpecified,voiceSession!=null,mSupervisor,checkedOptions,sourceRecord);......returnstartActivity(r,sourceRecord,voiceSession,voiceInteractor,startFlags,true/* doResume */,checkedOptions,inTask,outActivity);}复制代码

到这里,ActivityRecord.appToken已经被赋值。所以 Token 是在 AMS 的 startActivity 流程中创建的。但是 Token 的校验显然是发生在 WMS 中的,所以 AMS 还得把 Token 交到 WMS 。

WMS 是如何拿到 Token 的?

继续跟下去,startActivity()最后会调用到ActivityStack.startActivityLocked(),这个方法就是把 Token 给到 WMS 的关键。

>ActivityStack.javavoidstartActivityLocked(ActivityRecordr,ActivityRecordfocusedTopActivity,booleannewTask,booleankeepCurTransition,ActivityOptionsoptions){......if(r.getWindowContainerController()==null){// 创建 AppWindowContainerController 对象,其中包含 token 对象r.createWindowContainer();......}复制代码

其他代码都省略了,重点关注r.createWindowContainer(),这里的 r 就是一开始创建的 ActivityRecord 对象。

>ActivityRecord.javavoidcreateWindowContainer(){if(mWindowContainerController!=null){thrownewIllegalArgumentException("Window container="+mWindowContainerController+" already created for r="+this);}inHistory=true;finalTaskWindowContainerControllertaskController=task.getWindowContainerController();......// 构造函数中会调用 createAppWindow() 创建 AppWindowToken 对象mWindowContainerController=newAppWindowContainerController(taskController,appToken,this,Integer.MAX_VALUE/* add on top */,info.screenOrientation,fullscreen,(info.flags&FLAG_SHOW_FOR_ALL_USERS)!=0,info.configChanges,task.voiceSession!=null,mLaunchTaskBehind,isAlwaysFocusable(),appInfo.targetSdkVersion,mRotationAnimationHint,ActivityManagerService.getInputDispatchingTimeoutLocked(this)*1000000L);task.addActivityToTop(this);......}复制代码

在AppWindowContainerController的构造函数中传入了之前已经初始化过的appToken。

>AppWindowContainerController.javapublicAppWindowContainerController(TaskWindowContainerControllertaskController,IApplicationTokentoken,AppWindowContainerListenerlistener,intindex,intrequestedOrientation,booleanfullscreen,booleanshowForAllUsers,intconfigChanges,booleanvoiceInteraction,booleanlaunchTaskBehind,booleanalwaysFocusable,inttargetSdkVersion,introtationAnimationHint,longinputDispatchingTimeoutNanos,WindowManagerServiceservice){super(listener,service);mHandler=newH(service.mH.getLooper());mToken=token;synchronized(mWindowMap){......atoken=createAppWindow(mService,token,voiceInteraction,task.getDisplayContent(),inputDispatchingTimeoutNanos,fullscreen,showForAllUsers,targetSdkVersion,requestedOrientation,rotationAnimationHint,configChanges,launchTaskBehind,alwaysFocusable,this);......}}复制代码

createAppWindow()方法中会创建AppWindowToken对象,注意传入的 token 参数。

>AppWindowContainerController.javaAppWindowTokencreateAppWindow(WindowManagerServiceservice,IApplicationTokentoken,booleanvoiceInteraction,DisplayContentdc,longinputDispatchingTimeoutNanos,booleanfullscreen,booleanshowForAllUsers,inttargetSdk,intorientation,introtationAnimationHint,intconfigChanges,booleanlaunchTaskBehind,booleanalwaysFocusable,AppWindowContainerControllercontroller){returnnewAppWindowToken(service,token,voiceInteraction,dc,inputDispatchingTimeoutNanos,fullscreen,showForAllUsers,targetSdk,orientation,rotationAnimationHint,configChanges,launchTaskBehind,alwaysFocusable,controller);}复制代码

>AppWindowToken.javaAppWindowToken(WindowManagerServiceservice,IApplicationTokentoken,booleanvoiceInteraction,DisplayContentdc,booleanfillsParent){// 父类是 WindowTokensuper(service,token!=null?token.asBinder():null,TYPE_APPLICATION,true,dc,false/* ownerCanManageAppTokens */);appToken=token;mVoiceInteraction=voiceInteraction;mFillsParent=fillsParent;mInputApplicationHandle=newInputApplicationHandle(this);}复制代码

这里调用了父类的构造函数,AppWindowToken的父类是WindowToken。

>WindowToken.javaWindowToken(WindowManagerServiceservice,IBinder_token,inttype,booleanpersistOnEmpty,DisplayContentdc,booleanownerCanManageAppTokens,booleanroundedCornerOverlay){super(service);token=_token;windowType=type;mPersistOnEmpty=persistOnEmpty;mOwnerCanManageAppTokens=ownerCanManageAppTokens;mRoundedCornerOverlay=roundedCornerOverlay;// 接着跟进去onDisplayChanged(dc);}复制代码

>WindowToken.javavoidonDisplayChanged(DisplayContent dc){// 调用 DisplayContent.reParentWindowToken()dc.reParentWindowToken(this);mDisplayContent=dc;......}复制代码

>DisplayContent.javavoidreParentWindowToken(WindowToken token){......addWindowToken(token.token,token);}privatevoidaddWindowToken(IBinder binder,WindowToken token){......// mTokenMap 是一个 HashMap<IBinder, WindowToken>,mTokenMap.put(binder,token);......}复制代码

mTokenMap是一个HashMap<IBinder, WindowToken>对象,保存了 WindowToken 以及其 Token 信息。我们从 AMS 启动 Activity 一路追到这里,其实已经走到了 WMS 的逻辑。AMS 和 WMS 都是运行在 system_server 进程的,并不存在 binder 调用。AMS 就是按照上面的调用链把 Token 传递给了 WMS 。

再来一张清晰的流程图总结一下 Token 从 AMS 传递到 WMS 的整个流程:

WMS 是如何校验 Token 的?

其实一直很纠结源码解析类的文章应该怎么写。单纯说思想,但对很多人来说并不够;源码说多了,文章又显得枯燥乏味。大家有好的建议可以在评论区聊一聊。

这一块的源码我就不在文章里一点一点追了。大家可以对着下面的流程图自己啃一下源码。从Dialog.show()开始,最后走到WindowManagerService.addwWindow()。

在WMS.addWindow()方法中就会对 Token 进行校验,这一块来看一下源码。

publicintaddWindow(Sessionsession,IWindowclient,intseq,LayoutParamsattrs,intviewVisibility,intdisplayId,RectoutFrame,RectoutContentInsets,RectoutStableInsets,RectoutOutsets,DisplayCutout.ParcelableWrapperoutDisplayCutout,InputChanneloutInputChannel){......AppWindowTokenatoken=null;finalbooleanhasParent=parentWindow!=null;// 获取 WindowTokenWindowTokentoken=displayContent.getWindowToken(hasParent?parentWindow.mAttrs.token:attrs.token);if(token==null){if(rootType>=FIRST_APPLICATION_WINDOW&&rootType<=LAST_APPLICATION_WINDOW){Slog.w(TAG_WM,"Attempted to add application window with unknown token "+attrs.token+".  Aborting.");returnWindowManagerGlobal.ADD_BAD_APP_TOKEN;}......}else{......}......}复制代码

通过DisplayContent.getWindowToken()方法获取 WindowToken 对象之后,会对其进行一系列校验工作。看到DisplayContent,你应该能想到就是从上面提到过的mTokenMap集合中取值了。我们来看一看源码实现。

>DisplayContent.javaWindowTokengetWindowToken(IBinderbinder){returnmTokenMap.get(binder);}复制代码

没错,的确是从哈希表mTokenMap中直接获取。写到这里,整个流程已经走通了。

AMS 在启动 Activity 的时候,会构建表示 Activity 信息的 ActivityRecord 对象,其构造函数中会实例化 Token 对象

AMS 在接着上一步之后,会利用创建的 Token 构建 AppWindowContainerController 对象,最终将 Token 存储到 WMS 中的 mTokenMap 中

链接:https://juejin.im/post/6867390363020361742

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容