Android应用Activity、Dialog、PopWindow、Toast窗口添加机制及源码分析 《三》-Dialog

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 转载烦请注明出处,尊重劳动成果】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值