主頁 > 移動端開發 > Android全面決議之Window機制

Android全面決議之Window機制

2020-12-14 07:09:36 移動端開發

前言

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

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模板,如下圖:

我們的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 中的 Window
  • 直面底層:WindowManager 視圖系結以及體系結構
  • 奇妙的Window之旅
  • Android進階必備,Window機制探索

《Android開發藝術探索》

《Android進階解密》

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

標籤:Android

上一篇:Qt6 如何使用QRegExp,QTextCodec類?[

下一篇:Dart異步Future與事件回圈Event Loop

標籤雲
其他(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