主頁 > 移動端開發 > Android視窗設計之Dialog、PopupWindow、系統視窗的實作

Android視窗設計之Dialog、PopupWindow、系統視窗的實作

2020-12-20 12:11:29 移動端開發

????視窗設計之Dialog、PopupWindow、系統視窗的實作


Android應用程式視窗設計系列博客:

Android應用程式視窗設計之Window及WindowManager的創建
Android應用程式視窗設計之setContentView布局加載的實作
普法Android的Token前世今生以及在APP,AMS,WMS之間傳遞
Android應用程式視窗設計之視窗的添加
Android應用程式視窗設計之建立與WMS服務之間的通信程序
Android視窗設計之Dialog、PopupWindow、系統視窗的實作



引言

??通過前面系列博客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}}}
          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
          //此時的AppWindowToken擁有兩個WindowState,分別指向Activity和Dialog
          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!這里我們通過命令來查看下系統視窗的具體情況:


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萬的訪問量,所以在本篇博客沒有完全結束之前就放上去了,這里給各位說聲對不住了,爭取盡力在本月完成本篇博客的完成,希望各位多多點贊和關注,當然也可以吐槽我了!

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/237683.html

標籤:其他

上一篇:Android快速入門之滾動控制元件RecyclerView

下一篇:C++期末穩過復習無敵知識點1(輸入輸出類、呼叫數學函式類)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more