转载
目录
为什么不能使用 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 中