Android進(jìn)階之Dialog對(duì)應(yīng)的Context必須是Activity嗎?從源碼詳細(xì)分析
前言
創(chuàng)建Dialog的時(shí)候知道在Dialog的構(gòu)造方法中需要一個(gè)上下文環(huán)境,而對(duì)這個(gè)“上下文”沒(méi)有具體的概念結(jié)果導(dǎo)致程序報(bào)錯(cuò),
于是發(fā)現(xiàn)Dialog需要的上下文環(huán)境只能是activity。
所以接下來(lái)這篇文章將會(huì)從源碼的角度來(lái)徹底的理順這個(gè)問(wèn)題;
一、Dialog創(chuàng)建失敗
在Dialog的構(gòu)造方法中傳入一個(gè)Application的上下文環(huán)境。看看程序是否報(bào)錯(cuò):
- Dialog dialog = new Dialog(getApplication());
- TextView textView = new TextView(this);
- textView.setText("使用Application創(chuàng)建Dialog");
- dialog.setContentView(textView);
- dialog.show();
運(yùn)行程序,程序不出意外的崩潰了,我們來(lái)看下報(bào)錯(cuò)信息:
- 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)
這段錯(cuò)誤日志,有兩點(diǎn)我們需要注意一下
- 程序報(bào)了一個(gè)BadTokenException異常;
- 程序報(bào)錯(cuò)是在ViewRootImpl的setView方法中;
- 我們一定很疑惑BadTokenException到底是個(gè)啥,在說(shuō)明這個(gè)之前我們首先需要了解Token,在了解了Token的概念之后,再結(jié)合ViewRootImpl的setView方法,就能理解BadTokenException這個(gè)到底是什么,怎么產(chǎn)生的;
二、Token分析
1、token詳解
Token直譯成中文是令牌的意思,android系統(tǒng)中將其作為一種安全機(jī)制,其本質(zhì)是一個(gè)Binder對(duì)象,在跨進(jìn)程的通行中充當(dāng)驗(yàn)證碼的作用。比如:在activity的啟動(dòng)過(guò)程及界面繪制的過(guò)程中會(huì)涉及到ActivityManagerService,應(yīng)用程序,WindowManagerService三個(gè)進(jìn)程間的通信,此時(shí)Token在這3個(gè)進(jìn)程中充當(dāng)一個(gè)身份驗(yàn)證的功能,ActivityManagerService與WindowManagerService通過(guò)應(yīng)用程序的activity傳過(guò)來(lái)的Token來(lái)分辨到底是控制應(yīng)用程序的哪個(gè)activity。具體來(lái)說(shuō)就是:
- 在啟動(dòng)Activity的流程當(dāng)中,首先,ActivityManagerService會(huì)創(chuàng)建ActivityRecord由其本身來(lái)管理,同時(shí)會(huì)為這個(gè)ActivityRecord創(chuàng)建一個(gè)IApplication(本質(zhì)上就是一個(gè)Binder)。
- ActivityManagerService將這個(gè)binder對(duì)象傳遞給WindowManagerService,讓W(xué)indowManagerService記錄下這個(gè)Binder。
- 當(dāng)ActivityManagerService這邊完成數(shù)據(jù)結(jié)構(gòu)的添加之后,會(huì)返回給ActivityThread一個(gè)ActivityClientRecord數(shù)據(jù)結(jié)構(gòu),中間就包含了Token這個(gè)Binder對(duì)象。
- ActivityThread這邊拿到這個(gè)Token的Binder對(duì)象之后,就需要讓W(xué)indowManagerService去在界面上添加一個(gè)對(duì)應(yīng)窗口,在添加窗口傳給WindowManagerService的數(shù)據(jù)中WindowManager.LayoutParams這里面就包含了Token。
- 最終WindowManagerService在添加窗口的時(shí)候,就需要將這個(gè)Token的Binder和之前ActivityManagerService保存在里面的Binder做比較,驗(yàn)證通過(guò)說(shuō)明是合法的,否則,就會(huì)拋出BadTokenException這個(gè)異常。
- 到這里,我們就知道BadTokenException是怎么回事了,然后接下來(lái)分析為什么使用Application上下文會(huì)報(bào)BadTokenException異常,而Activity上下文則不會(huì)

2、為什么非要一個(gè)Token
因?yàn)樵赪MS那邊需要根據(jù)這個(gè)Token來(lái)確定Window的位置(不是說(shuō)坐標(biāo)),如果沒(méi)有Token的話,就不知道這個(gè)窗口應(yīng)該放到哪個(gè)容器上了;
因?yàn)榉茿ctivity的Context它的WindowManger沒(méi)有ParentWindow,導(dǎo)致在WMS那邊找不到對(duì)應(yīng)的容器,也就是不知道要把Dialog的Window放置在何處。
還有一個(gè)原因是沒(méi)有SYSTEM_ALERT_WINDOW權(quán)限(當(dāng)然要加權(quán)限啦,DisplayArea.Tokens的子容器,級(jí)別比普通應(yīng)用的Window高,也就是會(huì)顯示在普通應(yīng)用Window的前面,如果不加權(quán)限控制的話,被濫用還得了)。
在獲得SYSTEM_ALERT_WINDOW權(quán)限并將Dialog的Window.type指定為SYSTEM_WINDOW之后能正常顯示,是因?yàn)閃MS會(huì)為SYSTEM_WINDOW類型的窗口專門(mén)創(chuàng)建一個(gè)WindowToken(這下就有容器了),并放置在DisplayArea.Tokens里面(這下知道放在哪里了);
三、創(chuàng)建dialog流程分析
1、activity的界面最后是通過(guò)ViewRootImpl的setView方法連接WindowManagerService,從而讓W(xué)indowManagerService將界面繪制到手機(jī)屏幕上。而從上面的異常日志中其實(shí)也可以看出,Dialog的界面也是通過(guò)ViewRootImpl的setView連接WindowManagerService,從而完成界面的繪制的。
我們首先來(lái)看Dialog的構(gòu)造方法。不管一個(gè)參數(shù)的構(gòu)造方法。兩個(gè)參數(shù)的構(gòu)造方法,最終都會(huì)調(diào)用到3個(gè)參數(shù)的構(gòu)造方法:
- Dialog(@NonNull Context context, @StyleRes int themeResId, boolean
- createContextThemeWrapper) {
- ......
- //1.創(chuàng)建一個(gè)WindowManagerImpl對(duì)象
- mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- //2.創(chuàng)建一個(gè)PhoneWindow對(duì)象
- final Window w = new PhoneWindow(mContext);
- mWindow = w;
- //3.使dialog能夠響應(yīng)用戶的事件
- w.setCallback(this);
- w.setOnWindowDismissedCallback(this);
- //4.為window對(duì)象設(shè)置WindowManager
- w.setWindowManager(mWindowManager, null, null);
- w.setGravity(Gravity.CENTER);
- mListenersHandler = new ListenersHandler(this);
- }
這段代碼可以看出dialog的創(chuàng)建實(shí)質(zhì)上和activity界面的創(chuàng)建沒(méi)什么兩樣,都需要完成一個(gè)應(yīng)用窗口Window的創(chuàng)建,和一個(gè)應(yīng)用窗口視圖對(duì)象管理者WindowManagerImpl的創(chuàng)建。
然后Dialog同樣有一個(gè)setContentView方法:
- public void setContentView(@LayoutRes int layoutResID) {
- mWindow.setContentView(layoutResID);
- }
- 依然是調(diào)用PhoneWindow的setContentView方法。再接著我們來(lái)看下dialog的show方法:
- public void show() {
- ......
- //1.得到通過(guò)setView方法封裝好的DecorView
- mDecor = mWindow.getDecorView();
- ......
- //2.得到創(chuàng)建PhoneWindow時(shí)已經(jīng)初始化的成員變量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.通過(guò)WindowManagerImpl添加DecorView到屏幕
- mWindowManager.addView(mDecor, l);
- mShowing = true;
- sendShowMessage();
- } finally {
- }
- }
這段代碼和activity的makeVisable方法類似,這里也不多說(shuō)了,注釋已經(jīng)大概的寫(xiě)清楚了。然后調(diào)用WindowManagerImpl的addView方法:
- @Override
- public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
- applyDefaultToken(params);
- mGlobal.addView(view, params, mDisplay, mParentWindow);
- }
- 接著調(diào)用了WindowManagerGlobal的addView方法:
- public void addView(View view, ViewGroup.LayoutParams params,
- Display display, Window parentWindow) {
- ......
- //1.將傳進(jìn)來(lái)的ViewGroup.LayoutParams類型的params轉(zhuǎn)成
- WindowManager.LayoutParams類型的wparams
- final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)
- params;
- //2.如果WindowManagerImpl是在activity的方法中被創(chuàng)建則不為空
- if (parentWindow != null) {
- parentWindow.adjustLayoutParamsForSubWindow(wparams);
- } else {
- ......
- }
- ViewRootImpl root;
- View panelParentView = null;
- synchronized (mLock) {
- ......
- root = new ViewRootImpl(view.getContext(), display);
- view.setLayoutParams(wparams);
- //3.將視圖對(duì)象view,ViewRootImpl以及wparams分別存入相應(yīng)集合的對(duì)應(yīng)位置
- mViews.add(view);
- mRoots.add(root);
- mParams.add(wparams);
- }
- // do this last because it fires off messages to start doing things
- try {
- //4.通過(guò)ViewRootImpl聯(lián)系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的方法中被創(chuàng)建則不為空
- if (parentWindow != null) {
- parentWindow.adjustLayoutParamsForSubWindow(wparams);
- } else {
- ......
- }
2、這里會(huì)首先判斷一個(gè)類型為Window的parentWindow 是否為空,如果不為空會(huì)通過(guò)Window的adjustLayoutParamsForSubWindow方法調(diào)整一個(gè)類型為WindowManager.LayoutParams的變量wparams的一些屬性值。應(yīng)用程序請(qǐng)求WindowManagerService服務(wù)時(shí)會(huì)傳入一個(gè)Token,其實(shí)那個(gè)Token就會(huì)通過(guò)Window的adjustLayoutParamsForSubWindow方法存放在wparams的token變量中,也就是說(shuō)如果沒(méi)有調(diào)用Window的adjustLayoutParamsForSubWindow方法就會(huì)導(dǎo)致wparams的token變量為空。然后我們接下來(lái)看一下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;
- }
這里我們可以看到這段代碼首先會(huì)做一個(gè)判斷如果wp.type的值有沒(méi)有位于WindowManager.LayoutParams.FIRST_SUB_WINDOW與WindowManager.LayoutParams.LAST_SUB_WINDOW之間,如果沒(méi)有則會(huì)給wp.token賦值。wp.type代表窗口類型,有3種級(jí)別,分別為系統(tǒng)級(jí),應(yīng)用級(jí)以及子窗口級(jí)。而這里是判斷是否為子窗口級(jí)級(jí)別。而Dialog的WindowManager.LayoutParams.type默認(rèn)是應(yīng)用級(jí)的,因此會(huì)走else分支,給wp.token賦值mAppToken。至于mAppToken是什么,我們待會(huì)再來(lái)分析。
3、看WindowManagerGlobal的addView方法的,會(huì)調(diào)用ViewRootImpl的setView方法,我們來(lái)看一下,ViewRootImpl是如何連接WindowManagerService傳遞token的:
- public void setView(View view, WindowManager.LayoutParams attrs, View
- panelParentView) {
- synchronized (this) {
- if (mView == null) {
- mView = view;
- try {
- ......
- //1.通過(guò)binder對(duì)象mWindowSession調(diào)用WindowManagerService的接口請(qǐng)求
- 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.如果請(qǐng)求失?。╰oken驗(yàn)證失?。﹦t拋出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");
- case WindowManagerGlobal.ADD_INVALID_TYPE:
- throw new WindowManager.InvalidDisplayException(
- "Unable to add window " + mWindow
- + " -- the specified window type is not valid");
- }
- throw new RuntimeException(
- "Unable to add window -- unknown error code " + res);
- }
- ......
- }
- }
- }
這段代碼有兩處需要注意:
- 會(huì)通過(guò)一個(gè)mWindowSession的binder對(duì)象請(qǐng)求WindowManagerService服務(wù),傳遞一個(gè)類型為WindowManager.LayoutParams的變量mWindowAttributes到WindowManagerService,mWindowAttributes里面裝有代表當(dāng)前activity的token對(duì)象。然后通過(guò)WindowManagerService服務(wù)創(chuàng)建屏幕視圖。
- 會(huì)根據(jù)請(qǐng)求WindowManagerService服務(wù)的返回結(jié)果判斷是否請(qǐng)求成功,如果請(qǐng)求失敗會(huì)拋出異常,注釋的地方就是在文章開(kāi)頭示例拋出的異常。此時(shí)attrs.token為空。如果創(chuàng)建dialog的上下文環(huán)境改為activity的為什么就不為空呢?
四、分析創(chuàng)建Dialog的上下文Activity為何與眾不同
1、上文的分析中可以看出attrs.token的賦值在Window的adjustLayoutParamsForSubWindow方法中。而Dialog默認(rèn)的WindowManager.LayoutParams.type是應(yīng)用級(jí)別的,因此,如果能進(jìn)入這個(gè)方法內(nèi),attrs.token肯定能被賦值?,F(xiàn)在只有一種情況,如果不是activity的上下文環(huán)境就沒(méi)有進(jìn)入到這個(gè)方法內(nèi)。這時(shí)我們?cè)倏碬indowManagerGlobal的addView方法的:
- public void addView(View view, ViewGroup.LayoutParams params,
- Display display, Window parentWindow) {
- ......
- //2.如果WindowManagerImpl是在activity的方法中被創(chuàng)建則不為空
- if (parentWindow != null) {
- parentWindow.adjustLayoutParamsForSubWindow(wparams);
- } else {
- ......
- }
- ......
- }
從這里看出如果Window類型的parentWindow為空,就不會(huì)進(jìn)入adjustLayoutParamsForSubWindow方法。從而可以得出結(jié)論如果不是activity的上下文環(huán)境WindowManagerGlobal的第四個(gè)參數(shù)parentWindow為空。緊接著我們?cè)賮?lái)分析為什么其他的上下文會(huì)導(dǎo)致parentWindow為空。
WindowManagerGlobal調(diào)用addView方法在WindowManagerImpl的addView方法中:
- @Override
- public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
- applyDefaultToken(params);
- mGlobal.addView(view, params, mDisplay, mParentWindow);
- }
- WindowManagerImpl的addView方法在Dialog的首位方法中調(diào)用:
- public void show() {
- ......
- try {
- mWindowManager.addView(mDecor, l);
- mShowing = true;
- sendShowMessage();
- } finally {
- }
- }
對(duì)比這兩個(gè)方法。可以看出WindowManagerImpl的addView方法調(diào)用WindowManagerGlobal的addView方法是多出來(lái)了兩個(gè)參數(shù)mDisplay, mParentWindow,我們只看后一個(gè),多了一個(gè)Window類型的mParentWindow,可以一mParentWindow并不是在Dialog的show方法中賦值的。那么它在哪賦值呢?在WindowManagerImpl類中搜索mParentWindow發(fā)現(xiàn)它在WindowManagerImpl的兩個(gè)參數(shù)的構(gòu)造方法中被賦值。從這里我們可以猜測(cè),如果是使用的activity上下文,那么在創(chuàng)建WindowManagerImpl實(shí)例的時(shí)候用的是兩個(gè)參數(shù)的構(gòu)造方法,而其他的上下文是用的一個(gè)參數(shù)的構(gòu)造方法?,F(xiàn)在問(wèn)題就集中到了WindowManagerImpl是如何被創(chuàng)建的了。
我們?cè)倩剡^(guò)頭來(lái)看Dialog的構(gòu)造方法中WindowManagerImpl是如何創(chuàng)建的:
- Dialog(@NonNull Context context, @StyleRes int themeResId, boolean
- createContextThemeWrapper) {
- ......
- mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- ......
- }
- 然后分別查看activity的getSystemService方法,和Application的getSystemService方法:
- activity的getSystemService方法
- @Override
- public Object getSystemService(@ServiceName @NonNull String name) {
- ......
- if (WINDOW_SERVICE.equals(name)) {
- return mWindowManager;
- } else if (SEARCH_SERVICE.equals(name)) {
- ensureSearchManager();
- return mSearchManager;
- }
- return super.getSystemService(name);
- }
在這個(gè)方法中直接返回了activity的mWindowManager對(duì)象,activity的mWindowManager對(duì)象在activity的attach方法中:
- final void attach(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) {
- ......
- mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken, mComponent.flattenToString(),
- (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
- ......
- }
2、我們?cè)倏碬indow的setWindowManager方法:
- public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
- boolean hardwareAccelerated) {
- //1.將ActivityManagerService傳過(guò)來(lái)的Token保存到mAppToken中
- mAppToken = appToken;
- //2.創(chuàng)建WindowManagerImpl
- mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
- }
這段代碼兩個(gè)地方需要注意,一是前ActivityManagerService傳過(guò)來(lái)的Token賦值給Winow的mAppToken,這個(gè)token最后會(huì)保存到attr.token,具體操作在Window的adjustLayoutParamsForSubWindow方法中。二是調(diào)用WindowManagerImpl的createLocalWindowManager方法創(chuàng)建WindowManagerImpl:
- public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
- return new WindowManagerImpl(mDisplay, parentWindow);
- }
到這里就可以看出如果創(chuàng)建Dialog的上下文是activity,則會(huì)調(diào)用WindowManagerImpl兩個(gè)參數(shù)的構(gòu)造方法,從而導(dǎo)致parentWindow不為空。
3、Application的getSystemService方法:
由于Application是Context的子類,所以Application的getSystemService最終會(huì)調(diào)到ContextImpl的getSystemService方法
- @Override
- public Object getSystemService(String name) {
- return SystemServiceRegistry.getSystemService(this, name);
- }
- 直接調(diào)用了SystemServiceRegistry的getSystemService方法,這個(gè)方法又會(huì)得到匿名內(nèi)部類CachedServiceFetcher<WindowManager>的createService方法的返回值。
- @Override
- public WindowManager createService(ContextImpl ctx) {
- return new WindowManagerImpl(ctx.getDisplay());
- }});
從這個(gè)方法中可以看出上下文為Application時(shí),調(diào)用的是WindowManagerImpl的一個(gè)參數(shù)的構(gòu)造方法,從而parentWindow為空;
總結(jié)
- 創(chuàng)建dialog時(shí),如果傳入構(gòu)造方法不是一個(gè)activity類型的上下文,則導(dǎo)致WindowManagerImpl類型為Window的變量mParentWindow,從而導(dǎo)致WindowManagerGlobal的addView不會(huì)調(diào)用Window的adjustLayoutParamsForSubWindow方法,從而不會(huì)給attr.token賦值,導(dǎo)致在WindowManagerService服務(wù)中的身份驗(yàn)證失敗,拋出BadTokenException異常;
- Show一個(gè)普通的Dialog需要的并不是Activity本身,而是一個(gè)容器的token,我們平時(shí)會(huì)傳Activity,只不過(guò)是Activity剛好對(duì)應(yīng)WMS那邊的一個(gè)WindowState的容器而已;
本文轉(zhuǎn)載自微信公眾號(hào)「Android開(kāi)發(fā)編程」