参考文章:
为什么Dialog不能用Application的Context
使用Dialog时需要注意的问题
创建Dialog所需的上下文为什么必须是Activity?
demo
public class Activity {
public void method() {
DialogTest dialogTest = new DialogTest(getApplicationContext());
dialogTest.show();
}
}
public class DialogTest extends Dialog {
DialogTest(@NonNull Context context) {
super(context);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_test);
}
}
- 上面代码会GG, 错误提示如下:
一、Dialog展示前的准备工作
1.1 Dialog构造函数
public Dialog(@NonNull Context context) {
this(context, 0, true);
}
Dialog(Context context, int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
// context被ContextThemeWrapper持有, 这里根据名字大致可以猜出来是利用了装饰者模式,
// 通过需要mContext去访问实际的context.
mContext = new ContextThemeWrapper(context, themeResId);
} else {...}
// 1.context有两种类型, 一种是instanceof Activity, 一种是instanceof Application.
// 2.针对这两种类型, 获取的mWindowManager肯定是在细节上有所区别的, 根据对下文的分析可知, 这个
// 区别决定了后续Dialog展示时的校验工作.
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
// 查看源码这里第二个参数对应的是IBinder的Token, 但是这里传入了null, 而后续又需要使用这个token.
w.setWindowManager(mWindowManager, null, null);
}
小节:
有两点需要注意一下:
- 1、传入的Context有两个类型, Application与Activity类型, 这两种类型对返回的mWindowManager有影响.
- 2、Window.setWindowManager时第二个参数IBinder token传入的是null, 但是在下文中Token又是从何而来?
- 3、PhoneWindow.mAppToken为null, PhoneWindow.mAppToken对后续Dialog的显示有和影响?
1.2 mWindowManager的获取
1.2.1 context instanceof Application
public class ContextImpl {
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
}
1.2.2 context instanceof Activity
@Override
public Object getSystemService(@ServiceName @NonNull String name) {
if (WINDOW_SERVICE.equals(name)) {
// 返回Activity对应的WindowManager.
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
小节:
前者返回Application对应的WindowManager, 后者返回Activity对应的WindowManager, 而在Activity.attach中知道, WindowManager持有了Activity的Token.
这两个类型的WindowManagerService之间有什么区别???
1.3 PhoneWindow的初始化
private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
小节:
初始化PhoneWindow的同时将与PhoneWindow1:1对应的LayoutParams.type设置为TYPE_APPLICATION类型.
但是PhoneWindow内部的mAppToken为null.
关于窗口类型:
- 1、应用窗口: 该窗口对应一个Activity.
- 2、子窗口:该窗口必须有一个父窗口, 父窗口可以是一个应用类型窗口, 也可以是任何其他类型的窗口.
- 3、系统窗口: 系统窗口不需要对应任何Activity, 也不需要父窗口.
1.4 PhoneWindow与WindowManager的关系
1.4.1 PhoneWindow. setWindowManager
public void setWindowManager(WindowManager wm, IBinder appToken, String appName) {
setWindowManager(wm, appToken, appName, false);
}
// 1.PhoneWindow.mAppToken默认为null.
// 2.通过wm创建一个与PhoneWindow对应的WindowManager, 他这里为什么要创建而不仅仅是使用Activity的WindowManager?
// 目前来看可能是因为Activity的生命周期长于Dialog的缘故?
public void setWindowManager(WindowManager wm, IBinder appToken, String appName, boolean hardwareAccelerated) {
mAppToken = appToken;
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
1.4.2 WindowManagerImpl.createLocalWindowManager
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}
// parentWindow指向PhoneWindow.
private WindowManagerImpl(Context context, Window parentWindow) {
mContext = context;
mParentWindow = parentWindow;
}
小节:
执行完成以后, PhoneWindow.mAppToken = null, 但是PhoneWindow.mWindowAttributes != null, 但是这些与WindowManagerImpl有什么关联呢???
Context类型 | 关系 | 实际指向 |
---|---|---|
Activity类型 | Dialog.mWindow | new PhoneWIndow |
App类型 | Dialog.mWindow | new PhoneWindow |
Activity类型 | Dialog.mWindowManager | Activity.mWindowManager |
App类型 | Dialog.mWindowManager | new WindowManager |
Activity类型 | Dialog.mWindowManager.mWindow | Activity.mWindow |
App类型 | Dialog.mWindowManager.mWindow | null |
Activity类型 | Dialog.mWindow.mWindowManager | new WindowManagerImpl |
App类型 | Dialog.mWindow.mWindowManager | new WindowManagerImpl |
二、Dialog的显示
2.1 Dialog.show
public void show() {
...
mDecor = mWindow.getDecorView();
WindowManager.LayoutParams l = mWindow.getAttributes();
// 一定要注意这里的mWindowManager是Dialog的mWindowManager, 而不是PhoneWindow的mWindowManager.
mWindowManager.addView(mDecor, l);
mShowing = true;
...
}
小节:
一定要注意这里的mWindowManager与PhoneWindow.mWindowManager区分开来.
2.2 WindowManagerImpl.addView
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// 1.针对mParentWindow有两种情况:
// (1)如果Context是Activity类型, 则mParentWindow指向Activity.mWindow.
// (2)如果Context是App类型, 则mParentWindow为null.
// 2.params指向Dialog.mWindow.params, type为TYPE_APPLICATION, 同时token=null.
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
2.3 WindowManagerImpl.applyDefaultToken
private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) {
// Only use the default token if we don't have a parent window.
if (mDefaultToken != null && mParentWindow == null) {
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
// Only use the default token if we don't already have a token.
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (wparams.token == null) {
wparams.token = mDefaultToken;
}
}
}
2.4 WindowManagerGlobal.addView
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
// 如果Context为App类型, parentWindow=null, 如果Context为Activity类型, parentWindow指向
// Activity.mWindow.
if (parentWindow != null) {
// 所以如果Context为Activity类型, 进入到这里.
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
// 执行到这里, 再次强调一下;
// (1)如果Context为App类型, 则wparams.token=null, parentWindow=null.
// (2)如果Context为Activity类型, 则wparams.token=mAppToken, parentWindow指向Activity.mWindow.
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
// 将ViewRootImpl、LayoutParams、Decor产生关联.
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
2.5 PhoneWindow.adjustLayoutParamsForSubWindow
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
CharSequence curTitle = wp.getTitle();
// 窗口类型为子窗口
if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
if (wp.token == null) {
View decor = peekDecorView();
if (decor != null) {
wp.token = decor.getWindowToken();
}
}
// 窗口类型为系统窗口
} else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
// We don't set the app token to this system window because the life cycles should be
// independent. If an app creates a system window and then the app goes to the stopped
// state, the system window should not be affected (can still show and receive input
// events).
if (curTitle == null || curTitle.length() == 0) {
final StringBuilder title = new StringBuilder(32);
title.append("Sys").append(wp.type);
if (mAppName != null) {
title.append(":").append(mAppName);
}
wp.setTitle(title);
}
// 窗口类型为应用窗口
} else {
// 结合上文其实可知, wp.token默认为null, 如果Context为Activity类型, 则会进入到这里对
// wp.token进行赋值, mAppToken在何处进行的赋值? 但是有一个已知条件Context为Activity时,
// PhoneWindow实际指向Activity.mWindow, 所以到这里先看一下Activity.attach方法<2.5.1>
// 所以此时将Activity.mToken赋值给PhoneWindow.mAppToken, 然后在赋值给wp.token.
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
if ((curTitle == null || curTitle.length() == 0) && mAppName != null) {
wp.setTitle(mAppName);
}
}
if (wp.packageName == null) {
wp.packageName = mContext.getPackageName();
}
if (mHardwareAccelerated ||
(mWindowAttributes.flags & FLAG_HARDWARE_ACCELERATED) != 0) {
wp.flags |= FLAG_HARDWARE_ACCELERATED;
}
}
2.5.1 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,
Window window, ActivityConfigCallback activityConfigCallback) {
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}
- 将Activity.mToken赋值给mWindow.mAppToken, Activity.mToken是在system_server进程中通过AMS创建然后序列化传给了App进程的目标Activity.
2.6 ViewRootImpl.setView
// 1.Context是Application类型: attrs.token=null.
// 2.Context是Activity类型: attrs.token=Activity.mToken.
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
mView = view;
// 将attrs中的属性拷贝给mWindowAttributes.
mWindowAttributes.copyFrom(attrs);
attrs = mWindowAttributes;
// mWindowSession指向WindowManagerService中创建的Session.
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
...
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?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
}
}
}
}
2.7 Session.addToDisplay
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
2.8 WindowManagerService.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) {
int[] appOp = new int[1];
// 进行权限的检查, 如果窗口级别为应用级别或者子窗口级别, 不用进行权限校验, 如果是指定的系统级别, 则
// 需要进行权限, 然后判断是否开启了对应的权限校验功能.
int res = mPolicy.checkAddPermission(attrs, appOp);
// 权限校验除了问题, 终止接下来的流程.
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
boolean reportNewConfig = false;
WindowState parentWindow = null;
long origId;
final int callingUid = Binder.getCallingUid();
final int type = attrs.type;
synchronized(mWindowMap) {
// 如果窗口级别为子窗口级别.
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
// 根据attrs.token获取WindowState.
parentWindow = windowForClientLocked(null, attrs.token, false);
if (parentWindow == null) {
Slog.w(TAG_WM, "Attempted to add window with token that is not a window: "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
&& parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
Slog.w(TAG_WM, "Attempted to add window with token that is a sub-window: "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
}
//
if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
Slog.w(TAG_WM, "Attempted to add private presentation window to a non-private display. Aborting.");
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
}
AppWindowToken atoken = null;
// 假设窗口级别为非子窗口级别, hasParent = false.
final boolean hasParent = parentWindow != null;
// 通过attrs.token获取对应的WindowToken:
// 如果Context为Application类型, attrs.token = null, token = null.
// 如果Context为Activity类型, attrs.token = Activity.mToken, token != null.
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
// If this is a child window, we want to apply the same type checking rules as the
// parent window type.
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
boolean addToastWindowRequiresToken = false;
// 如果Context为Application类型时, token == null.
// 如果Context为Activity类型时, token != null.
if (token == null) {
// 平时遇到的问题就是在这里, 如果type类型为TYPE_APPLICATION类型且token = null, 抛出异常.
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW)
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
if (rootType == TYPE_INPUT_METHOD)
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
if (rootType == TYPE_VOICE_INTERACTION)
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
if (rootType == TYPE_WALLPAPER)
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
if (rootType == TYPE_DREAM)
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
if (rootType == TYPE_QS_DIALOG)
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
if (rootType == TYPE_ACCESSIBILITY_OVERLAY)
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
if (type == TYPE_TOAST)
if (doesAddToastWindowRequireToken(attrs.packageName, callingUid, parentWindow))
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
token = new WindowToken(this, binder, type, false, displayContent,
session.mCanAddInternalSystemWindow);
// token != null的情况且type为应用级别.
} else if (...) {...}
return res;
}
}
4.10 PhoneWindowManager.checkAddPermission
// 如果Context为Application类型, attrs.token = null.
// 如果Context为Activity类型, attrs.token = Activity.mToken.
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
// 获取对应的窗口类型.
int type = attrs.type;
// 窗口级别: 应用级别、子窗口级别.
if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
return WindowManagerGlobal.ADD_OKAY;
}
String permission = null;
// 如果是以下几个类型, 需要申请相应的权限.
switch (type) {
case TYPE_TOAST:
outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW;
break;
case TYPE_DREAM:
case TYPE_INPUT_METHOD:
case TYPE_WALLPAPER:
case TYPE_PRIVATE_PRESENTATION:
case TYPE_VOICE_INTERACTION:
case TYPE_ACCESSIBILITY_OVERLAY:
break;
case TYPE_PHONE:
case TYPE_PRIORITY_PHONE:
case TYPE_SYSTEM_ALERT:
case TYPE_SYSTEM_ERROR:
case TYPE_SYSTEM_OVERLAY:
permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
break;
default:
permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
}
if (permission != null) {
if (permission == android.Manifest.permission.SYSTEM_ALERT_WINDOW) {
final int callingUid = Binder.getCallingUid();
if (callingUid == Process.SYSTEM_UID) {
return WindowManagerGlobal.ADD_OKAY;
}
final int mode = mAppOpsManager.checkOp(outAppOp[0], callingUid, attrs.packageName);
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
case AppOpsManager.MODE_IGNORED:
return WindowManagerGlobal.ADD_OKAY;
case AppOpsManager.MODE_ERRORED:
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
default:
if (mContext.checkCallingPermission(permission) != PackageManager.PERMISSION_GRANTED) {
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
} else {
return WindowManagerGlobal.ADD_OKAY;
}
}
}
if (mContext.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
}
}
return WindowManagerGlobal.ADD_OKAY;
}
4.11 PhoneWindowManager.prepareAddWindowLw:
@Override
public int prepareAddWindowLw(WindowState win, WindowManager.LayoutParams attrs) {
switch (attrs.type) {
case TYPE_STATUS_BAR:...break;
case TYPE_NAVIGATION_BAR:...break;
case TYPE_NAVIGATION_BAR_PANEL:
case TYPE_STATUS_BAR_PANEL:
case TYPE_STATUS_BAR_SUB_PANEL:
case TYPE_VOICE_INTERACTION_STARTING:...break;
case TYPE_KEYGUARD_SCRIM:...break;
}
return WindowManagerGlobal.ADD_OKAY;
}
五、如何避免在Dialog中使用ApplicationContext抛异常的问题:
5.1 改变Dialog窗口的级别类型:
在show方法执行之前;
public class Window {
public void setType(int type) {
final WindowManager.LayoutParams attrs = getAttributes();
attrs.type = type;
dispatchWindowAttributesChanged(attrs);
}
}
DialogTest(@NonNull Context context) {
super(context);
getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
}
5.2 申请权限:
或者直接将type类型改为Toast类型;
ublic class DialogTest extends Dialog {
DialogTest(@NonNull Context context) {
super(context);
getWindow().setType(WindowManager.LayoutParams.TYPE_TOAST);
}
}