Android窗口设计之Dialog、PopupWindow、系统窗口的实现

    窗口设计之Dialog、PopupWindow、系统窗口的实现


Android应用程序窗口设计系列博客:

Android应用程序窗口设计之Window及WindowManager的创建
Android应用程序窗口设计之setContentView布局加载的实现
普法Android的Token前世今生以及在APP,AMS,WMS之间传递
Android应用程序窗口设计之窗口的添加
Android应用程序窗口设计之建立与WMS服务之间的通信过程
Android窗口设计之Dialog、PopupWindow、系统窗口的实现
Android应用程序建立与AMS服务之间的通信过程



本篇博客编写思路总结和关键点说明:
在这里插入图片描述

为了更加方便的读者阅读博客,通过导读思维图的形式将本博客的关键点列举出来,从而方便读者取舍和阅读!



引言

  通过前面系列博客Android应用程序窗口设计实现的不懈努力,我们从多个维度以Android源码为理论依据完成了Android应用程序窗口设计的如下知识点的分析和探讨:

  • Android应用程序窗口的创建以及对应窗口管理器的创建
  • Android应用程序窗口加载对应的布局
  • Android应用程序窗口的添加处理流程

有了上述知识的加持,读者应该对Android应用程序的窗口设计和实现有了一定程度的认识了(精通吗,那肯定是远远不够的,只能说路漫漫而修远吾将上下而求索!)!但是我们知道Android的窗口并不单单是只有应用程序的窗口类型,还有子窗口类型和系统窗口类型等几种,正好有读者私下询问关于此方面的知识并且我正好也有意想完善关于Android窗口实现的系列博客,所以本篇我们将Android窗口中的Dialog、PopupWindow、系统窗口等的实现也纳入进来,来个一锅端(必须来点狠的才行)!

但是,这里有一个前提就是关于Android窗口设计之Dialog、PopupWindow、系统窗口的实现博客的阅读前,强烈建议读者一定要在将Android应用程序窗口实现系列博客有所理解的前提下,因为本篇博客和前面的知识点联系非常紧密,而且在前面分析过的我也只会简单带过,而不会重复进行了!

注意:本篇的介绍是基于Android 7.xx平台为基础的,其中涉及的代码路径如下:

frameworks/base/core/java/android/view/
	--- WindowManager.java
 	--- View.java
	--- ViewManager.java
	--- ViewRootImpl.java
	--- Window.java
	--- Display.java
	--- WindowManagerImpl.java
	--- WindowManager.java
	--- WindowManagerGlobal.java
	--- IWindowManager.aidl
	--- IWindow.aidl
	--- IWindowSession.aidl

frameworks/base/services/core/java/com/android/server/wm/
	---WindowManagerService.java
	---AppWindowToken.java
	---WindowState.java
	---Session.java
	---WindowToken.java

frameworks/base/core/java/android/app/Dialog.java
frameworks/base/core/java/android/widget/PopupWindow.java



一.WindowManager.LayoutParams窗口管理布局配置文件

  在前面的系列博客中我们多处看到了WindowManager.LayoutParams的靓丽身影了,但是我们都只是简单的带过了,它用一句话来概括总结就是用于向WindowManager窗口管理者描述Window窗口的的管理布局等策略!因为后续博客会重点使用到该类,我们这里先对它重点分析一下。老规矩,先来看看它的类图,如下:


在这里插入图片描述

我们这里可以看到WindowManager.LayoutParams继承于ViewGroup的内部类LayoutParams,我们这里简单看下ViewGroup.LayoutParams(主要是Android对它功能的描述!),如下:

//[ViewGroup.java]
/*
     * LayoutParams are used by views to tell their parents how they want to be
     * laid out.
*/
public static class LayoutParams {
	...
}

上面Android对它的官方解释是"视图使用LayoutParams来告诉其父布局如何布置它们"!而我们的WindowManager.LayoutParams扩展开来就是告诉WindowManager窗口管理者描述Window窗口的的管理布局等策略(这个地方读者自行慢慢体会)!

好了这里我们从整体上对WindowManager.LayoutParams有了一个认识了,下面我们从它的三个非常重要的参数type,token,flags入手详细分析一下LayoutParams。


1.1 WindowManager.LayoutParams.type表明窗口类型

  如果读者对Android的窗口有一定了解的话,一定知道窗口通常被分为三类,即应用程序窗口,系统窗口,和子窗口。那么在窗口设计实现中是怎么标识的呢,这里就是通过WindowManager.LayoutParams的参数type来进行标识了。并且上述三种窗口LayoutParams的类中有明确的指明了取值的范围定义,这里我们看看它们分类的依据如下:

  • Android应用程序窗口:它的type取值范围为FIRST_APPLICATION_WINDOW到LAST_APPLICATION_WINDOW之间,它们的典型代表就是Activity和Dialog(不要惊讶,Dialog真的是,真的是)。通常我们的Android应用程序窗口取值为TYPE_APPLICATION!它们在WindowManager.LayoutParams源码中的定义如下(在这里我只会对其中的重点的用我蹩脚的英文进行一下翻译,其它的读者就自行阅读体会吗,这样你才能理解的更深):
//[WindowManager.java]
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
        public static final int FIRST_APPLICATION_WINDOW = 1;

		//此处表示普通应用程序窗口的窗口类型开始
        public static final int TYPE_BASE_APPLICATION   = 1;


		/*
			用来标识普通应用程序窗口(譬如Activity,Dialog),并且和它对应的token必须设置为Activity的token
			从而用来指定窗口属于谁
		*/
        public static final int TYPE_APPLICATION        = 2;

        /**
         * Window type: special application window that is displayed while the
         * application is starting.  Not for use by applications themselves;
         * this is used by the system to display something until the
         * application can show its own windows.
         * In multiuser systems shows on all users' windows.
         */
        public static final int TYPE_APPLICATION_STARTING = 3;

        /**
         * Window type: a variation on TYPE_APPLICATION that ensures the window
         * manager will wait for this window to be drawn before the app is shown.
         * In multiuser systems shows only on the owning user's window.
         */
        public static final int TYPE_DRAWN_APPLICATION = 4;

		//此处表示普通应用程序窗口的窗口类型结束
        public static final int LAST_APPLICATION_WINDOW = 99;
}
  • Android子窗口:它的type取值范围为FIRST_SUB_WINDOW到LAST_SUB_WINDOW之间,这类窗口的存在必须依附于顶层窗口,譬如前面的Android应用程序窗口。它们的典型代表就是PopupWindow弹出式窗口。它们在WindowManager.LayoutParams源码中的定义如下(在这里我只会对其中的重点的用我蹩脚的英文进行一下翻译,其它的读者就自行阅读体会吗,这样你才能理解的更深):
//[WindowManager.java]
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
         /*
         	子窗口类型的开始标识。这些类型窗口的token必须设置为它们所依附连接
         	的窗口, 这些类型的窗口以Z顺序保留在其附加窗口的旁边,并且
            坐标空间是相对于其附加窗口的
         */
        public static final int FIRST_SUB_WINDOW = 1000;

        /**
         * Window type: a panel on top of an application window.  These windows
         * appear on top of their attached window.
         */
         /*
         	窗口类型:应用程序窗口顶部的面板。
         	这些窗户出现在其附加窗口的顶部
         */
        public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;

        /**
         * Window type: window for showing media (such as video).  These windows
         * are displayed behind their attached window.
         */
        public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;

        /**
         * Window type: a sub-panel on top of an application window.  These
         * windows are displayed on top their attached window and any
         * {@link #TYPE_APPLICATION_PANEL} panels.
         */
        public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;

        /** Window type: like {@link #TYPE_APPLICATION_PANEL}, but layout
         * of the window happens as that of a top-level window, <em>not</em>
         * as a child of its container.
         */
        public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;

        /**
         * Window type: window for showing overlays on top of media windows.
         * These windows are displayed between TYPE_APPLICATION_MEDIA and the
         * application window.  They should be translucent to be useful.  This
         * is a big ugly hack so:
         * @hide
         */
        public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW + 4;

        /**
         * Window type: a above sub-panel on top of an application window and it's
         * sub-panel windows. These windows are displayed on top of their attached window
         * and any {@link #TYPE_APPLICATION_SUB_PANEL} panels.
         * @hide
         */
        public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;

		//子窗口类型结束标识
        public static final int LAST_SUB_WINDOW = 1999;
}
  • Android系统窗口:它的type取值范围为FIRST_SYSTEM_WINDOW到LAST_SYSTEM_WINDOW之间,这类窗口可以不依附于任何窗口存在,它们通常不能被普通应用App创建,它们的典型代表就是输入法,系统弹窗,Toast等等。它们在WindowManager.LayoutParams源码中的定义如下(在这里我只会对其中的重点的用我蹩脚的英文进行一下翻译,其它的读者就自行阅读体会吗,这样你才能理解的更深):
//[WindowManager.java]
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
		//用于表示系统窗口的开始,非应用程序创建
        public static final int FIRST_SYSTEM_WINDOW     = 2000;

		public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
		public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
		public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;

		//用于表示系统窗口的结束,非应用程序创建
        public static final int LAST_SYSTEM_WINDOW      = 2999;
}

不知道眼尖的读者有没有发现,WindowManager.LayoutParams里面窗口的type类型值定义是一个递增保留的连续增大数值,从注释可以看出来其实就是窗口的Z-ORDER序列(值越大显示的位置越在上面,你需要将屏幕想成三维坐标模式)。这个也很好理解!


1.2 WindowManager.LayoutParams.token标识窗口

我们先来看看token的定义,如下:

//[WindowManager.java]
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
        /**
         * Identifier for this window.  This will usually be filled in for
         * you.
         */
        public IBinder token = null;
}

这里我们先抛开token在WindowManager.LayoutParams中的功能,先回归本质可以看到它是一个IBinder类型,说到Binder我们先回忆回忆Binder的作用(并且通过实际分析,我们知道此处是一个匿名Binder,如果对它还有不是很熟悉的可以参见Android Binder框架实现之何为匿名/实名Binder),通过前面的博客我们知道Binder有两个比较重要的用途:

  • 一个是拿到Binder代理端后可跨Binder调用实体端的函数接口
  • 另一个作用便是在多个进程中标识同一个对象

并且匿名Binder的上述两个作用是同时存在的,而我们这里的token重点的作用是起到用于标识窗口的作用,并且它是根据窗口类型的不同而不同的,这里我们先对简单总结下(后续在实际分析中会有详细的体现):

  • Android应用程序窗口token:它对应的token值为Activity的启动过程中在AMS服务中构建一个ActivityRecord实例对象来标记Activity过程中创建的Token对应的Binder实体在Android应用程序端的代理端,关于此处传递经历有点坎坷,可以详见博客普法Android的Token前世今生以及在APP,AMS,WMS之间传递

  • Android子窗口token:Android子窗口的token比较特殊,它会被设置成为它依附窗口的的W本地Binder对象(这里大伙现有一个基本轮廓,后续分析的过程中会体现出来)

  • Android系统窗口token:它比较简单,它的token通常为null


1.3 WindowManager.LayoutParams.flags窗口的各种行为选项标志

我们先来看看flags的定义,如下:

//[WindowManager.java]
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
        /**
         * Various behavioral options/flags.  Default is none.
         *
         * @see #FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
         * @see #FLAG_DIM_BEHIND
         * @see #FLAG_NOT_FOCUSABLE
         * @see #FLAG_NOT_TOUCHABLE
         * @see #FLAG_NOT_TOUCH_MODAL
         * @see #FLAG_TOUCHABLE_WHEN_WAKING
         * @see #FLAG_KEEP_SCREEN_ON
         * @see #FLAG_LAYOUT_IN_SCREEN
         * @see #FLAG_LAYOUT_NO_LIMITS
         * @see #FLAG_FULLSCREEN
         * @see #FLAG_FORCE_NOT_FULLSCREEN
         * @see #FLAG_SECURE
         * @see #FLAG_SCALED
         * @see #FLAG_IGNORE_CHEEK_PRESSES
         * @see #FLAG_LAYOUT_INSET_DECOR
         * @see #FLAG_ALT_FOCUSABLE_IM
         * @see #FLAG_WATCH_OUTSIDE_TOUCH
         * @see #FLAG_SHOW_WHEN_LOCKED
         * @see #FLAG_SHOW_WALLPAPER
         * @see #FLAG_TURN_SCREEN_ON
         * @see #FLAG_DISMISS_KEYGUARD
         * @see #FLAG_SPLIT_TOUCH
         * @see #FLAG_HARDWARE_ACCELERATED
         * @see #FLAG_LOCAL_FOCUS_MODE
         * @see #FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
         */
         public int flags;
}

关于flags我想读者应该不默认了,在Android源码的设计中比较常见,这里使用flags来对窗口做一些定制化!这里就不详细的说明了,读者可以自行阅读并实际源码调试了解其中深意!




二.Android应用程序Dialog窗口实现流程分析

  在前面的分析中我们重点突出了Dialog是应用程序窗口的事实,虽然它从Android的归类上来说是应用程序窗口,但是从显示表现上来说偏向于子窗口,必须依附到Activity才能存在,而从性质上来说,仍然是应用窗口,有自己的AppWindowToken。好了其它的不多说了,我们直接从简单实例入手开始Android应用程序Dialog窗口实现流程分析。

Android应用程序Dialog窗口实现流程分析可以和Activity窗口实现流程对比,你会发现其中有很多异曲同工之妙!

在这里插入图片描述


2.1 Android应用程序Dialog窗口简单实例

分析总要有个源头不是,这里我们就以最最简单的Dialog创建实例入手来分析,简单的demo如下:

    private void showDialog() {
    	//注意这里传入的参数是Activity的实例对象
        Dialog mDialog = new Dialog(MainActivity.this);
        //加载Dialog窗口布局
        mDialog.setContentView(R.layout.title);
        mDialog.show();//显示Dialog
    }

其实无论Android对外何种封装出来的XXXDialog类,归根究底它们都是来源于对Dialog基类的再次封装而已,所以我们回归本质从最简单的Dialog开始入手分析Android应用程序窗口的实现。


2.2 Android应用程序Dialog窗口创建源码分析

  对于Android中各种类的实现,通常是从其构造方法开始的,而我们的Dialog也不能免俗!在开始分析前,我们先看下Dialog的类图:

在这里插入图片描述

Dialog中的关键成员变量和Activity何其相似啊,不感慨了我们接着分析源码!

//[Dialog.java]
public class Dialog implements DialogInterface, Window.Callback,
        KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
    ...
	public Dialog(@NonNull Context context) {
		this(context, 0, true);
	}
       public Dialog(@NonNull Context context, @StyleRes int themeResId) {
       	this(context, themeResId, true);
   	}

    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == 0) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            //创建主题ContextThemeWrapper
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }
		
		/*
			注意此处使用的是context而不是mContext,并且context是从Activity传递过来的对应的是Activity
			此处很重要,此处很重要,很重要
		*/
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

		/*
			创建Dialog对应的窗口和管理器这个逻辑和和Activity窗口和对应管理器的创建何其相似啊
		*/
		final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        //注意此时传递的其它两个参数均为null
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }
}

绝大部分的时候我们的Dialog的创建都是在Activity创建的(除开系统Dialog窗口的创建,这个需要归纳到系统窗口的创建流程中去),而我们此处的Dialog也是在Activity的子类中进行创建的,所以我们传入的参数context指向了Activity,好了有了上面的知识铺垫。我们重点看下下面的源码逻辑:

/*
	注意此处使用的是context而不是mContext,并且context是从Activity传递过来的对应的是Activity
	此处很重要,此处很重要,很重要
*/
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE)

还记得我们在博客中Android应用程序窗口设计之建立与WMS服务之间的通信过程的章节一Android应用程序建立和WMS的Binder通信的过程吗,在获取WMS核心服务对外接口类ActivityManager的过程中说到,Activity会重写getSystemService方法,并且对WMS服务对外接口类机型缓存吗!

//[Activity.java]
    @Override
    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)) {
            return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ensureSearchManager();
            return mSearchManager;
        }
        return super.getSystemService(name);
    }

所以我们的Dialog的成员变量mWindowManager和其所在Activity实例的成员变量mWindowManager都是指向同一个对象的,这个点是关键!

对于Activity实例的成员变量mWindowManager创建流程有不清楚的详见博客Android应用程序窗口设计之Window及WindowManager的创建,总之此处的Dialog的mWindowManager和其对应的Activity的mWindowManager指向的是同一个

然后在Dialog的构造方法中就是一些比较常规的操作了,其主要步骤如下:

  • 创建Dialog对应的窗口
  • 将前面获取的mWindowManager设置为Dialog对应窗口的管理器(这里再次强调,此处的mWindowManager指向)

2.3 Android应用程序Dialog窗口布局加载源码分析

  Dialog我们也创建成功了,按照Activity窗口处理流程来说是时候加载布局文件了,而这里Dialog和Activity加载布局文件的方法类似都是setContentView,并且最后都是交由对应窗口PhoneWindow来处理的,如下:

//[Dialog.java[
    public void setContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
    	//此处的mWindow指向了PhoneWindow
        mWindow.setContentView(view, params);
    }

关于PhoneWindow的setContentView方法处理流程,这里就不赘述了,可以参见博客Android应用程序窗口设计之setContentView布局加载的实现一问,总之通过该方法以及一系列调用方法,会创建Dialog窗口对应的DecocrView对象实例,然后将加载的布局文件填充到DecorView中。


2.4 Android应用程序Dialog窗口显示源码分析

  Dialog我们也创建成功了,布局也加载了,万事俱备只欠东风了,就待我们将显示了,而这个就需要借助它的show()方法了,我们看看其具体执行的逻辑如下:

//[Dialog.java]
    public void show() {
        if (!mCreated) {//其实Dialog也是有生命周期的,譬如onCreate()方法
            dispatchOnCreate(null);
        } else {
			...
        }
        onStart();//这里的onStart()方法
        mDecor = mWindow.getDecorView();//前面构建好的Decorview		
		...
        WindowManager.LayoutParams l = mWindow.getAttributes();//窗口布局配置,其type为默认的TYPE_APPLICATION        
		...
        mWindowManager.addView(mDecor, l);//添加窗口,很熟悉的操作啊如果对Activity窗口显示的流程熟悉的
        mShowing = true;
        sendShowMessage();
    }

如果对Android应用程序Activity对应窗口实现熟悉的读者,这里得来基本不花费功夫了!我们看看Dialog在show()方法中执行了那些逻辑:

  • 调用Dialog生命周期的onCreate()和onStart()方法,不要惊讶Dialog也是有生命周期的,所以我们可以自己继承Dialog,然后重写它的生命周期方法
  • 获取前面已经填充好的布局文件的DecorView对象实例
  • 获取Diloag对应窗口的窗口布局配置文件
  • 通过窗口管理器mWindowManager的方法addView()开始窗口添加流程(注意它传入的两个参数)

我们重点看下mWindowManager的addView()方法,注意此时的mWindowManager和Activiyt的mWindowManager都是指向了WindowManagerImpl的同一个实例,该对象的addView方法实际最终调用到到是单例对象WindowManagerGlobal的addView方法(前文有提到),其关系如下:

在这里插入图片描述

好了,前面介绍了这么多是时候直接开撸源码了,翠花上源码addView如下:

//[WindowManagerImpl.java]
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        android.util.SeempLog.record_vg_layout(383,params);
        applyDefaultToken(params);//设置默认Token
        //这里我们需要注意的是此时的mParentWindow指向了Dialog所在Activity对应的窗口PhoneWIndow,因为此时的WindowManagerImpl和Activity的WindowManagerImpl是同一个实例,这个地方很重要
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

接着往下看

//[WindowManagerGlobal.java]
    public void addView(View view, //此处的view指向DecorView
    					ViewGroup.LayoutParams params,//这里的params为Window对应的默认WindowManager.LayoutParams实例对象mWindowAttributes
            			Display display, //这里的Display具体指向表示物理显示设备有关的逻辑显示的大小(譬如尺寸分辨率)和密度的信息
            			Window parentWindow) //这里的parentWindow指向了Activity对应的窗口实例对象PhoneWindow,这个地方很重要
   	{
        //参数有效性的检查
        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;
		/*
			注意此时的parentWindow为不为null,
			指向的是Activity对应的PhoneWindow窗口,这个在窗口的创建过程中已经有说明

			根据窗口类型调整LayoutParams的相关参数
			
		*/
		if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
			...
        }

        ViewRootImpl root;
        View panelParentView = null;        
        synchronized (mLock) {
            if (mSystemPropertyUpdater == null) {//监听Property的变化
				...
            }

			/*
				从mViews中查找是否已经有添加过同样的View
			*/
            int index = findViewLocked(view, false);
            if (index >= 0) {
				...
            }

			...
			/*
				构建ViewRootImpl
				它很重要,它很重要,它很重要,它是WindowManagerGlobal最最得力的干将没有之一
				基本包揽了WindowManagerGlobal绝大部分工作
			*/
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);//这三者之间是一一对应的情况
            mRoots.add(root);
            mParams.add(wparams);
        }

        try {
        	/*
        		将DecorView添加到ViewRootImpl中,最后添加到WMS中
        		注意这里传递的参数
        	*/
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            synchronized (mLock) {//发生异常处理情况
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }                 

这里的操作基本和Activity窗口的操作逻辑一致了,没有啥特别的,但是这里我们需要关注的是adjustLayoutParamsForSubWindow()对传入的参数params进行调整,而这里parentWindow指向的是Dialog所在Activity对应窗口的,的这个方法我们简单看下:

//[Window.java]
    void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
		...
		/*
			判断窗口类型是不是应用程序子窗口类型
		*/
		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) {
					//LayoutParams的token设置为W本地Binder对象
                    wp.token = decor.getWindowToken();
                }
            }
			...
        } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
			...
        } else {
        	/*
        		注意此时的Window指向的是Activity所在窗口的Window
        		如果不是子窗口和系统窗口,同时当前窗口没有指定容器
        		则设置token为当前窗口的mAppToken代理对象
        		否则设置为指定容器的mAppToken代理对象
        		此时的mAppToken是从AMS传递过来的
        	*/
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
			...
    }

看到了,这里会将Dialog的WindowManager.LayoutParams的token指向它所在Activity的窗口的WindowManager.LayoutParams的token,这个就是关键点。至于后续的的窗口处理流程这里就不再分析了,因为和Activiyt的窗口添加流程完全一直了,有不了解的详见博客Android应用程序窗口设计之窗口的添加里面有非常详见的分析了!


2.5 Dialog在实际开发中需要的注意点

  在Android的实际开发中,我们自定义或者使用Dialog需要注意(普通的Dialog,系统窗口的Dialog除外),在构建它时传入的Context参数一定要是Activity或者它的子类,千万不能是Service的Context或者Application的Context,否则会报如下的运行时错误:

12-18 11:14:59.131  9162  9162 E AndroidRuntime: FATAL EXCEPTION: main
12-18 11:14:59.131  9162  9162 E AndroidRuntime: Process: com.example.mywindow, PID: 9162
12-18 11:14:59.131  9162  9162 E AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.view.ViewRootImpl.setView(ViewRootImpl.java:683)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.app.Dialog.show(Dialog.java:329)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at com.example.mywindow.MainActivity.showDialog(MainActivity.java:61)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at com.example.mywindow.MainActivity.access$0(MainActivity.java:57)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at com.example.mywindow.MainActivity$1.onClick(MainActivity.java:37)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.view.View.performClick(View.java:5637)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.view.View$PerformClick.run(View.java:22433)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.os.Handler.handleCallback(Handler.java:751)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:95)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.os.Looper.loop(Looper.java:154)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:6121)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
12-18 11:14:59.131  9162  9162 E AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)
12-18 11:19:25.765  9506  9506 D AndroidRuntime: >>>>>> START com.android.internal.os.RuntimeInit uid 0 <<<<<<

产生上述错误的原因就是因为普通的Dialog的窗口必须复用创建它的Activity对应的token,这个需要注意!


2.5 Android应用程序Dialog窗口实现流程分析小结

  至此Android应用程序Dialog窗口实现流程分析到这里分析就告一段落了,它的流程和Activity的窗口实现非常非常相似,唯一不同的一点就是Dialog窗口实现中的WindowManager.LayoutParams.token复用了它所在Activity的token,其它的流程基本和Activity一致(如果你对Android应用程序Activity的窗口实现有一定了解的话,那么Dialog窗口实现就是手到擒来的事情了),无外乎如下几点常规操作:

  • 构建Dialog对应的窗口和窗口管理器
  • 加载Dialog对应的布局文件
  • 对Dialog的窗口进行UI布局
  • 然后对Dialog的窗口通过窗口管理器通知WMS进行相关操作,主要是渲染和显示等相关的逻辑处理

虽然分析完毕了,但是我们对于Dialog窗口实现中涉及的一些重要数据还是有必要重点拿出来说说,分别是

  • IWindow: APP端窗口暴露给WMS的抽象实例,同时也是WMS向APP端发送消息的Binder通道,它在APP端的实现为W
  • IWindowSession:WMS端暴露给窗口端的用于和WMS服务端通信的Binder通道
  • WindowState:WMS端窗口的令牌,与IWindow窗口一一对应,是WMS管理窗口的重要依据
  • WindowToken:是窗口的令牌,也是窗口分组的依据,在WMS端,和分组对应的数据结构是WindowToken
  • Token:是在AMS构建Activity对应的ActivityRecord时里面的IApplicationToken的实例,会在Activity创建过程中传递到AMS中,并且Token会在Activity从创建到显示的过程中会在App进程和AMS,WMS之间进行传递,关于Token可以详见博客普法Android的Token前世今生以及在APP,AMS,WMS之间传递,此处很重要!

我们先看看Activity在创建对应Dialog之前,前面各种的取值(注意我是通过dumpsys window全部打印出来,然后截取出来的,这招我在Activity启动过程中分析Activity对应的stack和task时也用到过,详见博客Activity Stack/Task调度算法复盘分析),如下:

#注意我们演示APP对应的包名为com.example.mywindow,Activity信息为com.example.mywindow.MainActivity
WINDOW MANAGER POLICY STATE (dumpsys window policy)
	#标明当前获取焦点的窗口和App
    mFocusedWindow=Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}
    mFocusedApp=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}
    mTopFullscreenOpaqueWindowState=Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}
    mTopFullscreenOpaqueOrDimmingWindowState=Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}

#Session这个是也老熟人了吗,用于App进程和WMS进程通信的匿名Binder,并且此时不要问我如何定位下面的Session就是我们的
#App进程对应的,往后看你就懂了
WINDOW MANAGER SESSIONS (dumpsys window sessions)
  Session Session{878ac3c 6449:1000}:
    mNumWindow=1 mClientDead=false mSurfaceSession=android.view.SurfaceSession@5e33cb1


WINDOW MANAGER DISPLAY CONTENTS (dumpsys window displays)
  Display: mDisplayId=0
    init=720x1280 320dpi cur=720x1280 app=720x1184 rng=720x672-1184x1136
    deferred=false layoutNeeded=false

  Application tokens in top down Z order:
    mStackId=1 #是不是有种熟悉的感觉,WMS中对于窗口也是采取stack的模式的
    mDeferDetach=false
    mFullscreen=true
    mBounds=[0,0][720,1280]
      taskId=122
        mFullscreen=true
        mBounds=[0,0][720,1280]
        mdr=false
        #AppWindowToken不会默认吗
        appTokens=[AppWindowToken{87e485b token=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}}]
        mTempInsetBounds=[0,0][0,0]
          #Token不会默认吗
          Activity #0 AppWindowToken{87e485b token=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}}
          #对应Activity的窗口信息,此时是将Activity的对应的AppWindowToken信息dump出来
          windows=[Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}]
          windowType=2 hidden=false hasVisible=true
          app=true voiceInteraction=false
          allAppWindows=[Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}]
          task={taskId=122 appTokens=[AppWindowToken{87e485b token=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}}] mdr=false}
           appFullscreen=true requestedOrientation=-1
          hiddenRequested=false clientHidden=false reportedDrawn=true reportedVisible=true
          numInterestingWindows=1 numDrawnWindows=1 inPendingTransaction=false allDrawn=true (animator=true)
          startingData=null removed=false firstWindowDrawn=true mIsExiting=false

    DimLayerController
      Stack=1
        dimLayer=shared, animator=null, continueDimming=false
        mDimSurface=Surface(name=DimLayerController/Stack=0) mLayer=22009 mAlpha=0.0
        mLastBounds=[-180,-320][900,1600] mBounds=[-180,-320][900,1600]
        Last animation:  mDuration=200 mStartTime=243235 curTime=303700
         mStartAlpha=0.6 mTargetAlpha=0.0

#显示出所有Token相关信息
WINDOW MANAGER TOKENS (dumpsys window tokens)
  All tokens:
    #注意Activity对应的窗口是对应AppWindowToken,不是WindowToken的
  AppWindowToken{87e485b token=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}}

#显示出所有window相关信息
WINDOW MANAGER WINDOWS (dumpsys window windows)
  Window #3 Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}:
    mDisplayId=0 stackId=1 mSession=Session{878ac3c 6449:1000} mClient=android.os.BinderProxy@aeed31a
    mOwnerUid=1000 mShowToOwnerOnly=true package=com.example.mywindow appop=NONE
    mAttrs=WM.LayoutParams{(0,0)(fillxfill) sim=#120 ty=1 fl=#81810100 wanim=0x103038a vsysui=0x600 needsMenuKey=2}
    Requested w=720 h=1136 mLayoutSeq=174
    mHasSurface=true mShownPosition=[0,0] isReadyForDisplay()=true hasSavedSurface()=false mWindowRemovalAllowed=false
    WindowStateAnimator{93debc3 com.example.mywindow/com.example.mywindow.MainActivity}:
      Surface: shown=true layer=21015 alpha=1.0 rect=(0.0,0.0) 720.0 x 1280.0

此时的我,只想说Android给我们相关的调试信息还是蛮充分的,所以我们在源码学习中一定要充分的利用!用得好可以起到事倍功倍的作用,这个很重要,很重要!

那我们接着看看Activity加载完成Dialog窗口以后,其对应的window调试信息,如下:

WINDOW MANAGER POLICY STATE (dumpsys window policy)
	#注意此时的焦点Window和前面的对比
    mFocusedWindow=Window{e14bb3 u0 com.example.mywindow/com.example.mywindow.MainActivity}
    mFocusedApp=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}
    mTopFullscreenOpaqueWindowState=Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}
    mTopFullscreenOpaqueOrDimmingWindowState=Window{e14bb3 u0 com.example.mywindow/com.example.mywindow.MainActivity}


WINDOW MANAGER SESSIONS (dumpsys window sessions)
  #Session一个应用程序只会拥有一个,但是此时它的mNumWindow已经增加到2个了
  Session Session{878ac3c 6449:1000}:
    mNumWindow=2 mClientDead=false mSurfaceSession=android.view.SurfaceSession@5e33cb1


WINDOW MANAGER DISPLAY CONTENTS (dumpsys window displays)
  Display: mDisplayId=0
    init=720x1280 320dpi cur=720x1280 app=720x1184 rng=720x672-1184x1136
    deferred=false layoutNeeded=false

  Application tokens in top down Z order:
    mStackId=1
    mDeferDetach=false
    mFullscreen=true
    mBounds=[0,0][720,1280]
      taskId=122
        mFullscreen=true
        mBounds=[0,0][720,1280]
        mdr=false
        #此时的Dailog和Activity共用同一个AppWindowToken
        appTokens=[AppWindowToken{87e485b token=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}}]
        mTempInsetBounds=[0,0][0,0]
          Activity #0 AppWindowToken{87e485b token=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}}
          //此时的AppWindowToken拥有两个WindowState,分别指向Activity和Dialog
          windows=[Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}, Window{e14bb3 u0 com.example.mywindow/com.example.mywindow.MainActivity}]
          windowType=2 hidden=false hasVisible=true
          app=true voiceInteraction=false
          allAppWindows=[Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}, Window{e14bb3 u0 com.example.mywindow/com.example.mywindow.MainActivity}]
          task={taskId=122 appTokens=[AppWindowToken{87e485b token=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}}] mdr=false}
           appFullscreen=true requestedOrientation=-1
          hiddenRequested=false clientHidden=false reportedDrawn=true reportedVisible=true
          numInterestingWindows=2 numDrawnWindows=2 inPendingTransaction=false allDrawn=true (animator=true)
          startingData=null removed=false firstWindowDrawn=true mIsExiting=false

WINDOW MANAGER TOKENS (dumpsys window tokens)
  All tokens:
    AppWindowToken{87e485b token=Token{9bde555 ActivityRecord{655280c u0 com.example.mywindow/.MainActivity t122}}}

WINDOW MANAGER WINDOWS (dumpsys window windows)
  #对应Dialog窗口信息
  Window #4 Window{e14bb3 u0 com.example.mywindow/com.example.mywindow.MainActivity}:
    mDisplayId=0 stackId=1 mSession=Session{878ac3c 6449:1000} mClient=android.os.BinderProxy@432d522
    mOwnerUid=1000 mShowToOwnerOnly=true package=com.example.mywindow appop=NONE
    mAttrs=WM.LayoutParams{(0,0)(wrapxwrap) gr=#11 sim=#120 ty=2 fl=#1800002 fmt=-3 wanim=0x103038b surfaceInsets=Rect(64, 64 - 64, 64) needsMenuKey=2}
    Requested w=640 h=192 mLayoutSeq=179
    mHasSurface=true mShownPosition=[40,520] isReadyForDisplay()=true hasSavedSurface()=false mWindowRemovalAllowed=false
    WindowStateAnimator{4d216e com.example.mywindow/com.example.mywindow.MainActivity}:
      mAnimating=false mLocalAnimating=false mAnimationIsEntrance=true mAnimation=null mStackClip=1
      Surface: shown=true layer=21020 alpha=1.0 rect=(-24.0,456.0) 768.0 x 320.0
	
  #对应Activity窗口信息
  Window #3 Window{c1dfe4b u0 com.example.mywindow/com.example.mywindow.MainActivity}:
    mDisplayId=0 stackId=1 mSession=Session{878ac3c 6449:1000} mClient=android.os.BinderProxy@aeed31a
    mOwnerUid=1000 mShowToOwnerOnly=true package=com.example.mywindow appop=NONE
    mAttrs=WM.LayoutParams{(0,0)(fillxfill) sim=#120 ty=1 fl=#81810100 wanim=0x103038a vsysui=0x600 needsMenuKey=2}
    Requested w=720 h=1136 mLayoutSeq=179
    mHasSurface=true mShownPosition=[0,0] isReadyForDisplay()=true hasSavedSurface()=false mWindowRemovalAllowed=false
    WindowStateAnimator{93debc3 com.example.mywindow/com.example.mywindow.MainActivity}:
      Surface: shown=true layer=21015 alpha=1.0 rect=(0.0,0.0) 720.0 x 1280.0

好了,有了上面知识的铺垫,读者应该就很容易理解下面的关系图了(这个可是精华)。

在这里插入图片描述

在这里插入图片描述




三.Android系统窗口实现流程分析

  在Android的窗口中有一类比较特殊的窗口,那就是系统窗口譬如我们创建的输入法,系统弹出框啊!它最大的特点就是不需要依附于任何的窗口,可以独立存在任何场合(譬如长按电源键弹出的对话框)。在本章节中我们将来分析一下Android系统窗口实现的流程!


3.1 Android系统窗口实现流程分析简单实例

分析总要有个源头不是,这里我们就以最最简单的系统窗口创建实例入手来分析,简单的demo如下:

   private void showSystemWindow() {
   		//注意此处传入的是Applcation的上下文,所以说明系统窗口不依赖任何的窗口
        final WindowManager wm = (WindowManager) getApplicationContext()
                .getSystemService(Context.WINDOW_SERVICE);

        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.gravity = Gravity.CENTER;
        // 注意要设置此属性,不然其它的View无法获取焦点
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

		params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;//这个地方是重点,设置窗口的类型

		//通过布局加载器加载布局文件
        final View mView = LayoutInflater.from(this).inflate(R.layout.title,
                null);

        wm.addView(mView, params);//调用WindowMangerImpl添加窗口

    }

很简单的逻辑!

这里有一点需要注意,普通的应用程序进程没有权限添加系统窗口的否则会提示如下的错误信息:

12-19 15:06:09.346  7924  7924 D AndroidRuntime: Shutting down VM
12-19 15:06:09.361  7924  7924 E AndroidRuntime: FATAL EXCEPTION: main
12-19 15:06:09.361  7924  7924 E AndroidRuntime: Process: com.example.mywindow, PID: 7924
12-19 15:06:09.361  7924  7924 E AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f050fcf -- permission denied for window type 2003
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at android.view.ViewRootImpl.setView(ViewRootImpl.java:703)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at com.example.mywindow.MainActivity.showSystemWindow(MainActivity.java:100)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at com.example.mywindow.MainActivity.access$0(MainActivity.java:85)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at com.example.mywindow.MainActivity$1.onClick(MainActivity.java:40)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at android.view.View.performClick(View.java:5637)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at android.view.View$PerformClick.run(View.java:22433)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at android.os.Handler.handleCallback(Handler.java:751)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:95)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at android.os.Looper.loop(Looper.java:154)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:6121)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
12-19 15:06:09.361  7924  7924 E AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)

3.2 Android系统窗口实现流程分析

关于Android系统窗口的创建,布局文件的加载这里就不过多分析了,它和前面几乎没有差别(这个读者有了前面的分析基础,得来基本就是so easy了)!我们这里重点来看下WindowManagerGlobal对addView()方法的处理,如下:

//[WindowManagerGlobal.java]
    public void addView(View view, //此处的view指向DecorView
    					ViewGroup.LayoutParams params,//这里的params为Window对应的默认WindowManager.LayoutParams实例对象mWindowAttributes
            			Display display, //这里的Display具体指向表示物理显示设备有关的逻辑显示的大小(譬如尺寸分辨率)和密度的信息
            			Window parentWindow) //这里的parentWindow为null,详见WindowManager的获取流程
   	{
        //参数有效性的检查
        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;
		
		//此时的parentWindow为null,不会执行此流程,当然如果前面获取的是WindowManager传入的是Activity就会执行了
		if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
			...
        }

        ViewRootImpl root;
        View panelParentView = null;        
        synchronized (mLock) {
            if (mSystemPropertyUpdater == null) {//监听Property的变化
				...
            }

			/*
				从mViews中查找是否已经有添加过同样的View
			*/
            int index = findViewLocked(view, false);
            if (index >= 0) {
				...
            }

			...
			/*
				构建ViewRootImpl
				它很重要,它很重要,它很重要,它是WindowManagerGlobal最最得力的干将没有之一
				基本包揽了WindowManagerGlobal绝大部分工作
			*/
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);//这三者之间是一一对应的情况
            mRoots.add(root);
            mParams.add(wparams);
        }

        try {
        	/*
        		将DecorView添加到ViewRootImpl中,最后添加到WMS中
        		注意这里传递的参数
        	*/
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            synchronized (mLock) {//发生异常处理情况
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }                 

这里的操作基本和Activity窗口的操作逻辑一致了,没有啥特别的,但是这里我们需要关注的是adjustLayoutParamsForSubWindow()对传入的参数params进行调整,而这里parentWindow指向的是系统窗口创建时获取窗口管理器时传入的Context上下文(虽然我们传入的是应用程序Application的上下文,但是我们看看假如当我们传入的是Activity的上下文呢),

//[Window.java]
    void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
		...
		/*
			判断窗口类型是不是应用程序子窗口类型
		*/
		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) {
					//LayoutParams的token设置为W本地Binder对象
                    wp.token = decor.getWindowToken();
                }
            }
			...
        } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
			...
        } else {
        	/*
        		注意此时的Window指向的是Activity所在窗口的Window
        		如果不是子窗口和系统窗口,同时当前窗口没有指定容器
        		则设置token为当前窗口的mAppToken代理对象
        		否则设置为指定容器的mAppToken代理对象
        		此时的mAppToken是从AMS传递过来的
        	*/
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
			...
    }

这里可以看到那怕我们创建系统窗口的时候传入的是Activity的上下文,它的WindowManager.LayoutParams.token依然为null!

我们接着往下看,其中ViewRootImpl对窗口的处理这里我们不关注,我们重点看看WMS服务对窗口的处理流程,这个也是和Dialog和Activity窗口处理流程的差异点,我们上源码,如下:

    public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {

		//这里的client为IWindow的代理对象,用于WMS和ViewRootImpl进行通信
		//session为前面创建的用于APP端和WMS通信的匿名Binder
		...

        WindowState attachedWindow = null;
        final int type = attrs.type;
 

        synchronized(mWindowMap) {
			...

			//判断窗口是否已经存在,肯定没有
            if (mWindowMap.containsKey(client.asBinder())) {
                Slog.w(TAG_WM, "Window " + client + " is already added");
                return WindowManagerGlobal.ADD_DUPLICATE_ADD;
            }

			//判断添加窗口的类型,老子属于系统窗口不会走入此分支
            if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
				..
            }

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

            boolean addToken = false;
			//根据attrs.token从mTokenMap中取出系统窗口在WMS服务中的描述符WindowToken,此时的attrs.token取值为null
            WindowToken token = mTokenMap.get(attrs.token);
            AppWindowToken atoken = null;
            boolean addToastWindowRequiresToken = false;

            if (token == null) {//第一次add的情况下token怎么会有值
                ...
                //为系统窗口创建WindowToken
                token = new WindowToken(this, attrs.token, -1, false);
                addToken = true;
            } 
			//应用程序窗口
			else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
				...
            } //输入法窗口
			else if (type == TYPE_INPUT_METHOD) {
				...
            }//壁纸窗口
			else if (type == TYPE_WALLPAPER) {
				...
            }//Dream窗口 
			else if (type == TYPE_DREAM) {
				...
            } else if (type == TYPE_ACCESSIBILITY_OVERLAY) {
				...
            } else if (type == TYPE_TOAST) {
				...
            } else if (type == TYPE_QS_DIALOG) {
				...
            } else if (token.appWindowToken != null) {
				...
            }

			//构建WindowState
            WindowState win = new WindowState(this, session, client, token,
                    attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);
			...

			//以键值对<IWindow.Proxy/Token,WindowToken>形式保存到mTokenMap表中
            if (addToken) {
                mTokenMap.put(attrs.token, token);
            }
            win.attach();
			//以键值对<IWindow的代理对象,WindowState>形式保存到mWindowMap表中
            mWindowMap.put(client.asBinder(), win);
			
			...

        return res;
    }

这里我们可以看到对于系统窗口会执行如下的逻辑:

  • 构建一个WindowToken,注意Dialog和Activity对应的是AppWindowToken
  • 然后以传递过来的参数构建WindowState
  • 以键值对<IWindow.Proxy/Token,WindowToken>形式保存WindowToken到mTokenMap表中,这里注意此时的key为null(说实话这里我有一个疑问,就是假如同时添加多个这种value呢,这个不会有哈希冲突吗)
  • 以键值对<IWindow的代理对象,WindowState>形式保存WindowState到mWindowMap表中

3.3 Android系统窗口实现流程分析小结

  至此Android系统窗口实现流程分析小结就告一段落了,它的流程和Activity以及Dialog窗口的实现流程差别不大,唯一不同的一点就是Dialog窗口实现中的WindowManager.LayoutParams.token为null,且它没有对应的AppWindowToken只有WindowToken(type的区别这里就不需要细讲了)!这里我们通过命令来查看下系统窗口的具体情况:


WINDOW MANAGER SESSIONS (dumpsys window sessions)
  #系统窗口的Session共用调用它的Android应用程序进程的
  Session Session{e781850 8410:1000}:
    mNumWindow=2 mClientDead=false mSurfaceSession=android.view.SurfaceSession@ccda26

WINDOW MANAGER TOKENS (dumpsys window tokens)
  All tokens:
  WindowToken{80ef651 null} #这个指向了刚才的系统窗口
 
WINDOW MANAGER WINDOWS (dumpsys window windows)
  Window #6 Window{b286c14 u0 com.example.mywindow}:
    mDisplayId=0 stackId=0 mSession=Session{e781850 8410:1000} mClient=android.os.BinderProxy@114a667
    mOwnerUid=1000 mShowToOwnerOnly=true package=com.example.mywindow appop=SYSTEM_ALERT_WINDOW
    mAttrs=WM.LayoutParams{(0,0)(wrapxwrap) gr=#11 sim=#20 ty=2003 fl=#1000008}
    Requested w=168 h=38 mLayoutSeq=343
    mHasSurface=true mShownPosition=[276,597] isReadyForDisplay()=true hasSavedSurface()=false mWindowRemovalAllowed=false
    WindowStateAnimator{c9c4cbd }:
      Surface: shown=true layer=111000 alpha=1.0 rect=(276.0,597.0) 168.0 x 38.0

各位读者,由于本人想在12月底之前突破50万的访问量,所以在本篇博客没有完全结束之前就放上去了,这里给各位说声对不住了。争取尽力在本月完成本篇博客的完成。希望各位多多点赞和关注,当然也可以吐槽我了!



四.Android子窗口之PopupWindow实现流程分析

  在Android的窗口中除了上述比较特殊一类窗口系统窗口之外,还有一种就是Android的子窗口,其中比较常见的就是弹出式的PopupWindow窗口!它最大的特点就是必须依附于Android应用程序窗口,而不可以独立存在。在本章节中我们将来分析一下Android子窗口PopupWindow实现的流程!


4.1 Android子窗口之PopupWindow简单实例

分析总要有个源头不是,这里我们就以最最简单的PopupWindow创建实例入手来分析,简单的demo如下:

    private void showPopuWindow() {

        TextView contentView = new TextView(MainActivity.this);
		//构建PopupWindow窗口实例对象
        PopupWindow mPopupWindow = new PopupWindow(contentView,android.view.ViewGroup.LayoutParams.WRAP_CONTENT,android.view.ViewGroup.LayoutParams.WRAP_CONTENT);

        View rootView = LayoutInflater.from(MainActivity.this).inflate(
                R.layout.activity_main, null);
		//显示窗口
        mPopupWindow.showAtLocation(rootView.getWindowToken(), Gravity.CENTER, 0, 0);

    }

无论何种复杂的PopupWindow实现,其归根究底它们都是来源于对PopupWindow基类的再次封装而已,所以我们回归本质从最简单的PopupWindow开始入手分析Android应用程序窗口的实现。


4.2 Android子窗口之PopupWindow创建源码分析

  对于Android中各种类的实现,通常是从其构造方法开始的,而我们的PopupWindow也不能免俗!在开始分析前,我们先看下PopupWindow的类图:

在这里插入图片描述

PopupWindow中的关键成员变量和Activity以及Dialog何其相似啊(当然也不完全一样,有点点差别),不感慨了我们接着分析源码!

//[PopupWindow.java]
	//注意当没有指定focusable的值时默认为false,即PopupWindow不能获取到焦点
    public PopupWindow(View contentView, int width, int height) {
        this(contentView, width, height, false);
    }

    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中有个缓存机制,会缓存Activity应用窗口创建过程中的WidnowManager实例对象
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }

        setContentView(contentView);//加载布局文件,这个不重点分析了
        setWidth(width);
        setHeight(height);
        setFocusable(focusable);
    }

PopupWindow的构造逻辑并不是非常的复杂,但是有几点我们需要注意的是:

  • PopupWindow如果没有指明是focusable的话,那么是不能获取焦点的
  • 此处我们是在Activity中创建PopupWindow的,这里我们必须记住此时的mWindowManager和Dialog都是一样,共用的是和Activity同样一个实例对象,即引用的是Activity的mWindowManager实例对象!

其它的和Dialog创建差不多,这里就简单的带过了(因为我相信能阅读至此的读者,功力已经大增了,这点妖魔鬼怪是小意思了!)。

我行,我可以的!


4.3 Android子窗口之PopupWindow显示源码分析

  PopupWindow我们也创建成功了,布局也加载了,万事俱备只欠东风了,就待我们将显示了,而这个就需要借助它的showAtxxx()方法了,这里我们以showAtLocation()方法为例说明,如下:

//[PopupWindow.java]
    public void showAtLocation(View parent, int gravity, int x, int y) {
    	//此处parent.getWindowToken()获取到的是Activity在窗口创建过程中创建的W对象
    	//这个地方如果有不清楚的可以参见Activity窗口流程
        showAtLocation(parent.getWindowToken(), gravity, x, y);
    }

    /**
     * Display the content view in a popup window at the specified location.
     *
     * @param token Window token to use for creating the new window
     * @param gravity the gravity which controls the placement of the popup window
     * @param x the popup's x location offset
     * @param y the popup's y location offset
     *
     * @hide Internal use only. Applications should use
     *       {@link #showAtLocation(View, int, int, int)} instead.
     */
    public void showAtLocation(IBinder token, int gravity, int x, int y) {
        if (isShowing() || mContentView == null) {
            return;
        }

        TransitionManager.endTransitions(mDecorView);

        detachFromAnchor();//此处忽略

        mIsShowing = true;
        mIsDropdown = false;
        mGravity = gravity;

		//构建WindowManager.LayoutParams
        final WindowManager.LayoutParams p = createPopupLayoutParams(token);
        preparePopup(p);//初始化PopuWindow

        p.x = x;
        p.y = y;

        invokePopup(p);//开启显示流程
    }

在开发分析PopupWindow窗口显示逻辑之前,我们重点需要关注的是入参参数token的由来,这个我在前面有专门的博客有重点分析了,它是在Activity在窗口创建过程中创建的W对象,如果有对此处不清楚的读者墙裂建议参见下源码普法Android的Token前世今生以及在APP,AMS,WMS之间传递和>Android应用程序窗口设计之窗口的添加,这里我就不细述了。

我们接着分析源码,在showAtLocation方法中的主要逻辑如下:

  • 调用createPopupLayoutParams()方法调整WindowManager.LayoutParams参数
  • 调用preparePopup()初始化PopuWindow相关参数
  • 调用invokePopup方法开启显示流程

这里我们对上述三个流程,一一分析各个突破!

4.3.1 createPopupLayoutParams()调整WindowManager.LayoutParams参数
//[PopupWindow.java]
 private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
 private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();


        p.gravity = computeGravity();
        p.flags = computeFlags(p.flags);
        p.type = mWindowLayoutType;//这两个的取值是关键
        p.token = token;//这两个的取值是关键
        p.softInputMode = mSoftInputMode;
        p.windowAnimations = computeAnimationResource();

        if (mBackground != null) {
            p.format = mBackground.getOpacity();
        } else {
            p.format = PixelFormat.TRANSLUCENT;
        }

        if (mHeightMode < 0) {
            p.height = mLastHeight = mHeightMode;
        } else {
            p.height = mLastHeight = mHeight;
        }

        if (mWidthMode < 0) {
            p.width = mLastWidth = mWidthMode;
        } else {
            p.width = mLastWidth = mWidth;
        }

        p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH
                | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;

        p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));

        return p;
    }

这里主要是对WindowManager.LayoutParams的参数进行填充,我们对上述方法中最最要关注的就是两个点:

  • type值的填充:可以看到赋予的是TYPE_APPLICATION_PANEL明确指向了子窗口类型
  • token值的指向:token值的指向传递流程比较复杂,我这里直接点明(在我们的当前环境是创建它的Activity的窗口W对象)

其它的都是一些常规布局参数的调整,这里就不过多分析了!

4.3.2 preparePopup()初始化PopuWindow相关参数
//[PopupWindow.java]
    private void preparePopup(WindowManager.LayoutParams p) {
        if (mContentView == null || mContext == null || mWindowManager == null) {
            throw new IllegalStateException("You must specify a valid content view by "
                    + "calling setContentView() before attempting to show the popup.");
        }


        if (mDecorView != null) {
            mDecorView.cancelTransitions();
        }


        if (mBackground != null) {
            mBackgroundView = createBackgroundView(mContentView);
            mBackgroundView.setBackground(mBackground);
        } else {
            mBackgroundView = mContentView;
        }

		//构建DecorView
        mDecorView = createDecorView(mBackgroundView);

		
        mBackgroundView.setElevation(mElevation);

        p.setSurfaceInsets(mBackgroundView, true /*manual*/, true /*preservePrevious*/);

        mPopupViewInitialLayoutDirectionInherited =
                (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
    }

通过上述源码可以看到,preparePopup()源码逻辑不是很复杂主要是根据前面获取的参数对mDecorView 进行一些调整和PopuWindow的窗口的一些变量初始化。

4.3.3 invokePopup方法开启显示流程
//[]
   private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }

        final PopupDecorView decorView = mDecorView;
        decorView.setFitsSystemWindows(mLayoutInsetDecor);

        setLayoutDirectionFromAnchor();

        mWindowManager.addView(decorView, p);//老面孔了

        if (mEnterTransition != null) {
            decorView.requestEnterTransition(mEnterTransition);
        }
    }

前面所做的一切都是为了此时的荣耀时刻,这里我们又看到熟悉的面孔了addView()方法了,这个方法在应用程序端的处理,前面已经详细的分析了,这个理就不过多细说了。


4.4 Android子窗口之PopupWindow实现WMS服务对其处理流程

我们知道addView()方法最终会调用到WMS端的addWindow()方法,我们看看addWindow()方法中对于子窗口添加的处理流程,如下:

//[WindowManagerService.java]
    public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {

		//这里的client为IWindow的代理对象,用于WMS和ViewRootImpl进行通信
		//session为前面创建的用于APP端和WMS通信的匿名Binder
		
		
		...
        WindowState attachedWindow = null;

        final int callingUid = Binder.getCallingUid();
        final int type = attrs.type;
		...

        synchronized(mWindowMap) {
			...

			//判断窗口是否已经存在
            if (mWindowMap.containsKey(client.asBinder())) {
                Slog.w(TAG_WM, "Window " + client + " is already added");
                return WindowManagerGlobal.ADD_DUPLICATE_ADD;
            }

			//判子窗口类型,会进入此分支
            if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
				
				/*   
					根据attrs.token从mWindowMap中取出应用程序窗口在WMS服务中的描述符WindowState	
					这里的前提是父窗口必须在WMS中已经被添加了,此处的token指向的是Activity的W对象在WMS端的代理
				*/
				
				attachedWindow = windowForClientLocked(null, attrs.token, false);
				/**************************************************************************/
				//这里为额演示方便直接将代码放上来
				    final WindowState windowForClientLocked(Session session, IBinder client,
				            boolean throwOnError) {
				        WindowState win = mWindowMap.get(client);
				        if (localLOGV) Slog.v(
				            TAG_WM, "Looking up client " + client + ": " + win);
				        if (win == null) {
				            RuntimeException ex = new IllegalArgumentException(
				                    "Requested window " + client + " does not exist");
				            if (throwOnError) {
				                throw ex;
				            }
				            Slog.w(TAG_WM, "Failed looking up window", ex);
				            return null;
				        }
				        if (session != null && win.mSession != session) {
				            RuntimeException ex = new IllegalArgumentException(
				                    "Requested window " + client + " is in session " +
				                    win.mSession + ", not " + session);
				            if (throwOnError) {
				                throw ex;
				            }
				            Slog.w(TAG_WM, "Failed looking up window", ex);
				            return null;
				        }
				
				        return win;
				    }
				/**************************************************************************/
                if (attachedWindow == 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 (attachedWindow.mAttrs.type >= FIRST_SUB_WINDOW
                        && attachedWindow.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;
                }
            }



            boolean addToken = false;
			//根据attrs.token从mTokenMap中取出WindowToken,由于前面没有添加过,此处取出的肯定为null
            WindowToken token = mTokenMap.get(attrs.token);
            AppWindowToken atoken = null;
            boolean addToastWindowRequiresToken = false;

            if (token == null) {
				//创建PopupWindow对应的WindowToken
                token = new WindowToken(this, attrs.token, -1, false);
                addToken = true;
            } 
			//应用程序窗口
			else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {

            } //输入法窗口
			else if (type == TYPE_INPUT_METHOD) {
				...
            } else if (type == TYPE_VOICE_INTERACTION) {
				...
            }//壁纸窗口
			else if (type == TYPE_WALLPAPER) {
				...
            }//Dream窗口 
			else if (type == TYPE_DREAM) {

            } else if (type == TYPE_ACCESSIBILITY_OVERLAY) {

            } else if (type == TYPE_TOAST) {

            } else if (type == TYPE_QS_DIALOG) {
				...
            } else if (token.appWindowToken != null) {
				...
            }

			//为PopupWindow窗口创建WindowState对象
            WindowState win = new WindowState(this, session, client, token,
                    attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);
	
			
			//以键值对<IWindow.Proxy/Token,WindowToken>形式保存到mTokenMap表中
			//子窗口此处的Key就是IWindow.Proxy类型
            if (addToken) {
                mTokenMap.put(attrs.token, token);
            }
            win.attach();
			//以键值对<IWindow的代理对象,WindowState>形式保存到mWindowMap表中
            mWindowMap.put(client.asBinder(), win);
			
			...

            if (type == TYPE_INPUT_METHOD) {

            } else if (type == TYPE_INPUT_METHOD_DIALOG) {

            } else {
                addWindowToListInOrderLocked(win, true);//将WindowState添加到关联的AppWindowToken中
				
            }
            ...

        return res;
    }

这里我们可以看到WMS的addWindow()对于子窗口的处理流程也不是很复杂,主要逻辑如下:

  • 首先判断窗口是否重复添加了

  • 判断窗口类型是否是子窗口,是为该子窗口则创建WIndowToken对象实例

  • 然后为该子窗口构建WindowState对象实例

  • 以键值对<IWindow.Proxy/Token,WindowToken>形式将前面构建的WIndowToken保存到mTokenMap表中,注意此处的key为IWindow.Proxy

  • 以键值对<IWindow的代理对象,WindowState>形式保存到mWindowMap表中

> 上述的关系详见,下面的示意图

  • 最后将WindowState添加到关联的AppWindowToken中

4.5 PopupWindow在实际开发中需要的注意点

  在Android的实际开发中,我们自定义或者使用PopupWindow需要注意,在构建它时传入的Context参数一定要是Activity或者它的子类,千万不能是Service的Context或者Application的Context,否则会报如下的运行时错误:

12-22 11:43:29.294  8393  8393 E AndroidRuntime: FATAL EXCEPTION: main
12-22 11:43:29.294  8393  8393 E AndroidRuntime: Process: com.example.mywindow, PID: 8393
12-22 11:43:29.294  8393  8393 E AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.view.ViewRootImpl.setView(ViewRootImpl.java:679)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.widget.PopupWindow.invokePopup(PopupWindow.java:1378)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.widget.PopupWindow.showAtLocation(PopupWindow.java:1154)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at com.example.mywindow.MainActivity.showPopuWindow(MainActivity.java:82)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at com.example.mywindow.MainActivity.access$0(MainActivity.java:73)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at com.example.mywindow.MainActivity$1.onClick(MainActivity.java:41)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.view.View.performClick(View.java:5637)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.view.View$PerformClick.run(View.java:22433)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.os.Handler.handleCallback(Handler.java:751)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:95)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.os.Looper.loop(Looper.java:154)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:6121)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
12-22 11:43:29.294  8393  8393 E AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)

产生上述错误的原因就是因为PopopWindow窗口必须复要附加在attachWindow窗口之上,传入的不是Activity类型的则找不到attach窗口进而报错!


4.6 Android子窗口之PopupWindow实现流程分析小结

  Android子窗口之PopupWindow实现流程分析就告一段落了,它的流程和Activity以及Dialog窗口的实现流程差别不大,唯一不同的一点就是PopupWindow窗口实现中的WindowManager.LayoutParams.token必须为attach窗口的W对象,且它没有对应的AppWindowToken只有WindowToken(type的区别,这里就不需要细讲了)!这里我们通过命令来查看下系统窗口的具体情况:

WINDOW MANAGER POLICY STATE (dumpsys window policy)
	#当前获取焦点的应用程序和窗口WindowState信息
    mFocusedWindow=Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}
    mFocusedApp=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}
    mTopFullscreenOpaqueWindowState=Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}
    mTopFullscreenOpaqueOrDimmingWindowState=Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}

WINDOW MANAGER SESSIONS (dumpsys window sessions)
  #此处Session需要从后面分析得到!
  Session Session{2f32c1f 6338:1000}:
    mNumWindow=1 mClientDead=false mSurfaceSession=android.view.SurfaceSession@c8fbc04


WINDOW MANAGER DISPLAY CONTENTS (dumpsys window displays)
  Display: mDisplayId=0
    init=720x1280 320dpi cur=720x1280 app=720x1184 rng=720x672-1184x1136
    deferred=false layoutNeeded=false

  Application tokens in top down Z order:
    mStackId=1
    mDeferDetach=false
    mFullscreen=true
    mBounds=[0,0][720,1280]
      taskId=132
        mFullscreen=true
        mBounds=[0,0][720,1280]
        mdr=false
        appTokens=[AppWindowToken{f0ba2d3 token=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}}]
        mTempInsetBounds=[0,0][0,0]
          Activity #0 AppWindowToken{f0ba2d3 token=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}}
          windows=[Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}]
          windowType=2 hidden=false hasVisible=true
          app=true voiceInteraction=false
          #AppWindowToken拥有的WindwoState
          allAppWindows=[Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}]
          task={taskId=132 appTokens=[AppWindowToken{f0ba2d3 token=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}}] mdr=false}
           appFullscreen=true requestedOrientation=-1
          hiddenRequested=false clientHidden=false reportedDrawn=true reportedVisible=true
          numInterestingWindows=1 numDrawnWindows=1 inPendingTransaction=false allDrawn=true (animator=true)
          startingData=null removed=false firstWindowDrawn=true mIsExiting=false


WINDOW MANAGER TOKENS (dumpsys window tokens)
  All tokens:
  AppWindowToken{f0ba2d3 token=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}}

#展示所有WindowState信息
WINDOW MANAGER WINDOWS (dumpsys window windows)
  Window #2 Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}:
    mDisplayId=0 stackId=1 mSession=Session{2f32c1f 6338:1000} mClient=android.os.BinderProxy@bc1d535
    mOwnerUid=1000 mShowToOwnerOnly=true package=com.example.mywindow appop=NONE
    mAttrs=WM.LayoutParams{(0,0)(fillxfill) sim=#120 ty=1 fl=#81810100 wanim=0x103038a vsysui=0x600 needsMenuKey=2}
    Requested w=720 h=1136 mLayoutSeq=138
    mHasSurface=true mShownPosition=[0,0] isReadyForDisplay()=true hasSavedSurface()=false mWindowRemovalAllowed=false
    WindowStateAnimator{9a88017 com.example.mywindow/com.example.mywindow.MainActivity}:
      Surface: shown=true layer=21010 alpha=1.0 rect=(0.0,0.0) 720.0 x 1280.0

那我们接着看看Activity加载完成PopupWindow窗口以后,其对应的window调试信息,如下:

WINDOW MANAGER POLICY STATE (dumpsys window policy)
	#注意此时焦点的Window依然为Activity对应的窗口,这个和我们前面分析的一致默认PopupWindow是没有焦点的
    mFocusedWindow=Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}
    mFocusedApp=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}
    mTopFullscreenOpaqueWindowState=Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}
    mTopFullscreenOpaqueOrDimmingWindowState=Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}

WINDOW MANAGER SESSIONS (dumpsys window sessions)
  #此时可以看到我们的目标Session管理着两个窗口
  Session Session{2f32c1f 6338:1000}:
    mNumWindow=2 mClientDead=false mSurfaceSession=android.view.SurfaceSession@c8fbc04

WINDOW MANAGER DISPLAY CONTENTS (dumpsys window displays)
  Display: mDisplayId=0
    init=720x1280 320dpi cur=720x1280 app=720x1184 rng=720x672-1184x1136
    deferred=false layoutNeeded=false

  Application tokens in top down Z order:
    mStackId=1
    mDeferDetach=false
    mFullscreen=true
    mBounds=[0,0][720,1280]
      taskId=132
        mFullscreen=true
        mBounds=[0,0][720,1280]
        mdr=false
        appTokens=[AppWindowToken{f0ba2d3 token=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}}]
        mTempInsetBounds=[0,0][0,0]
          Activity #0 AppWindowToken{f0ba2d3 token=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}}
          windows=[Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}]
          windowType=2 hidden=false hasVisible=true
          app=true voiceInteraction=false
          #可以看到此时多了一个WindowState指向了PopupWIndow
          allAppWindows=[Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}, Window{4729c22 u0 PopupWindow:d8468bc}]
          task={taskId=132 appTokens=[AppWindowToken{f0ba2d3 token=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}}] mdr=false}
           appFullscreen=true requestedOrientation=-1
          hiddenRequested=false clientHidden=false reportedDrawn=true reportedVisible=true
          numInterestingWindows=2 numDrawnWindows=2 inPendingTransaction=false allDrawn=true (animator=true)
          startingData=null removed=false firstWindowDrawn=true mIsExiting=false

WINDOW MANAGER TOKENS (dumpsys window tokens)
  All tokens:
  WindowToken{a4e9470 android.os.BinderProxy@bc1d535}#指向PopupWindow窗口
  AppWindowToken{f0ba2d3 token=Token{7d76d0d ActivityRecord{9f59ba4 u0 com.example.mywindow/.MainActivity t132}}}

WINDOW MANAGER WINDOWS (dumpsys window windows)
  Window #3 Window{4729c22 u0 PopupWindow:d8468bc}:
    mDisplayId=0 stackId=1 mSession=Session{2f32c1f 6338:1000} mClient=android.os.BinderProxy@39475ed
    mOwnerUid=1000 mShowToOwnerOnly=true package=com.example.mywindow appop=NONE
    mAttrs=WM.LayoutParams{(0,0)(wrapxwrap) gr=#11 sim=#21 ty=1000 fl=#41800008 pfl=0x18000 fmt=-3 surfaceInsets=Rect(0, 0 - 0, 0) (manual)}
    Requested w=275 h=38 mLayoutSeq=142
    #注意mAttachedWindow指向的是谁
    mAttachedWindow=Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity} mLayoutAttached=true
    mHasSurface=true mShownPosition=[222,621] isReadyForDisplay()=true hasSavedSurface()=false mWindowRemovalAllowed=false
    WindowStateAnimator{4ab06b3 PopupWindow:d8468bc}:
      Surface: shown=true layer=21015 alpha=1.0 rect=(222.0,621.0) 275.0 x 38.0
  Window #2 Window{c7c1dca u0 com.example.mywindow/com.example.mywindow.MainActivity}:
    mDisplayId=0 stackId=1 mSession=Session{2f32c1f 6338:1000} mClient=android.os.BinderProxy@bc1d535
    mOwnerUid=1000 mShowToOwnerOnly=true package=com.example.mywindow appop=NONE
    mAttrs=WM.LayoutParams{(0,0)(fillxfill) sim=#120 ty=1 fl=#81810100 wanim=0x103038a vsysui=0x600 needsMenuKey=2}
    Requested w=720 h=1136 mLayoutSeq=142
    mHasSurface=true mShownPosition=[0,0] isReadyForDisplay()=true hasSavedSurface()=false mWindowRemovalAllowed=false
    WindowStateAnimator{9a88017 com.example.mywindow/com.example.mywindow.MainActivity}:
      Surface: shown=true layer=21010 alpha=1.0 rect=(0.0,0.0) 720.0 x 1280.0

好了,有了上面知识的铺垫和具体的打印信息,读者应该就很容易理解下面的关系图了(这个可是精华)。

在这里插入图片描述




五.总结

其实Android的窗口实现中还有一个比较特殊,并且经常会被我们用到的Toast类型的系统窗口没有分析到,这里主要是限于篇幅 和为了阅读的方面性(Toast涉及的知识点有点多,如果硬要凑在一篇中估计读者也没有这么多耐心一下子接收这么多信息量!)

  好了,至此Android窗口设计之Dialog、PopupWindow、系统窗口的实现就分析到这里告一段落了,我们回过头来对其流程捋一捋,你会惊奇的发现Dialog、PopupWindow、系统窗口的实现和Android应用程序窗口实现基本一致只是在细节的处理上有所不同(譬如Token的处理上面)。而全篇博客分析下来我们的Dialog、PopupWindow、系统窗口实现的最最核心流程莫过于如下几点:

  • 各种窗口的window创建以及对应管理器的创建
  • 通过窗口管理器加载各种窗口布局
  • 通过窗口管理器添加已经加载好布局的窗口
  • 对窗口中UI进行布局
  • WMS服务对布局完成好的窗口进行下一步的操作,主要是开启渲染和显示流程

而上述的一切一切的起点都是从Android应用程序窗口实现而构建的,所以在学习Android各种窗口的实现之前,墙裂建议读者一定要对Android应用程序窗口的实现先弄清楚,这样才能事半功倍的(所以一定要先阅读Android应用程序窗口设计系列博客)。




六.写在最后

  今天的博客到这里就真的完结了,不知道Android窗口系列博客下来各位读者是否有所收获或者有所感悟!当然如果读者有什么问题或者疑问也可以和我一起探讨那是最好不过的了。在下篇博客中我们将继续分析Android窗口中比较特殊的一个类型Toast,欢迎继续关注和阅读。好了,青山不改绿水长流,各位江湖见!当然各位读者的点赞和关注是我写作路上前进的最大动力了,如果有啥不对或者不爽的也可以踩一踩也无妨!

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值