最近在參與一個關于副屏廣告的項目中,涉及到Presentation這個副屏類,class Presentation extends Dialog,指定display去在特定顯示器上顯示,如果是需要副屏在副屏,則指定為1即可。
在寫副屏demo過程中,發現當指定Dialog窗口類型Type為TYPE_APPLICATION這些普通應用窗口時,Context可以使用Activity 的context,而不能使用getApplicationContext(),否則報以下異常信息。
11-11 09:23:39.837 E/AndroidRuntime(17598): FATAL EXCEPTION: main
11-11 09:23:39.837 E/AndroidRuntime(17598): Process: com.will.Screen, PID: 17598
11-11 09:23:39.837 E/AndroidRuntime(17598): android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.view.ViewRootImpl.setView(ViewRootImpl.java:683)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:380)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.app.Dialog.show(Dialog.java:322)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.app.Presentation.show(Presentation.java:237)
11-11 09:23:39.837 E/AndroidRuntime(17598): at com.will.Screen.MainActivity$1.onClick(MainActivity.java:48)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.view.View.performClick(View.java:5647)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.view.View$PerformClick.run(View.java:22443)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.os.Handler.handleCallback(Handler.java:751)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.os.Handler.dispatchMessage(Handler.java:95)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.os.Looper.loop(Looper.java:154)
11-11 09:23:39.837 E/AndroidRuntime(17598): at android.app.ActivityThread.main(ActivityThread.java:6119)
11-11 09:23:39.837 E/AndroidRuntime(17598): at java.lang.reflect.Method.invoke(Native Method)
11-11 09:23:39.837 E/AndroidRuntime(17598): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
11-11 09:23:39.837 E/AndroidRuntime(17598): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
Log異常信息顯示token為null,token可以理解成一個窗口令牌。在分析這個異常發生原因前,先來理解幾個概念:
Window:定義窗口樣式和行為的抽象基類,用于作為頂層的view加到WindowManager中,其實現類是PhoneWindow。
每個Window都需要指定一個Type(應用窗口、子窗口、系統窗口)。Activity對應的窗口是應用窗口;PopupWindow,ContextMenu,OptionMenu是常用的子窗口;像Toast和系統警告提示框(如ANR)就是系窗口,還有很多應用的懸浮框也屬于系統窗口類型。
WindowManager:用來在應用與window之間的管理接口,管理窗口順序,消息等。
WindowManagerService:簡稱Wms,WindowManagerService管理窗口的創建、更新和刪除,顯示順序等,是WindowManager這個管理接口的真正的實現類。它運行在System_server進程,作為服務端,客戶端(應用程序)通過IPC調用和它進行交互。
Token:Token主是指窗口令牌(Window Token),是一種特殊的Binder令牌,Wms用它唯一標識系統中的一個窗口。
Activity有一個PhoneWindow,當我們調用setContentView時,其實最終結果是把我們的DecorView作為子View添加到PhoneWindow的DecorView中。而最終這個DecorView,過WindowMnagerImpl的addView方法添加到WMS中去的,由WMS負責管理和繪制(真正的繪制在SurfaceFlinger服務中)。
- Presentation窗口Type設置為應用窗口類型時
跟Activity對應的窗口一樣,Presentation繼承于Dialog,而Dialog有一個PhoneWindow的實例。當Presentation設置為是TYPE_APPLICATION,屬于應用窗口類型:
mPresentation.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION);
Dialog的構造函數為:
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
.......
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
android.util.Log.e("dialog","wjx----------w :" + w );
mWindow = w;
android.util.Log.e("dialog","wjx----------mWindow :" + mWindow );
android.util.Log.e("dialog","wjx------dialog----mWindowManager :" + mWindowManager );
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
注意w.setWindowManager(mWindowManager, null, null)這句,把appToken設置為null。這也是Dialog和Activity窗口的一個區別,Activity會將這個appToken設置為ActivityThread傳過來的token。
當使用的是Activity context時,如上的mWindowManager獲取的是Activity 的mWindowManager,在Activity的代碼實現如下:
public Object getSystemService(@ServiceName @NonNull String name) {
if (getBaseContext() == null) {
throw new IllegalStateException(
"System services not available to Activities before onCreate()");
}
if (WINDOW_SERVICE.equals(name)) {
android.util.Log.e("wjx","wjx---activity------getSystemService---mWindowManager:" + mWindowManager);
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
在執行w.setWindowManager(mWindowManager, null, null)時,最終會執行到Window.java中,
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
注意mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this)這句執行,代碼執行在WindowManagerImpl.java中
private WindowManagerImpl(Context context, Window parentWindow) {
mContext = context;
mParentWindow = parentWindow;
}
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}
Window parentWindow即為this傳入的window類型,而我們使用的是Activity的context,所以此處的parentWindow即為Activity 的window。
根據異常log信息顯示,當使用getApplicationContext()會報token null異常,而使用Activity context則正常,先來看下為什么使用Activity context時,tocke 不為null。
窗口創建,都會通過WindowManagerService.java的addWindow()來實現,代碼如下(只貼出與問題相關一小部分代碼):
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
.....
synchronized(mWindowMap) {
......
boolean addToken = false;
WindowToken token = mTokenMap.get(attrs.token);
android.util.Log.e(TAG_WM, "wjx---windowmanagerservice-----attrs.token:" + attrs.token);
AppWindowToken atoken = null;
boolean addToastWindowRequiresToken = false;
if (token == null) {
android.util.Log.e(TAG_WM, "wjx-----token == null-----");
if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
........
token = new WindowToken(this, attrs.token, -1, false);
addToken = true;
} else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
atoken = token.appWindowToken;
android.util.Log.e(TAG_WM,"wjx---------addWindow-------atoken:" + atoken.toString());
if (atoken == null) {
Slog.w(TAG_WM, "Attempted to add window with non-application token "
+ token + ". Aborting.");
return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
} else if (atoken.removed) {
Slog.w(TAG_WM, "Attempted to add window with exiting application token "
+ token + ". Aborting.");
return WindowManagerGlobal.ADD_APP_EXITING;
}
..........
mPolicy.adjustWindowParamsLw(win.mAttrs);
win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));
.....
}
根據如上代碼邏輯可知,當atoken = token.appWindowToken為null時,就報出了文章中的token=null的異常。
- 使用Activity context時,appWindowToken即為Activity的appWindowToken,在Activity啟動的時候,WindowManagerService就調用了addAppToken(),此函數會執行mTokenMap.put(token.asBinder(), atoken)操作,會將appWindowToken存儲到一個HashMap mTokenMap中。所以不會報錯
- 使用getApplicationContext()時,appWindowToken為null,就導致了上述異常問題