Dialog 窗口添加机制

代码示例

首先举两个例子:
例子1

@OnClick(R.id.but)
    void onClick() {
        Log.d("LiaBin", "activity window token:" + this.getWindow().getAttributes().token);

        Dialog dialog = new ProgressDialog(this);
        dialog.show();
        Log.d("LiaBin", "dialog window token:" + dialog.getWindow().getAttributes().token);
    }

输出结果:
11-21 03:24:38.038 2040-2040/lbb.demo.first D/LiaBin: activity window token:android.os.BinderProxy@18421fac
11-21 03:24:38.054 2040-2040/lbb.demo.first D/LiaBin: dialog window token:null

例子2

@OnClick(R.id.but)
    void onClick() {
        Dialog dialog = new ProgressDialog(getApplicationContext());
        dialog.show();
    }

例子3

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();
    }
}

输出结果都是:
E/AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window – token null is not for an application
E/AndroidRuntime: at android.view.ViewRootImpl.setView(ViewRootImpl.java:566)
E/AndroidRuntime: at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:282)
E/AndroidRuntime: at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
E/AndroidRuntime: at android.app.Dialog.show(Dialog.java:298)

为什么会出现以上两种输出结果,看以下分析。

Dialog源码分析

Dialog是一系列XXXDialog的基类,我们可以new任意Dialog或者通过Activity提供的onCreateDialog(……)、onPrepareDialog(……)和showDialog(……)等方法来管理我们的Dialog,但是究其实质都是来源于Dialog基类,所以我们对于各种XXXDialog来说只用分析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中。

总结如下:

  1. dialog使用有自己的window,不同于activity的window
  2. dialog的mWindowManager变量其实就是activity对象的mWindowManager变量,此时注意因为window通过setWindowManager方法也会复制自己的mWindowManager,但这个mWindowManager是通过createLocalWindowManager返回的。不同于dialog的mWindowManager变量。不要混淆
  3. activity覆盖了context的getSystemService方法,如果WINDOW_SERVICE,那么返回的是当前activity的mWindowManager对象

至此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 {
        }
    }

mWindow.getAttributes();所以可以看见Dialog的新Window与Activity的Window的type同样都为TYPE_APPLICATION。然后这句话是重点,有木有跟Activity窗口添加的时候很像,没错

mWindowManager.addView(mDecor, l);

Dialog机制大概就这些了,现在来分析一下,上面两个代码示例
第一个问题:

Dialog dialog = new ProgressDialog(this);为什么这样是正常的?

所以此时dialog的mWindowManager变量其实就是activity对象的mWindowManager变量。
还记得吗?在WindowManager.addView实际上执行的是WindowManagerImpl的addView

public final class WindowManagerImpl implements WindowManager {
    //继承自Object的单例类
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Window mParentWindow;
    ......
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        //mParentWindow是上面分析的在Activity中获取WindowManagerImpl实例化时传入的当前Window
        //view是Activity中最顶层的mDecor
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    ......
}

所以此时mParentWindow其实就是Activity的PhoneWindow对象,虽然dialog有自己的PhoneWindow,但是这两者完全是两码事,不要混淆
所以在WindowManagerGlobal.addView方法中调用

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
        if (parentWindow != null) {
            //依据当前Activity的Window调节sub Window的LayoutParams
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        }
        //adjustLayoutParamsForSubWindow方法中
        if (wp.token == null) {
            wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;//把wp的token设置为当前window的mAppToken
        }

调整wp的时候,所以最后wp.token拿到的就是Activity的mToken,就不为null,所以在最后WindowManagerService的addWindow方法中就不会让ViewRootImpl中抛异常了,所以一切OK

第二个问题:

Log.d(“LiaBin”, “dialog window token:” + dialog.getWindow().getAttributes().token); 打印的为什么是null,而不是activity的token

现在就很好理解了,首先dialog.getWindow(),那么获取的就是dialog的PhoneWindow,而Dialog的window的mWindowAttributes的token值初始化就为null

虽然调用了adjustLayoutParamsForSubWindow方法,但是并没有调整Dialog的window的mWindowAttributes的token值,因为以下代码行就把两者关系断了,调整的是另外一个对象

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

第三个问题:

Dialog dialog = new ProgressDialog(getApplicationContext());为什么会抛异常BadTokenException: Unable to add window – token null is not for an application

因为mContext赋值为了getApplicationContext(),那么

//获取WindowManager对象
        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

那么此时的mWindowManager就是全局唯一的mWindowManager了,而不是activity的mWindowManager。可以看上一篇的分析。调用的其实是ContextImpl的getSystemService方法

所以在WindowManagerGlobal.addView方法中parentWindow就为null了,所以就不会去调用adjustLayoutParamsForSubWindow方法了,所以最后params的token就为null了

在最后WindowManagerService的addWindow方法,把param的token取出来一看是null,就return WindowManagerGlobal.ADD_NOT_APP_TOKEN;返回给ViewRootImpl的setView方法
再来看ViewRootImpl的setView方法,片段

case WindowManagerGlobal.ADD_NOT_APP_TOKEN:throw new WindowManager.BadTokenException(“Unable to add window – token ” + attrs.token + ” is not for an application”);

所以最后抛BadTokenException异常啦

第四个问题:

在服务中调用Dialog dialog = new ProgressDialog(this);为什么要会抛异常

因为service中并没有跟activity做同样的处理,调用的其实是ContextImpl的getSystemService方法,所以此时的mWindowManager就是全局唯一的mWindowManager了,

另外一种情况:

在Activity中使用Dialog的时候,为什么有时候会报错“Unable to add window – token is not valid; is your activity running?”?这种情况一般发生在什么时候?一般发生在Activity进入后台,Dialog没有主动Dismiss掉,然后从后台再次进入App的时候。

PopWindow 窗口添加机制

public class PopupWindow {
    ......
    //我们只分析最常用的一种构造函数
    public PopupWindow(View contentView, int width, int height, boolean focusable) {
        if (contentView != null) {
            //获取mContext,contentView实质是View,View的mContext都是构造函数传入的,View又层级传递,所以最终这个mContext实质是Activity!!!很重要
            mContext = contentView.getContext();
            //获取Activity的getSystemService的WindowManager
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //进行一些Window类的成员变量初始化赋值操作
        setContentView(contentView);
        setWidth(width);
        setHeight(height);
        setFocusable(focusable);
    }
    ......
}

其中注意,view创建的时候都会把一个context参数传递进去,context就是当前的activity了,所以其实contentView.getContext();返回的是该Activity,所以mWindowManager共享当前Activity的mWindowManager变量。同时因为popupwindow构造函数的参数是view,并不是context,所以并不用担心getApplicationContext造成的问题

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        ......
        //anchor是Activity中PopWindow准备依附的View,这个View的token实质也是Activity的Window中的token,也即Activity的token
        //第一步   初始化WindowManager.LayoutParams
        WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
        //第二步
        preparePopup(p);
        ......
        //第三步
        invokePopup(p);
    }
    private void preparePopup(WindowManager.LayoutParams p) {
        ......
        //有无设置PopWindow的background区别
        if (mBackground != null) {
            ......
            //如果有背景则创建一个PopupViewContainer对象的ViewGroup
            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            //把背景设置给PopupViewContainer的ViewGroup
            popupViewContainer.setBackground(mBackground);
            //把我们构造函数传入的View添加到这个ViewGroup
            popupViewContainer.addView(mContentView, listParams);
            //返回这个ViewGroup
            mPopupView = popupViewContainer;
        } else {
            //如果没有通过PopWindow的setBackgroundDrawable设置背景则直接赋值当前传入的View为PopWindow的View
            mPopupView = mContentView;
        }
        ......
    }
    private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }
        mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
        setLayoutDirectionFromAnchor();
        mWindowManager.addView(mPopupView, p);
    }

可以看见preparePopup方法的作用就是判断设置View,如果有背景则会在传入的contentView外面包一层PopupViewContainer(实质是一个重写了事件处理的FrameLayout)之后作为mPopupView,如果没有背景则直接用contentView作为mPopupView

PopupViewContainer是一个PopWindow的内部私有类,它继承了FrameLayout,在其中重写了Key和Touch事件的分发处理逻辑。同时查阅PopupView可以发现,PopupView类自身没有重写Key和Touch事件的处理,所以如果没有将传入的View对象放入封装的ViewGroup中,则点击Back键或者PopWindow以外的区域PopWindow是不会消失的(其实PopWindow中没有向Activity及Dialog一样new新的Window,所以不会有新的callback设置,也就没法处理事件消费了)。

  1. 与Dialog不同的地方是没有新new Window而已(也就没法设置callback,无法消费事件,也就是前面说的PopupWindow弹出后可以继续与依赖的Activity进行交互的原因)。
  2. 如果设置了PopupWindow的background,则点击Back键或者点击PopupWindow以外的区域时PopupWindow就会dismiss;如果不设置PopupWindow的background,则点击Back键或者点击PopupWindow以外的区域PopupWindow不会消失。

另一方面,如果需要全屏的popupwindow,那么可以使用一下代码

view.showAtLocation(mActivity.getWindow().getDecorView(), Gravity.CENTER, 0, 0);

getWindow().getDecorView()就是获取顶层的DecorView

Toast 窗口添加机制

我们常用的Toast窗口其实和前面分析的Activity、Dialog、PopWindow都是不同的,因为它和输入法、墙纸类似,都是系统窗口。

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        //new一个Toast对象
        Toast result = new Toast(context);
        //获取前面有篇文章分析的LayoutInflater
        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        //加载解析Toast的布局,实质transient_notification.xml是一个LinearLayout中套了一个@android:id/message的TextView而已
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        //取出布局中的TextView
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        //把我们的文字设置到TextView上
        tv.setText(text);
        //设置一些属性
        result.mNextView = v;
        result.mDuration = duration;
        //返回新建的Toast
        return result;
    }
    public void show() {
        ......
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            //把TN对象和一些参数传递到远程NotificationManagerService中去
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

这里使用了IBinder机制,其实是通过远程NotificationManagerService服务来管理toast的

private static class TN extends ITransientNotification.Stub {
        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
        params.type = WindowManager.LayoutParams.TYPE_TOAST;
        ......
        //仅仅是实例化了一个Handler,非常重要!!!!!!!!
        final Handler mHandler = new Handler(); 
        ......
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don't do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };
        ......
        //实现了AIDL的show与hide方法
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }
        ......
    }

此时说明toast的type是TYPE_TOAST,这里直接new了一个handler,所以如果在子线程中直接显示一个taost,就会报异常,除非在子线程中调用Looper的prepare和looper方法,才可以在线程中显示toast。接下来重点分析handleShow方法

public void handleShow() {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                // remove the old view if necessary
                //如果有必要就通过WindowManager的remove删掉旧的
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                //通过得到的context(一般是ContextImpl的context)获取WindowManager对象(上一篇文章分析的单例的WindowManager)
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ......
                //在把Toast的View添加之前发现Toast的View已经被添加过(有partent)则删掉
                if (mView.getParent() != null) {
                    ......
                    mWM.removeView(mView);
                }
                ......
                //把Toast的View添加到窗口,其中mParams.type在构造函数中赋值为TYPE_TOAST!!!!!!特别重要
                mWM.addView(mView, mParams);
                ......
            }
        }

mWM此时是全局单例的WindowManager,调用的是ContextImpl的getSystemService方法获取

最后总结一下:

  1. 通过分析TN类的handler可以发现,如果想在非UI线程使用Toast需要自行声明Looper,否则运行会抛出Looper相关的异常;UI线程不需要,因为系统已经帮忙声明。
  2. 在使用Toast时context参数尽量使用getApplicationContext(),可以有效的防止静态引用导致的内存泄漏。 因为首先toast构造函数中拿到了toast,所以如果在当前activity中弹出一个toast,然后finish掉该toast,toast并不依赖activity,是系统级的窗口,当然也不会随着activity的finish就消失,只是随着设置时间的到来而消失,所以如果此时设置toast显示的时间足够长,那么因为toast持有该activity的引用,那么该activty就一直不能被回收,一直到toast消失,造成内存泄漏,所以最好使用getApplicationContext()
  3. 有时候我们会发现Toast弹出过多就会延迟显示,因为上面源码分析可以看见Toast.makeText是一个静态工厂方法,每次调用这个方法都会产生一个新的Toast对象,当我们在这个新new的对象上调用show方法就会使这个对象加入到NotificationManagerService管理的mToastQueue消息显示队列里排队等候显示;所以如果我们不每次都产生一个新的Toast对象(使用单例来处理)就不需要排队,也就能及时更新了。
  4. Toast的显示交由远程的NotificationManagerService管理是因为Toast是每个应用程序都会弹出的,而且位置和UI风格都差不多,所以如果我们不统一管理就会出现覆盖叠加现象,同时导致不好控制,所以Google把Toast设计成为了系统级的窗口类型,由NotificationManagerService统一队列管理。