Android全面解析之Window機制

一隻修仙的猿發表於2020-12-13

前言

你好!
我是一隻修仙的猿,歡迎閱讀我的文章。

Window,讀者可能更多的認識是windows系統的視窗。在windows系統上,我們可以多個視窗同時執行,每個視窗代表著一個應用程式。但在安卓上貌似並沒有這個東西,但讀者可以馬上想到,不是有小視窗模式嗎,像米UI最新的系統,不就是可以隨意建立一個小視窗,然後兩個應用同時操作?是的,那是屬於android中,window的一種表現方式。但是手機螢幕終究不能和電腦相比,因為螢幕太小了,小到只能操作一款應用,多個視窗就顯得非常不習慣,所以Android上關於視窗方面的知識讀者可能接觸不多。那window的意思就只是小米系統中那種小視窗嗎?

當然不是。Android框架層意義上的window和我們認識的window其實是有點不一樣的。我們日常最直觀的,每個應用介面,都有一個應用級的window。再例如popupWindow、Toast、dialog、menu都是需要通過建立window來實現。所以其實window我們一直都見到,只是不知道那就是window。瞭解window的機制原理,可以更好地瞭解window,進而更好地瞭解android是怎麼管理螢幕上的view。這樣,當我們需要使用dialog或者popupWindow的時候,可以懂得他背後究竟做了什麼,才能夠更好的運用dialog、popupWindow等。

當然,到此如果你有很多的疑問,甚至質疑我的理論,那就希望你可以閱讀完這一篇文章。我會從window是什麼,有什麼用,內部機制是什麼,各種元件是如何建立window等等方面來闡述Android中的window。文章內容非常多,讀者可自選章節閱讀。

什麼是window機制

先假設如果沒有window,會發生什麼:

我們看到的介面ui是view,如我們的應用佈局,更簡單是一個button。假如螢幕上現在有一個Button,如圖1,現在往螢幕中間新增一個TextView,那麼最終的結果是圖2,還是圖3:

示例圖

在上圖的圖2中,如果我要實現點選textView執行他的監聽事件邏輯,點選不是textView的區域讓textView消失,需要怎麼實現呢?讀者可能會說,我們可以在Activity中新增這部分的邏輯,那如果我們需要讓一個懸浮窗在所有介面顯示呢,如上文我講到的小米懸浮窗,兩個不用應用的view,怎麼確定他們的顯示次序?又例如我們需要彈出一個dialog來提示使用者,怎麼樣可以讓dialog永遠處於最頂層呢,包括顯示dialog期間應用彈出的如popupWindow必須顯示在dialog的低下,但toast又必須顯示在dialog上面。

很明顯,我們的螢幕可以允許多個應用同時顯示非常多的view,他們的顯示次序或者說顯示高度是不一樣的,如果沒有一個統一的管理者,那麼每一家應用都想要顯示在最頂層,那麼螢幕上的view會非常亂。

同時,當我們點選螢幕時,這個觸控事件應該傳給哪個view?很明顯我們都知道應該傳給最上層的view,但是接受事件的是螢幕,是另一個系統服務,他怎麼知道觸控位置的最上層是哪個view呢?即時知道,他又怎麼把這個事件準確地傳給他呢?

為了解決等等這些問題,急需有一個管理者來統一管理螢幕上的顯示的view,才能讓程式有條不紊地走下去。而這,就是Android中的window機制。

window機制就是為了管理螢幕上的view的顯示以及觸控事件的傳遞問題。

什麼是window?

那什麼是window,在Android的window機制中,每個view樹都可以看成一個window。為什麼不是每個view呢?因為view樹中每個view的顯示次序是固定的,例如我們的Activity佈局,每一個控制元件的顯示都是已經安排好的,對於window機制來說,屬於“不可再分割的view”。

什麼是view樹?例如你在佈局中給Activity設定了一個佈局xml,那麼最頂層的佈局如LinearLayout就是view樹的根,他包含的所有view就都是該view樹的節點,所以這個view樹就對應一個window。

舉幾個具體的例子:

  • 我們在新增dialog的時候,需要給他設定view,那麼這個view他是不屬於antivity的佈局內的,是通過WindowManager新增到螢幕上的,不屬於activity的view樹內,所以這個dialog是一個獨立的view樹,所以他是一個window。
  • popupWindow他也對應一個window,因為它也是通過windowManager新增上去的,不屬於Activity的view樹。
  • 當我們使用使用windowManager在螢幕上新增的任何view都不屬於Activity的佈局view樹,即使是隻新增一個button。

view樹(後面使用view代稱,後面我說的view都是指view樹)是window機制的操作單位,每一個view對應一個window,view是window的存在形式,window是view的載體,我們平時看到的應用介面、dialog、popupWindow以及上面描述的懸浮窗,都是window的表現形式。注意,我們看到的不是window,而是view。window是view的管理者,同時也是view的載體。他是一個抽象的概念,本身並不存在,view是window的表現形式。這裡的不存在,指的是我們在螢幕上是看不到window的,他不像windows系統,如下圖:

windows系統視窗

有一個很明顯的標誌:看,我就是window。但在Android中我們是無法感知的,我們只能看到view無法看到window,window是控制view需要怎麼顯示的管理者。每個成功的男人背後都有一個女人,每個view背後都有一個window。

window本身並不存在,他只是一個概念。舉個例子:如班集體,就是一個概念,他的存在形式是這整個班的學生,當學生不存在那麼這個班集體也就不存在。但是他的好處是得到了一個新的概念,我們可以以班為單位來安排活動。因他不存在,所以也很難從原始碼中找到他的痕跡,window機制的操作單位都是view,如果要說他在原始碼中的存在形式,筆者目前的認知就是在WindowManagerService中每一個view對應一個windowStatus。WindowManagerService是什麼如果沒了解過可以先忽略後面會講到。讀者可以慢慢思考一下這個抽象的概念,後面會慢慢深入講原始碼幫助理解。

  • view是window的存在形式,window是view的載體
  • window是view的管理者,同時也是view的載體。他是一個抽象的概念,本身並不存在,view是window的表現形式

思考:Android中不是有一個抽象類叫做window還有一個PhoneWindow實現類嗎,他們不就是window的存在形式,為什麼說window是抽象不存在的?讀者可自行思考,後面會講到。

Window的相關屬性

在瞭解window的操作流程之前,先補充一下window的相關屬性。

window的type屬性

前面我們講到window機制解決的一個問題就是view的顯示次序問題,這個屬性就決定了window的顯示次序。window是有分類的,不同類別的顯示高度範圍不同,例如我把1-1000m高度稱為低空,1001-2000m高度稱為中空,2000以上稱為高空。window也是一樣按照高度範圍進行分類,他也有一個變數Z-Order,決定了window的高度。window一共可分為三類:

  • 應用程式視窗:應用程式視窗一般位於最底層,Z-Order在1-99
  • 子視窗:子視窗一般是顯示在應用視窗之上,Z-Order在1000-1999
  • 系統級視窗:系統級視窗一般位於最頂層,不會被其他的window遮住,如Toast,Z-Order在2000-2999。如果要彈出自定義系統級視窗需要動態申請許可權

Z-Order越大,window越靠近使用者,也就顯示越高,高度高的window會覆蓋高度低的window。

window的type屬性就是Z-Order的值,我們可以給window的type屬性賦值來決定window的高度。系統為我們三類window都預設了靜態常量,如下(以下常用引數介紹轉自參考文獻第一篇文章):

  • 應用級window

    // 應用程式 Window 的開始值
    public static final int FIRST_APPLICATION_WINDOW = 1;
    
    // 應用程式 Window 的基礎值
    public static final int TYPE_BASE_APPLICATION = 1;
    
    // 普通的應用程式
    public static final int TYPE_APPLICATION = 2;
    
    // 特殊的應用程式視窗,當程式可以顯示 Window 之前使用這個 Window 來顯示一些東西
    public static final int TYPE_APPLICATION_STARTING = 3;
    
    // TYPE_APPLICATION 的變體,在應用程式顯示之前,WindowManager 會等待這個 Window 繪製完畢
    public static final int TYPE_DRAWN_APPLICATION = 4;
    
    // 應用程式 Window 的結束值
    public static final int LAST_APPLICATION_WINDOW = 99;
    
  • 子window

    // 子 Window 型別的開始值
    public static final int FIRST_SUB_WINDOW = 1000;
    
    // 應用程式 Window 頂部的皮膚。這些 Window 出現在其附加 Window 的頂部。
    public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
    
    // 用於顯示媒體(如視訊)的 Window。這些 Window 出現在其附加 Window 的後面。
    public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;
    
    // 應用程式 Window 頂部的子皮膚。這些 Window 出現在其附加 Window 和任何Window的頂部
    public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;
    
    // 當前Window的佈局和頂級Window佈局相同時,不能作為子代的容器
    public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;
    
    // 用顯示媒體 Window 覆蓋頂部的 Window, 這是系統隱藏的 API
    public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW + 4;
    
    // 子皮膚在應用程式Window的頂部,這些Window顯示在其附加Window的頂部, 這是系統隱藏的 API
    public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
    
    // 子 Window 型別的結束值
    public static final int LAST_SUB_WINDOW = 1999;
    
  • 系統級window

    // 系統Window型別的開始值
    public static final int FIRST_SYSTEM_WINDOW = 2000;
    
    // 系統狀態列,只能有一個狀態列,它被放置在螢幕的頂部,所有其他視窗都向下移動
    public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
    
    // 系統搜尋視窗,只能有一個搜尋欄,它被放置在螢幕的頂部
    public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;
    
    // 已經從系統中被移除,可以使用 TYPE_KEYGUARD_DIALOG 代替
    public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;
    
    // 系統對話方塊視窗
    public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;
    
    // 鎖屏時顯示的對話方塊
    public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;
    
    // 輸入法視窗,位於普通 UI 之上,應用程式可重新佈局以免被此視窗覆蓋
    public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11;
    
    // 輸入法對話方塊,顯示於當前輸入法視窗之上
    public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
    
    // 牆紙
    public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13;
    
    // 狀態列的滑動皮膚
    public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;
    
    // 應用程式疊加視窗顯示在所有視窗之上
    public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;
    
    // 系統Window型別的結束值
    public static final int LAST_SYSTEM_WINDOW = 2999;
    

Window的flags引數

flag標誌控制window的東西比較多,很多資料的描述是“控制window的顯示”,但我覺得不夠準確。flag控制的範圍包括了:各種情景下的顯示邏輯(鎖屏,遊戲等)還有觸控事件的處理邏輯。控制顯示確實是他的很大部分功能,但是並不是全部。下面看一下一些常用的flag,就知道flag的功能了(以下常用引數介紹轉自參考文獻第一篇文章):

// 當 Window 可見時允許鎖屏
public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001;

// Window 後面的內容都變暗
public static final int FLAG_DIM_BEHIND = 0x00000002;

// Window 不能獲得輸入焦點,即不接受任何按鍵或按鈕事件,例如該 Window 上 有 EditView,點選 EditView 是 不會彈出軟鍵盤的
// Window 範圍外的事件依舊為原視窗處理;例如點選該視窗外的view,依然會有響應。另外只要設定了此Flag,都將會啟用FLAG_NOT_TOUCH_MODAL
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;

// 設定了該 Flag,將 Window 之外的按鍵事件傳送給後面的 Window 處理, 而自己只會處理 Window 區域內的觸控事件
// Window 之外的 view 也是可以響應 touch 事件。
public static final int FLAG_NOT_TOUCH_MODAL  = 0x00000020;

// 設定了該Flag,表示該 Window 將不會接受任何 touch 事件,例如點選該 Window 不會有響應,只會傳給下面有聚焦的視窗。
public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;

// 只要 Window 可見時螢幕就會一直亮著
public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;

// 允許 Window 佔滿整個螢幕
public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;

// 允許 Window 超過螢幕之外
public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;

// 全屏顯示,隱藏所有的 Window 裝飾,比如在遊戲、播放器中的全屏顯示
public static final int FLAG_FULLSCREEN      = 0x00000400;

// 表示比FLAG_FULLSCREEN低一級,會顯示狀態列
public static final int FLAG_FORCE_NOT_FULLSCREEN   = 0x00000800;

// 當使用者的臉貼近螢幕時(比如打電話),不會去響應此事件
public static final int FLAG_IGNORE_CHEEK_PRESSES    = 0x00008000;

// 則當按鍵動作發生在 Window 之外時,將接收到一個MotionEvent.ACTION_OUTSIDE事件。
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;

@Deprecated
// 視窗可以在鎖屏的 Window 之上顯示, 使用 Activity#setShowWhenLocked(boolean) 方法代替
public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;

// 表示負責繪製系統欄背景。如果設定,系統欄將以透明背景繪製,
// 此 Window 中的相應區域將填充 Window#getStatusBarColor()和 Window#getNavigationBarColor()中指定的顏色。
public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;

// 表示要求系統桌布顯示在該 Window 後面,Window 表面必須是半透明的,才能真正看到它背後的桌布
public static final int FLAG_SHOW_WALLPAPER = 0x00100000;

window的solfInputMode屬性

這一部分就是當軟體盤彈起來的時候,window的處理邏輯,這在日常中也經常遇到,如:我們在微信聊天的時候,點選輸入框,當軟鍵盤彈起來的時候輸入框也會被頂上去。如果你不想被頂上去,也可以設定為被軟鍵盤覆蓋。下面介紹一下常見的屬性(以下常見屬性介紹選自參考文獻第一篇文章):

// 沒有指定狀態,系統會選擇一個合適的狀態或者依賴於主題的配置
public static final int SOFT_INPUT_STATE_UNCHANGED = 1;

// 當使用者進入該視窗時,隱藏軟鍵盤
public static final int SOFT_INPUT_STATE_HIDDEN = 2;

// 當視窗獲取焦點時,隱藏軟鍵盤
public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;

// 當使用者進入視窗時,顯示軟鍵盤
public static final int SOFT_INPUT_STATE_VISIBLE = 4;

// 當視窗獲取焦點時,顯示軟鍵盤
public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;

// window會調整大小以適應軟鍵盤視窗
public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;

// 沒有指定狀態,系統會選擇一個合適的狀態或依賴於主題的設定
public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;

// 當軟鍵盤彈出時,視窗會調整大小,例如點選一個EditView,整個layout都將平移可見且處於軟體盤的上方
// 同樣的該模式不能與SOFT_INPUT_ADJUST_PAN結合使用;
// 如果視窗的佈局引數標誌包含FLAG_FULLSCREEN,則將忽略這個值,視窗不會調整大小,但會保持全屏。
public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;

// 當軟鍵盤彈出時,視窗不需要調整大小, 要確保輸入焦點是可見的,
// 例如有兩個EditView的輸入框,一個為Ev1,一個為Ev2,當你點選Ev1想要輸入資料時,當前的Ev1的輸入框會移到軟鍵盤上方
// 該模式不能與SOFT_INPUT_ADJUST_RESIZE結合使用
public static final int SOFT_INPUT_ADJUST_PAN = 0x20;

// 將不會調整大小,直接覆蓋在window上
public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;

window的其他屬性

上面的三個屬性是window比較重要也是比較複雜 的三個,除此之外還有幾個日常經常使用的屬性:

  • x與y屬性:指定window的位置
  • alpha:window的透明度
  • gravity:window在螢幕中的位置,使用的是Gravity類的常量
  • format:window的畫素點格式,值定義在PixelFormat中

如何給window屬性賦值

window屬性的常量值大部分儲存在WindowManager.LayoutParams類中,我們可以通過這個類來獲得這些常量。當然還有Gravity類和PixelFormat類等。

一般情況下我們會通過以下方式來往螢幕中新增一個window:

// 在Activity中呼叫
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
windParams.flags = WindowManager.LayoutParams.FLAG_FULLSCREEN;
TextView view = new TextView(this);
getWindowManager.addview(view,windowParams);

我們可以直接給WindowManager.LayoutParams物件設定屬性。

第二種賦值方法是直接給window賦值,如

getWindow().flags = WindowManager.LayoutParams.FLAG_FULLSCREEN;

除此之外,window的solfInputMode屬性比較特殊,他可以直接在AndroidManifest中指定,如下:

 <activity android:windowSoftInputMode="adjustNothing" />

最後總結一下:

  • window的重要屬性有type、flags、solfInputMode、gravity等
  • 我們可以通過不同的方式給window屬性賦值
  • 沒必要去全部記下來,等遇到需求再去尋找對應的常量即可

Window的新增過程

通過理解原始碼之後,可以對之前的理論理解更加的透徹。window的新增過程,指的是我們通過WindowManagerImpl的addView方法來新增window的過程。

想要新增一個window,我們知道首先得有view和WindowManager.LayoutParams物件,才能去建立一個window,這是我們常見的程式碼:

Button button = new Button(this);
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
// 這裡對windowParam進行初始化
windowParam.addFlags...
// 獲得應用PhoneWindow的WindowManager物件進行新增window
getWindowManager.addView(button,windowParams);

然後接下來我們進入addView方法中看看。我們知道這個windowManager的實現類是WindowManagerImpl,上面講過,進入他的addView方法看一看:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

可以發現他把邏輯直接交給mGlobal去處理了。這個mGlobal是WindowManagerGlobal,是一個全域性單例,是WindowManager介面的具體邏輯實現。這裡運用的是橋接模式。那我們進WindowManagerGlobal的方法看一下:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    // 首先判斷引數是否合法
    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;
    // 如果不是子視窗,會對其做引數的調整
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } else {
        final Context context = view.getContext();
        if (context != null
                && (context.getApplicationInfo().flags
                        & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
            wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }
    }
    
	synchronized (mLock) {
        ...
        // 這裡新建了一個viewRootImpl,並設定引數
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);

        // 新增到windowManagerGlobal的三個重要list中,後面會講到
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // 最後通過viewRootImpl來新增window
        try {
            root.setView(view, wparams, panelParentView);
        } 
        ...
    }  
}

程式碼有點長,一步步看:

  • 首先對引數的合法性進行檢查
  • 然後判斷該視窗是不是子視窗,如果是的話需要對視窗進行調整,這個好理解,子視窗要跟隨父視窗的特性。
  • 接著新建viewRootImpl物件,並把view、viewRootImpl、params三個物件新增到三個list中進行儲存
  • 最後通過viewRootImpl來進行新增

補充一點關於WindowManagerGlobal中的三個list,他們分別是:

private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
     new ArrayList<WindowManager.LayoutParams>();

每一個window所對應的這三個物件都會儲存在這裡,之後對window的一些操作就可以直接來這裡取物件了。當window被刪除的時候,這些物件也會被從list中移除。

可以看到新增的window的邏輯就交給ViewRootImpl了。viewRootImpl是window和view之間的橋樑,viewRootImpl可以處理兩邊的物件,然後聯結起來。下面看一下viewRootImpl怎麼處理:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        ...
        try {
            mOrigWindowType = mWindowAttributes.type;
            mAttachInfo.mRecomputeGlobalAttributes = true;
            collectViewAttributes();
            // 這裡呼叫了windowSession的方法,呼叫wms的方法,把新增window的邏輯交給wms
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                    getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                    mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                    mTempInsets);
            setFrame(mTmpFrame);
        } 
        ...
    }
}

viewRootImpl的邏輯很多,重要的就是呼叫了mWindowSession的方法呼叫了WMS的方法。這個mWindowSession很重要重點講一下。

mWindowSession是一個IWindowSession物件,看到這個命名很快地可以像到這裡用了AIDL跨程式通訊。IWindowSession是一個IBinder介面,他的具體實現類在WindowManagerService,本地的mWindowSession只是一個Binder物件,通過這個mWindowSession就可以直接呼叫WMS的方法進行跨程式通訊。

那這個mWindowSession是從哪裡來的呢?我們到viewRootImpl的構造器方法中看一下:

public ViewRootImpl(Context context, Display display) {
	...
 	mWindowSession = WindowManagerGlobal.getWindowSession();
 	...
}

可以看到這個session物件是來自WindowManagerGlobal。再深入看一下:

public static IWindowSession getWindowSession() {
 synchronized (WindowManagerGlobal.class) {
     if (sWindowSession == null) {
         try {
             ...
             sWindowSession = windowManager.openSession(
                     new IWindowSessionCallback.Stub() {
                         ...
                     });
         } 
         ...
     }
     return sWindowSession;
 }
}

這熟悉的程式碼格式,可以看出來這個session是一個單例,也就是整個應用的所有viewRootImpl的windowSession都是同一個,也就是一個應用只有一個windowSession。對於wms而言,他是服務於多個應用的,如果說每個viewRootImpl整一個session,那他的任務就太重了。WMS的物件單位是應用,他在內部給每個應用session分配了一些資料結構如list,用於儲存每個應用的window以及對應的viewRootImpl。當需要操作view的時候,通過session直接找到viewRootImpl就可以操作了。

後面的邏輯就交給WMS去處理了,WMS就會建立window,然後結合引數計算window的高度等等,最後使用viewRootImpl進行繪製。這後面的程式碼邏輯就不講了,這是深入到WMS的內容,再講進去就太複雜了(筆者也還沒讀懂WMS)。讀原始碼的目的是瞭解整個系統的本質與工作流程,對系統整體的感知,而不用太深入程式碼細節,Android系統那麼多的程式碼,如果深入進去會出不來的,所以點到為止就好了。

我們知道windowManager介面是繼承viewManager介面的,viewManager還有另外兩個介面:removeView、updateView。這裡就不講了,有興趣的讀者可以自己去閱讀原始碼。講新增流程主要是為了理解window系統的運作,對內部的流程感知,以便於更好的理解window。

最後做個總結:

window的新增過程是通過PhoneWindow對應的WindowManagerImpl來新增window,內部會呼叫WindowManagerGlobal來實現。WindowManagerGlobal會使用viewRootImpl來進行跨程式通訊讓WMS執行建立window的業務。

每個應用都有一個windowSession,用於負責和WMS的通訊,如ApplicationThread與AMS的通訊。

window機制的關鍵類

前面的原始碼流程中涉及到很多的類,這裡把相關的類統一分析一下。先看一張圖:

window內部關鍵類

這基本上是我們這篇文章涉及到的所有關鍵類。且聽我慢慢講。(圖中綠色的window並不是一個類,而是真正意義上的window)

window相關

window的實現類只有一個:PhoneWindow,他繼承自Window抽象類。後面我會重點分析他。

WindowManager相關

顧名思義,windowManager就是window管理類。這一部分的關鍵類有windowManager,viewManager,windowManagerImpl,windowManagerGlobal。windowManager是一個介面,繼承自viewManager。viewManager中包含了我們非常熟悉的三個介面:addView,removeView,updateView
windowManagerImpl和PhoneWindow是成對出現的,前者負責管理後者。WindowManagerImpl是windowManager的實現類,但是他本身並沒有真正實現邏輯,而是交給了WindowManagerGlobal。WindowManagerGlobal是全域性單例,windowManagerImpl內部使用橋接模式,他是windowManager介面邏輯的真正實現

view相關

這裡有個很關鍵的類:ViewRootImpl。每個view樹都會有一個。當我使用windowManager的addView方法時,就會建立一個ViewRootImpl。ViewRootImpl的作用很關鍵:

  • 負責連線view和window的橋樑事務
  • 負責和WindowManagerService的聯絡
  • 負責管理和繪製view樹
  • 事件的中轉站

每個window都會有一個ViewRootImpl,viewRootImpl是負責繪製這個view樹和window與view的橋樑,每個window都會有一個ViewRootImpl。

WindowManagerService

這個是window的真正管理者,類似於AMS(ActivityManagerService)管理四大元件。所有的window建立最終都要經過windowManagerService。整個Android的window機制中,WMS絕對是核心,他決定了螢幕所有的window該如何顯示如何分發點選事件等等。

window與PhoneWindow的關係

解釋一下標題,window是指window機制中window這個概念,而PhoneWindow是指PhoneWindow這個類。後面我在講的時候,如果是指類,我會在後面加個‘類’字。如window是指window概念,window類是指window這個抽象類。讀者不要混淆。

還記得我在講window的概念的時候留了一個思考嗎?

思考:Android中不是有一個抽象類叫做window還有一個PhoneWindow實現類嗎,他們不就是window的存在形式,為什麼說window是抽象不存在的

這裡我再丟擲幾個問題:

  • 有一些資料認為PhoneWindow就是window,是view容器,負責管理容器內的view,windowManagerImpl可以往裡面新增view,如上面我們講過的addView方法。但是,同時它又說每個window對應一個viewRootImpl,但卻沒解釋為什麼每次addView都會新建一個viewRootImpl,前後傳送矛盾。
  • 有一些資料也是認為PhoneWindow是window,但是他說addView方法不是新增view而是新增window,同時拿這個方法的名字作為論據證明view就是window,但是他沒解釋為什麼在使用addView方法建立window的過程卻沒有建立PhoneWindow物件。

我們一步步來看。我們首先來看一下原始碼中對於window抽象類的註釋:

 Abstract base class for a top-level window look and behavior policy.  An
 instance of this class should be used as the top-level view added to the
 window manager. It provides standard UI policies such as a background, title
 area, default key processing, etc.
     
頂層視窗外觀和行為策略的抽象基類。此類的例項應用作新增到視窗管理器的頂層檢視。
它提供標準的UI策略,如背景、標題區域、預設鍵處理等。

大概意思就是:這個類是頂級視窗的抽象基類,頂級視窗必須繼承他,他負責視窗的外觀如背景、標題、預設按鍵處理等。這個類的例項被新增到windowManager中,讓windowManager對他進行管理。PhoneWindow是一個top-level window(頂級視窗),他被新增到頂級視窗管理器的頂層檢視,其他的window,都需要新增到這個頂層檢視中,所以更準確的來說,PhoneWindow並不是view容器,而是window容器。

那PhoneWindow的存在意義是什麼?

第一、提供DecorView模板。如下圖:

Android全面解析之Window機制

我們的Activity是通過setContentView把佈局設定到DecorView中,那麼DecorView本身的佈局,就成為了Activity介面的背景。同時DecorView是分為標題欄和內容兩部分,所以也可以可介面設定標題欄。同時,由於我們的介面是新增在的DecorView中,屬於DecorView的一部分。那麼對於DecorView的window屬性設定也會對我們的佈局介面生效。還記得谷歌的官方給window類註釋的最後一句話嗎:它提供標準的UI策略,如背景、標題區域、預設鍵處理等。這些都可以通過DecorView實現,這是PhoneWindow的第一個作用。

第二、抽離Activity中關於window的邏輯。Activity的職責非常多,如果所有的事情都自己做,那麼會造成本身程式碼極其臃腫。閱讀過Activity啟動的讀者可能知道,AMS也通過ActivityStarter這個類來抽離啟動Activity啟動的邏輯。這樣關於window相關的事情,就交給PhoneWindow去處理了。(事實上,Activity呼叫的是WindowManagerImpl,但因PhoneWindow和WindowManagerImpl兩者是成對存在,他們共同處理window相關的事務,所以這裡就簡單寫成交給PhoneWindow處理。)當Activity需要新增介面時,只需要一句setContentView,呼叫了PhoneWindow的setContentView方法,就把佈局設定到螢幕上了。具體怎麼完成,Activity不必管。

第三、限制元件新增window的許可權。PhoneWindow內部有一個token屬性,用於驗證一個PhoneWindow是否允許新增window。在Activity建立PhoneWindow的時候,就會把從AMS傳過來的token賦值給他,從而他也就有了新增token的許可權。而其他的PhoneWindow則沒有這個許可權,因而也無法新增window。這部分內容我在另一篇文章有詳細講解,感興趣的讀者可以前往瞭解一下傳送門

當然,PhoneWindow的作用肯定遠不止如此,這裡列出很重要的三條,也是筆者目前學習到的三個最重要的作用。官方對於一個類的設計的考慮肯定是非常多,不是筆者簡單的分析所能闡述,而只是給出一個新的思考方向,帶大家認識真正的window。

總結一下:

  • PhoneWindow本身不是真正意義上的window,他更多可以認為是輔助Activity操作window的工具類。
  • windowManagerImpl並不是管理window的類,而是管理PhoneWindow的類。真正管理window的是WMS。
  • PhoneWindow可以配合DecorView可以給其中的window按照一定的邏輯提供標準的UI策略
  • PhoneWindow限制了不同的元件新增window的許可權。

常見元件的window建立流程

上面講的是通過windowManagerImpl建立window的過程,我們通過前面的講解了解到,WindowManagerImpl是管理PhoneWindow的,他們是同時出現的。因而有兩種建立window的方式:

  • 已經存在PhoneWindow,直接通過WindowManagerImpl建立window
  • PhoneWindow尚未存在,先建立PhoneWindow,再利用windowManagerImpl來建立window

當我們在Activity中使用getWindowManager方法獲取到的就是應用的PhoneWindow對應的WindowManagerImpl。下面來講一下不同的元件是如何建立window的,

Activity

如果有閱讀過Activity的啟動流程的讀者,會知道Activity的啟動最後來到了ActivityThread的handleLaunchActivity這個方法。

關於Activity的啟動流程,我寫過一篇文章,有興趣的讀者可以點選下方連結前往:

Activity啟動流程詳解(基於api28)

至於為什麼是這個方法這裡就不講了,有興趣的讀者可以去看上面的文章。我們直接來看這個方法的程式碼:

public void handleLaunchActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    ...;
    // 這裡對WindowManagerGlobal進行初始化
    WindowManagerGlobal.initialize();

   	// 啟動Activity並回撥activity的onCreate方法
    final Activity a = performLaunchActivity(r, customIntent);
    ...
}


private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    try {
        // 這裡建立Application
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
		...
        if (activity != null) {
            ...
            Window window = null;
            if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                window = r.mPendingRemoveWindow;
                r.mPendingRemoveWindow = null;
                r.mPendingRemoveWindowManager = null;
            }
            appContext.setOuterContext(activity);
            // 這裡將window作為引數傳到activity的attach方法中
            // 一般情況下這裡window==null
            activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config,
                    r.referrer, r.voiceInteractor, window, r.configCallback,
                    r.assistToken);  
            ...
            // 最後這裡回撥Activity的onCreate方法
            if (r.isPersistable()) {
                mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }
        }
    
    ...
}

handleLaunchActivity的程式碼中首先對WindowManagerGlobal進行初始化,然後呼叫了performLaunchActivity方法。程式碼很多,這裡只擷取了重要部分。首先會建立Application物件,然後再呼叫Activity的attach方法,把window作為引數傳進去,最後回撥activity的onCreate方法。所以這裡最有可能建立window的方法就是Activity的attach方法了。我們進去看一下:

final void attach(...,Context context,Window window, ...) {
    ...;
 	// 這裡新建PhoneWindow物件,並對window進行初始化
	mWindow = new PhoneWindow(this, window, activityConfigCallback);
    // Activity實現window的callBack介面,把自己設定給window
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);    
    ...
    // 這裡初始化window的WindowManager物件
	mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);        
}

同樣只擷取了重要程式碼,attach方法引數非常多,我只留下了window相關的引數。在這方法裡首先利用傳進來的window建立了PhoneWindow。Activity實現window的callBack介面,可以把自己設定給window當觀察者。當window發生變化的時候可以通知activity。然後再建立WindowManager和PhoneWindow繫結在一起,這樣我們就可以通過windowManager操作PhoneWindow了。(這裡不是setWindowManager嗎,windowManager是什麼時候建立的?)我們進去setWindowManager方法看一下:

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    mAppToken = appToken;
    mAppName = appName;
    mHardwareAccelerated = hardwareAccelerated;
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    // 這裡建立了windowManager
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

這個方法裡首先會獲取到應用服務的WindowManager(實現類也是WindowManagerImpl),然後通過這個應用服務的WindowManager建立了新的windowManager。

從這裡可以看到是利用系統服務的windowManager來建立新的windowManagerImpl,因而這個應用所有的WindowManagerImpl都是同個核心windowManager,而建立出來的僅僅是包了個殼。

這樣PhoneWindow和WindowManagerImpl就繫結在一起了。Activity可以通過WindowManagerImpl來操作PhoneWindow。


到這裡Activity的PhoneWindow和WindowManagerImpl物件就建立完成了,接下來是如何把Activity的佈局檔案設定給PhoneWindow。在上面我講到呼叫Activity的attach方法之後,會回撥Activity的onCreate方法,在onCreate方法我們會呼叫setContentView來設定佈局,如下:

public void setContentView(View view, ViewGroup.LayoutParams params) {
    getWindow().setContentView(view, params);
    initWindowDecorActionBar();
}

這裡的getWindow就是獲取到我們上面建立的PhoneWindow物件。我們繼續看下去:

// 注意他有多個過載的方法,要選擇引數對應的方法
public void setContentView(int layoutResID) {
    // 建立DecorView
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 這裡根據佈局id載入佈局
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        // 回撥activity的方法
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

同樣我們只看重點程式碼:

  • 首先看decorView建立了沒有,沒有的話建立DecorView
  • 把佈局載入到DecorView中
  • 回撥Activity的callBack方法

這裡補充一下什麼是DecorView。DecorView是在PhoneWindow中預設好的一個佈局,這個佈局長這樣:

decorView

他是一個垂直排列的佈局,上面是ActionBar,下面是ContentView,他是一個FrameLayout。我們的Activity佈局就載入到ContentView裡進行顯示。所以Decorview是Activity佈局最頂層的viewGroup。

然後我們看一下怎麼初始化DercorView的:

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        // 這裡建立了DecorView
        mDecor = generateDecor(-1);
        ...
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        // 對DecorView進行初始化,得到ContentView
        mContentParent = generateLayout(mDecor);
        ...
    }
}

installDecor方法中主要是新建一個DecorView物件,然後載入預設好的佈局對DecorView進行初始化,(預設好的佈局就是上面講述的佈局)並獲取到這個預設佈局的ContentView。好了然後我們再回到window的setContentView方法中,初始化了DecorView之後,把Activity佈局載入到DecorView的ContentView中如下程式碼:

// 注意他有多個過載的方法,要選擇引數對應的方法
public void setContentView(int layoutResID) {
    ...
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 這裡根據佈局id載入佈局
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    ...
   	mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        // 回撥activity的方法
        cb.onContentChanged();
    }
}

所以可以看到Activitiy的佈局確實是新增到DecorView的ContentView中,這也是為什麼onCreate中使用的是setContentView而不是setView。最後會回撥Activity的方法告訴Activity,DecorView已經建立並初始化完成了。


到這裡DecorView建立完成了,但還缺少了最重要的一步:把DecorView作為window新增到螢幕上。從前面的介紹我們知道新增window需要用到WindowManagerImpl的addView方法。這一步是在ActivityThread的handleResumeActivity方法中被執行:

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    // 呼叫Activity的onResume方法
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    ...
    // 讓decorView顯示到螢幕上
	if (r.activity.mVisibleFromClient) {
        r.activity.makeVisible();
  	}

這一步方法有兩個重點:回撥onResume方法,把decorView新增到螢幕上。我們看一下makeVisible方法做了什麼:

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

是不是非常熟悉?直接呼叫WindowManagerImpl的addView方法來吧decorView新增到螢幕上,至此,我們的Activity介面就會顯示在螢幕上了。


好了,這部分很長,最後來總結一下:

  • 從Activity的啟動流程可以得到Activity建立Window的過程
  • 建立PhoneWindow -> 建立WindowManager -> 建立decorView -> 利用windowManager把DecorView顯示到螢幕上
  • 回撥onResume方法的時候,DecorView還沒有被新增到螢幕,所以當onResume被回撥,指的是螢幕即將到顯示,而不是已經顯示

popupWindow日常使用的也比較多,最常見的需求是彈一個選單出來等。popupWindow也是利用windowManager來往螢幕上新增window,但,popupWindow是依附於activity而存在的,當Activity未執行時,是無法彈出popupWindow的,通過原始碼可以知道,當呼叫onResume方法的時候,其實後續還有很多事情在做,這個時候Activity也是尚未完全啟動,所以popupWindow不能在onCreate、onStart、onResume方法中彈出。

彈出popupWindow的過程分為兩個:建立view;通過windowManager新增window。首先看到PopupWindow的構造方法:

public PopupWindow(View contentView, int width, int height, boolean focusable) {
    if (contentView != null) {
        mContext = contentView.getContext();
        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }

    setContentView(contentView);
    setWidth(width);
    setHeight(height);
    setFocusable(focusable);
}

他有多個過載方法,但最終都會呼叫到這個有四個引數的方法。主要是前面的得到context和根據context獲得WindowManager。


然後我們看到他的顯示方法。顯示方法有兩個:showAtLocationshowAsDropDown。主要是處理顯示的位置不同,其他都是相似的。我們看到第一個方法:

public void showAtLocation(View parent, int gravity, int x, int y) {
    mParentRootView = new WeakReference<>(parent.getRootView());
    showAtLocation(parent.getWindowToken(), gravity, x, y);
}

邏輯很簡單,父view的根佈局儲存了起來,然後呼叫另外的過載方法:

public void showAtLocation(IBinder token, int gravity, int x, int y) {
    // 如果contentView是空直接返回
    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);

    p.x = x;
    p.y = y;
	// 執行popupWindow顯示工作
    invokePopup(p);
}

這個方法的邏輯主要有:

  • 判斷contentView是否為空或者是否進行顯示
  • 做一些準備工作
  • 進行popupWindow顯示工作

這裡我們看一下他的準備工作做了什麼:

private void preparePopup(WindowManager.LayoutParams p) {
    ...
        
    if (mBackground != null) {
        mBackgroundView = createBackgroundView(mContentView);
        mBackgroundView.setBackground(mBackground);
    } else {
        mBackgroundView = mContentView;
    }
	// 建立了DecorView
    // 注意,這裡的DecorView並不是我們之前講的DecorView,而是他的內部類:PopupDecorView
    mDecorView = createDecorView(mBackgroundView);
    mDecorView.setIsRootNamespace(true);

    ...
}

接下來再看他的顯示工作:

private void invokePopup(WindowManager.LayoutParams p) {
    ...
   	// 呼叫windowManager新增window
    mWindowManager.addView(decorView, p);

    ...
}

到這裡popupWindow就會被新增到螢幕上了。


最後總結一下:

  • 根據引數構建popupDecorView
  • 把popupDecorView新增到螢幕上

Dialog

dialog的建立過程Activity比較像:建立PhoneWindow,初始化DecorView,新增DecorView。我這裡就簡單講解一下。首先看到他的構造方法:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    ...
    // 獲取windowManager
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
	// 構造PhoneWindow
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    // 初始化PhoneWindow
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setOnWindowSwipeDismissedCallback(() -> {
        if (mCancelable) {
            cancel();
        }
    });
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);
    mListenersHandler = new ListenersHandler(this);
}

這裡和前面的Activity建立過程非常像,但是有個重點需要注意mWindowManager其實是Activity的WindowManager,這裡的context一般是activity(實際上也只能是activity,非activity會丟擲異常,相關內容讀者有興趣可以閱讀這篇文章window的token驗證),我們看到activity的getSystemService方法:

public Object getSystemService(@ServiceName @NonNull String name) {
    if (getBaseContext() == null) {
        throw new IllegalStateException(
                "System services not available to Activities before onCreate()");
    }
	// 獲取activity的windowManager
    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}

可以看到這裡的windowManager確實是Activity的WindowManager。接下來看到他的show方法:

public void show() {
   ...
    // 回撥onStart方法,獲取前面初始化好的decorview
    onStart();
    mDecor = mWindow.getDecorView();
    ...
    WindowManager.LayoutParams l = mWindow.getAttributes();
    ...
    // 利用windowManager來新增window    
    mWindowManager.addView(mDecor, l);
    ...
    mShowing = true;
    sendShowMessage();
}

注意這裡的mWindowManager是Activity的WindowManager,所以實際上,這裡是新增到了Activity的PhoneWindow中。接下來的和前面的新增流程一樣,這裡我也不多講解了。


總結一下:

  • dialog和popupWindow不同,dialog建立了新的PhoneWindow,使用了PhoneWindow的DecorView模板。而popupWindow沒有
  • dialog的顯示層級數更高,會直接顯示在Activity上面,在dialog後新增的popUpWindow也會顯示在dialog下
  • dialog的建立流程和activity非常像

從Android架構角度看Window

前面我們介紹過關於PhoneWindow和window之間的關係,瞭解到PhoneWindow其實不是Window,只是一個window容器。不知讀者有沒想過一個問題,為什麼谷歌要建一個不是window但卻名字是window的類?是故意要迷惑我們嗎?要了解這個問題,我們先來回顧一下整個android的window機制結構。

首先從WindowManagerService開始,我們知道WMS是window的最終管理者,在WMS中為每一個應用持有一個session,關於session前面我們講過,每個應用都是全域性單例,負責和WMS通訊的binder物件。WMS為每個window都建立了一個windowStatus物件,同一個應用的window使用同個session進行跨程式通訊,結構大概如下:

WMS結構

而負責與WMS通訊的,是viewRootImpl。前面我們講過每個view樹即為一個window,viewRootImpl負責和WMS進行通訊,同時也負責view的繪製。如果把上面的圖畫仔細一點就是:

更詳細的結構圖

圖中每一個windowStatus對應一個viewRootImpl,WMS通過viewRootImpl來控制view。這也就是window機制的管理結構。當我們需要新增window的時候,最終的邏輯實現是WindowManagerGlobal,他的內部使用自己的session建立一個viewRootImpl,然後向WMS申請新增window,結構圖大概如下:

window的新增結構

windowManagerGlobal使用自己的IWindowSession建立viewRootImpl,這個IWindowSession是全域性單例。viewRootImpl和WMS申請建立window,然後WMS允許之後,再通知viewRootImpl繪製view,同時WMS通過windowStatus儲存了viewRootImpl的相關資訊,這樣如果WMS需要修改view,直接通過viewRootImpl就可以修改view了。


從上面的描述中可以發現我全程沒有提及到PhoneWindow和WindowManagerImpl。這是因為他們不屬於window機制內的類,而是封裝於window機制之上的框架。假設如果沒有PhoneWindow和WindowManager我們該如何新增一個window?首先需要呼叫WindowGlobal獲取session,再建立viewRootImpl,再訪問wms,然後再利用viewRootImpl繪製view,是不是很複雜,而這僅僅只是整體的步驟。而WindowManagerImpl正是這個功能。他內部擁有WindowManagerGlobal的單例,然後幫助我們完成了這一系列的步驟。同時,windowManagerImpl也是隻有一個例項,其他的windowManagerImpl都是建立在windowManagerImpl單例上。這一點在前面有通過原始碼介紹到。

另外,上面我講到PhoneWindow並不是window而是一個輔助Activity管理的工具類,那為什麼他不要命名為windowUtils呢?首先,PhoneWindow這個類是谷歌給window機制進行更上一層的封裝。PhoneWindow內部擁有一個DecorView,我們的佈局view都是新增到decorView中的,因為我們可以通過給decorView設定背景,寬高度,標題欄,按鍵反饋等等,來間接給我們的佈局view設定。這樣一來,PhoneWindow的存在,向開發者遮蔽真正的window,暴露給開發者一個“存在的”window。我們可以認為PhoneWindow就是一個window,window是view容器。當我們需要在螢幕上新增view的時候,只需要獲得應用window對應的windowManagerImpl,然後直接呼叫addView方法新增view即可。這裡也可以解釋為什麼windowManager的介面方法是addView而不是addWindow,一是window確實是以view的存在形式沒錯,二是為了向開發者遮蔽真正的window,讓我們以為是在往window中新增view,window是真實存在的東西。他們的關係畫個圖如下:

window整體結構

黃色部分輸於谷歌提供給開發者的window框架,而綠色是真正的window機制結構。通過PhoneWindow我們可以很方便地進行window操作,而不須瞭解底層究竟是如何工作的。PhoneWindow的存在,更是讓window的“可見性”得到了實現,讓window變成了一個“view容器”。

好了最後來總結一下:

  • Android內部的window機制與谷歌暴露給我們的api是不一樣的,谷歌封裝的目的是為了讓我們更好地使用window。
  • dialog、popupWindow等框架更是對具體場景進行更進一步的封裝。
  • 我們在瞭解window機制的時候,需要跳過應用層,看到window的本質,才能更好地幫助我們理解window。
  • 在android的其他地方也是一樣,利用封裝向開發者遮蔽底層邏輯,讓我們更好地運用。但如果我們需要了解他的機制的時候,就需要繞過這層封裝,看到本質。

總結

全文到這裡,就基本結束了。下面先總結一下我這篇文章說了什麼:

  • 詳述了什麼是window
  • 對window的各種引數進行講解
  • 講解window機制內的關鍵類
  • 從原始碼講解window的新增流程以及各大元件的window新增流程
  • 詳解了PhoneWindow與window的關係,談了關於谷歌的封裝思想

文中最重要的一點就是認識window的本質,區分好window和view之間的關係以及window與PhoneWindow的關係。

筆者在寫這篇文章的時候,對於各節的安排是比較猶豫的:如果先講概念,沒有原始碼流程的講解很難懂;先講原始碼流程,沒有概念的認知很難讀懂原始碼。最終還是決定了先講window的真正概念,先讓讀者有個整體上的感知。

文章很長,筆者對於window想要講的都在這篇文章中。

希望文章對你有幫助。

全文到此,感謝你的閱讀

原創不易,覺得有幫助可以點贊收藏評論轉發關注。
筆者才疏學淺,有任何錯誤歡迎評論區或私信交流。
如需轉載請私信或評論區交流。

另外歡迎光臨筆者的個人部落格:傳送門

參考文獻

《Android開發藝術探索》

《Android進階解密》

相關文章