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

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