Android应用Dialog窗口添加显示机制源码
3-1 Dialog窗口源码分析
写过APP都知道,Dialog是一系列XXXDialog的基类,我们可以new任意Dialog或者通过Activity提供的onCreateDialog(……)、onPrepareDialog(……)和showDialog(……)等方法来管理我们的Dialog,但是究其实质都是来源于Dialog基类,所以我们对于各种XXXDialog来说只用分析Dialog的窗口加载就可以了。
如下从Dialog的构造函数开始分析:
public class Dialog implements DialogInterface, Window.Callback,
KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
......
public Dialog(Context context) {
this(context, 0, true);
}
//构造函数最终都调运了这个默认的构造函数
Dialog(Context context, int theme, boolean createContextThemeWrapper) {
//默认构造函数的createContextThemeWrapper为true
if (createContextThemeWrapper) {
//默认构造函数的theme为0
if (theme == 0) {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme,
outValue, true);
theme = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, theme);
} else {
mContext = context;
}
//mContext已经从外部传入的context对象获得值(一般是个Activity)!!!非常重要,先记住!!!
//获取WindowManager对象
mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
//为Dialog创建新的Window
Window w = PolicyManager.makeNewWindow(mContext);
mWindow = w;
//Dialog能够接受到按键事件的原因
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
//关联WindowManager与新Window,特别注意第二个参数token为null,也就是说Dialog没有自己的token
//一个Window属于Dialog的话,那么该Window的mAppToken对象是null
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
......
}
可以看到,Dialog构造函数首先把外部传入的参数context对象赋值给了当前类的成员(我们的Dialog一般都是在Activity中启动的,所以这个context一般是个Activity),然后调用context.getSystemService(Context.WINDOW_SERVICE)获取WindowManager,这个WindowManager是哪来的呢?先按照上面说的context一般是个Activity来看待,可以发现这句实质就是Activity的getSystemService方法,我们看下源码,如下:
@Override
public Object getSystemService(@ServiceName @NonNull String name) {
if (getBaseContext() == null) {
throw new IllegalStateException(
"System services not available to Activities before onCreate()");
}
//我们Dialog中获得的WindowManager对象就是这个分支
if (WINDOW_SERVICE.equals(name)) {
//Activity的WindowManager
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
看见没有,Dialog中的WindowManager成员实质和Activity里面是一样的,也就是共用了一个WindowManager。
回到Dialog的构造函数继续分析,在得到了WindowManager之后,程序又新建了一个Window对象(类型是PhoneWindow类型,和Activity的Window新建过程类似);接着通过w.setCallback(this)设置Dialog为当前window的回调接口,这样Dialog就能够接收事件处理了;接着把从Activity拿到的WindowManager对象关联到新创建的Window中。
至此Dialog的创建过程Window处理已经完毕,很简单,所以接下来我们继续看看Dialog的show与cancel方法,如下:
public void show() {
......
if (!mCreated) {
//回调Dialog的onCreate方法
dispatchOnCreate(null);
}
//回调Dialog的onStart方法
onStart();
//类似于Activity,获取当前新Window的DecorView对象,所以有一种自定义Dialog布局的方式就是重写Dialog的onCreate方法,使用setContentView传入布局,就像前面文章分析Activity类似
mDecor = mWindow.getDecorView();
......
//获取新Window的WindowManager.LayoutParams参数,和上面分析的Activity一样type为TYPE_APPLICATION
WindowManager.LayoutParams l = mWindow.getAttributes();
......
try {
//把一个View添加到Activity共用的windowManager里面去
mWindowManager.addView(mDecor, l);
......
} finally {
}
}
可以看见Dialog的新Window与Activity的Window的type同样都为TYPE_APPLICATION,上面介绍WindowManager.LayoutParams时TYPE_APPLICATION的注释明确说过,普通应用程序窗口TYPE_APPLICATION的token必须设置为Activity的token来指定窗口属于谁。所以可以看见,既然Dialog和Activity共享同一个WindowManager(也就是上面分析的WindowManagerImpl),而WindowManagerImpl里面有个Window类型的mParentWindow变量,这个变量在Activity的attach中创建WindowManagerImpl时传入的为当前Activity的Window,而当前Activity的Window里面的mAppToken值又为当前Activity的token,所以Activity与Dialog共享了同一个mAppToken值,只是Dialog和Activity的Window对象不同。
3-2 Dialog窗口加载总结
通过上面分析Dialog的窗口加载原理,我们总结如下图:
从图中可以看出,Activity和Dialog共用了一个Token对象,Dialog必须依赖于Activity而显示(通过别的context搞完之后token都为null,最终会在ViewRootImpl的setView方法中加载时因为token为null抛出异常),所以Dialog的Context传入参数一般是一个存在的Activity,如果Dialog弹出来之前Activity已经被销毁了,则这个Dialog在弹出的时候就会抛出异常,因为token不可用了。在Dialog的构造函数中我们关联了新Window的callback事件监听处理,所以当Dialog显示时Activity无法消费当前的事件。
到此Dialog的窗口加载机制就分析完毕了,接下来我们说说应用开发中常见的一个诡异问题。
3-3 从Dialog窗口加载分析引出的应用开发问题
有了上面的分析我们接下来看下平时开发App初学者容易犯的几个错误。
实现在一个Activity中显示一个Dialog,如下代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
//重点关注构造函数的参数,创建一个Dialog然后显示出来
Dialog dialog = new ProgressDialog(this);
dialog.setTitle("TestDialogContext");
dialog.show();
}
}
分析:使用了Activity为context,也即和Activity共用token,符合上面的分析,所以不会报错,正常执行。
实现在一个Activity中显示一个Dialog,如下代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
//重点关注构造函数的参数,创建一个Dialog然后显示出来
Dialog dialog = new ProgressDialog(getApplicationContext());
dialog.setTitle("TestDialogContext");
dialog.show();
}
}
实现在一个Service中显示一个Dialog,如下代码:
public class WindowService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
//重点关注构造函数的参数
Dialog dialog = new ProgressDialog(this);
dialog.setTitle("TestDialogContext");
dialog.show();
}
}
分析:传入的Context是一个Service,类似上面传入ApplicationContext一样的后果,一样的原因,抛出如下异常:
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
at android.view.ViewRootImpl.setView(ViewRootImpl.java:566)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:272)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
at android.app.Dialog.show(Dialog.java:298)
至此通过我们平时使用最多的Dialog也验证了Dialog成功显示的必要条件,同时也让大家避免了再次使用Dialog不当出现异常的情况,或者出现类似异常后知道真实的背后原因是什么的问题。
可以看见,Dialog的实质无非也是使用WindowManager的addView、updateViewLayout、removeView进行一些操作展示。
【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重劳动成果】