帶你徹底理解 Android 中的 Window 和 WindowManager

王英豪_發表於2017-04-16

有時候我們需要在桌面上顯示一個類似懸浮窗的東西,這種效果就需要用 Window 來實現,Window 是一個抽象類,表示一個視窗,它的具體實現類是 PhoneWindow,實現位於 WindowManagerService 中。相信看到 WindowManagerService 你會有點眼熟,剛接觸 Android 時幾乎所有人都看到過這樣一張圖:

這裡寫圖片描述

WindowManagerService

WindowManagerService 就是位於 Framework 層的視窗管理服務,它的職責就是管理系統中的所有視窗。視窗的本質是什麼呢?其實就是一塊顯示區域,在 Android 中就是繪製的畫布:Surface,當一塊 Surface 顯示在螢幕上時,就是使用者所看到的視窗了。WindowManagerService 新增一個視窗的過程,其實就是 WindowManagerService 為其分配一塊 Surface 的過程,一塊塊的 Surface 在 WindowManagerService 的管理下有序的排列在螢幕上,Android 才得以呈現出多姿多彩的介面。於是根據對 Surface 的操作型別可以將 Android 的顯示系統分為三個層次,如下圖:

這裡寫圖片描述

一般的開發過程中,我們操作的是 UI 框架層,對 Window 的操作通過 WindowManager 即可完成,而 WindowManagerService 作為系統級服務執行在一個單獨的程式,所以 WindowManager 和 WindowManagerService 的互動是一個 IPC 過程。

Window 分類

Window 有三種型別,分別是應用 Window子 Window 和系統 Window。應用類 Window 對應一個 Acitivity,子 Window 不能單獨存在,需要依附在特定的父 Window 中,比如常見的一些 Dialog 就是一個子 Window。系統 Window是需要宣告許可權才能建立的 Window,比如 Toast 和系統狀態列都是系統 Window。

Window 是分層的,每個 Window 都有對應的 z-ordered,層級大的會覆蓋在層級小的 Window 上面,這和 HTML 中的 z-index 概念是完全一致的。在三種 Window 中,應用 Window 層級範圍是 1~99,子 Window 層級範圍是 1000~1999,系統 Window 層級範圍是 2000~2999,我們可以用一個表格來直觀的表示:

Window 層級
應用 Window 1~99
子 Window 1000~1999
系統 Window 2000~2999

這些層級範圍對應著 WindowManager.LayoutParams 的 type 引數,如果想要 Window 位於所有 Window 的最頂層,那麼採用較大的層級即可,很顯然系統 Window 的層級是最大的,當我們採用系統層級時,需要宣告許可權。

WindowManager 使用

我們對 Window 的操作是通過 WindowManager 來完成的,WindowManager 是一個介面,它繼承自只有三個方法的 ViewManager 介面:

public interface ViewManager{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

這三個方法其實就是 WindowManager 對外提供的主要功能,即新增 View、更新 View 和刪除 View。接下來來看一個通過 WindowManager 新增 Window 的例子,程式碼如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Button floatingButton = new Button(this);
        floatingButton.setText("button");
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                0, 0,
                PixelFormat.TRANSPARENT
        );
        // flag 設定 Window 屬性
        layoutParams.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
        // type 設定 Window 類別(層級)
        layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
        layoutParams.gravity = Gravity.CENTER;
        WindowManager windowManager = getWindowManager();
        windowManager.addView(floatingButton, layoutParams);

    }
}

程式碼中並沒有呼叫 Activity 的 setContentView 方法,而是直接通過 WindowManager 新增 Window,其中設定為系統 Window,所以應該新增許可權:

 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

效果如下:

這裡寫圖片描述

第二個介面是鎖屏介面,由於按鈕是處於較大層級的系統 Window 中的,所以可以看到 button。

WindowManager 的內部機制

在實際使用中無法直接訪問 Window,對 Window 的訪問必須通過 WindowManager。WindowManager 提供的三個介面方法 addView、updateViewLayout 以及 removeView 都是針對 View 的,這說明 View 才是 Window 存在的實體,上面例子實現了 Window 的新增,WindowManager 是一個介面,它的真正實現是 WindowManagerImpl 類:

        @Override
        public void addView(View view, ViewGroup.LayoutParams params){
            mGlobal.addView(view, params, mDisplay, mParentWindow);
        }

        @Override
        public void updateViewLayout(View view, ViewGroup.LayoutParams params){
            mGlobal.updateViewLayout(view, params);
        }

        @Override
        public void removeView(View view){
            mGlobal.removeView(view, false);
        }

可以看到,WindowManagerImpl 並沒有直接實現 Window 的三大操作,而是交給了 WindowManagerGlobal 來處理,下面以 addView 為例,分析一下 WindowManagerGlobal 中的實現過程:

1、檢查引數合法性,如果是子 Window 做適當調整

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

2、建立 ViewRootImpl 並將 View 新增到集合中

在 WindowManagerGlobal 內部有如下幾個集合比較重要:

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>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();

其中 mViews 儲存的是所有 Window 所對應的 View,mRoots 儲存的是所有 Window 所對應的 ViewRootImpl,mParams 儲存的是所有 Window 所對應的佈局引數,mDyingViews 儲存了那些正在被刪除的 View 物件,或者說是那些已經呼叫了 removeView 方法但是操作刪除還未完成的 Window 物件,可以通過表格直觀的表示:

集合 儲存內容
mViews Window 所對應的 View
mRoots Window 所對應的 ViewRootImpl
mParams Window 所對應的佈局引數
mDyingViews 正在被刪除的 View 物件

addView 操作時會將相關物件新增到對應集合中:

root = new ViewRootImpl(view.getContext(),display);
view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

3、通過 ViewRootImpl 來更新介面並完成 Window 的新增過程

在學習 View 的工作原理時,我們知道 View 的繪製過程是由 ViewRootImpl 來完成的,這裡當然也不例外,具體是通過 ViewRootImpl 的 setView 方法來實現的。在 setView 內部會通過 requestLayout 來完成非同步重新整理請求,如下:

public void requestLayout(){
   if(!mHandingLayoutInLayoutRequest){
       checkThread();
       mLayoutRequested = true;
       scheduleTraversals();
   }
}

可以看到 scheduleTraversals 方法是 View 繪製的入口,繼續檢視它的實現:

res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), 
          mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mInputChannel);

mWindowSession 的型別是 IWindowSession,它是一個 Binder 物件,真正的實現類是 Session,這也就是之前提到的 IPC 呼叫的位置。在 Session 內部會通過 WindowManagerService 來實現 Window 的新增,程式碼如下:

public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams, attrs, int viewVisibility, 
                  int displayId, Rect outContentInsets, InputChannel outInputChannel){
   return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outInputChannel);
}

終於,Window 的新增請求移交給 WindowManagerService 手上了,在 WindowManagerService 內部會為每一個應用保留一個單獨的 Session,具體 Window 在 WindowManagerService 內部是怎麼新增的,就不對其進一步的分析,因為到此為止我們對 Window 的新增這一從應用層到 Framework 的流程已經清楚了,下面通過圖示總結一下:

這裡寫圖片描述

理解了 Window 的新增過程,Window 的刪除過程和更新過程都是類似的,也就容易理解了,它們最終都會通過一個 IPC 過程將操作移交給 WindowManagerService 這個位於 Framework 層的視窗管理服務來處理。

Window 的建立過程

View 是 Android 中的檢視的呈現方式,但是 View 不能單獨存在,它必須附著在 Window 這個抽象的概念上面,因此有檢視的地方就有 Window。哪些地方有檢視呢?Android 可以提供檢視的地方有 Activity、Dialog、Toast,除此之外,還有一些依託 Window 而實現的檢視,比如 PopUpWindow(自定義彈出視窗)、選單,它們也是檢視,有檢視的地方就有 Window,因此 Activity、Dialog、Toast 等檢視都對應著一個 Window。這也是面試中常問到的一個知識點:一個應用中有多少個 Window?下面分別分析 Activity、Dialog以及 Toast 的 Window 建立過程。

1、 Activity 的 Window 建立過程

在瞭解了 Window 的概念及意義後,我們自然就清楚 Activity 的 Window 建立時機,Window 本質就是一塊顯示區域,所以關於 Activity 的 Window 建立應該發生在 Activity 的啟動過程,Activity 的啟動過程很複雜,最終會由 ActivityThread 中的 performLaunchActivity() 來完成整個啟動過程,在這個方法內部會通過類載入器建立 Activity 的例項物件,並呼叫其 attach 方法為其關聯執行過程中所依賴的一系列上下文環境變數。

Activity 的 Window 建立就發生在 attach 方法裡,系統會建立 Activity 所屬的 Window 物件併為其設定回撥介面,程式碼如下:

mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
...

可以看到, Window 物件的建立是通過 PolicyManager 的 makeNewWindow 方法實現的,由於 Activity 實現了 Window 的 Callback 介面,因此當 Window 接受到外界的狀態改變時就會回撥 Activity 的方法。Callback 介面中的方法很多,有幾個是我們非常熟悉的,如 onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent 等等。

再回到 Window 的建立,可以看到 Activity 的 Window 是通過 PolicyManager 的一個工廠方法來建立的,但是在 PolicyManager 的實際呼叫中,PolicyManager 的真正實現是 Policy 類,Policy 類中的 makeNewWindow 方法的實現如下:

public Window  makeNewWindow(Context context){
   return new PhoneWindow(context);
}

可以看出,Window 的具體實現類的確是 PhoneWindow。到這裡 Window 以及建立完成了,下面分析 Activity 的檢視是怎麼附屬到 Window 上的,而 Activity 的檢視由 setContentView 提供,所以從 setContentView 入手,它的原始碼如下:

public void setContentView(int layoutResID){
   getWindow().setContentView(layoutResID);
   initWindowDecorActionBar();
}

可以看到,Activity 將具體實現交給了 Window,而 Window 的具體實現是 PhoneWindow,所以只需要看 PhoneWindow 的相關邏輯即可,它的處理步驟如下:

(1)、如果沒有 DecorView 就建立一個

DecorView 是 Activity 中的頂級 View,是一個 FrameLayout,一般來說它的內部包含標題欄和內容欄,但是這個會隨著主題的變化而改變,不管怎麼樣,內容欄是一定存在的,並且有固定的 id:”android.R.id.content”,在 PhoneWindow 中,通過 generateDecor 方法建立 DecorView,通過 generateLayout 初始化主題有關佈局。

(2)、將 View 新增到 DecorView 的 mContentParent 中

這一步較為簡單,直接將 Activity 的檢視新增到 DecorView 的 mContentParent 中即可,由此可以理解 Activity 的 setContentView 這個方法的來歷了,為什麼不叫 setView 呢?因為 Activity 的佈局檔案只是被新增到 DecorView 的 mContentParent 中,因此叫 setContentView 更加具體準確。

(3)、回撥 Activity 的 onContentChanged 方法通知 Activity 檢視已經發生改變

前面分析到 Activity 實現了 Window 的 Callback 介面,這裡當 Activity 的檢視已經被新增到 DecorView 的 mContentParent 中了,需要通知 Activity,使其方便做相關的處理。

經過上面的三個步驟,DecorView 已經被建立並初始化完畢,Activity 的佈局檔案也已經成功新增到了 DecorView 的 mContentParent 中,但是這個時候 DecorView 還沒有被 WindowManager 正式新增到 Window 中。在 ActivityThread 的 handleResumeActivity 方法中,首先會呼叫 Acitivy 的 onResume 方法,接著會呼叫 Acitivy 的 makeVisible() 方法,正是在 makeVisible 方法中,DecorView 才真正的完成了顯示過程,到這裡 Activity 的檢視才能被使用者看到,如下:

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

2、 Dialog 的 Window 建立過程

Dialog 的 Window 的建立過程與 Activity 類似,步驟如下:

(1)、建立 Window

Dialog 中 Window 同樣是通過 PolicyManager 的 makeNewWindow 方法來完成的,建立後的物件也是 PhoneWindow。

(2)、初始化 DecorView 並將 Dialog 的檢視新增到 DecorView 中

這個過程也和 Activity 類似,都是通過 Window 去新增指定佈局檔案:

public void setContentView(int layoutResID){
   mWindow.setContentView(layoutResID);
}

(3)、將 DecorView 新增到 Window 中並顯示

在 Dialog 的 show 方法中,會通過 WindowManager 將 DecorView 新增到 Window 中,如下:

mWindowManager.addView(mDecor, 1);
mShowing = true;

從上面三個步驟可以發現,Dialog 的 Window 建立過程和 Activity 建立過程很類似,當 Dialog 關閉時,它會通過 WindowManager 來移除 DecorView。普通的 Dialog 必須採用 Activity 的 Context,如果採用 Application 的 Context 就會報錯。這是因為沒有應用 token 導致的,而應用 token 一般只有 Activity 擁有,另外,系統 Window 比較特殊,可以不需要 token。

3、 Toast 的 Window 建立過程

Toast 與 Dialog 不同,它的工作過程稍顯複雜,首先 Toast 也是基於 Window 來實現的,但是由於 Toast 具有定時取消這一功能,所以系統採用了 Handler。在 Toast 內部有兩類 IPC 過程,一是 Toast 訪問 NotificationManagerService,第二類是 NotificationManagerService 回撥 Toast 裡的 TN 介面。NotificationManagerService 同 WindowManagerService 一樣,都是位於 Framework 層的服務,下面簡稱 NotificationManagerService 為 NMS。

Toast 屬於系統 Window,它內部的檢視可以是系統預設樣式也可以通過 setView 方法自定義 View,不管如何,它們都對應 Toast 的內部成員 mNextView,Toast 提供 show 和 cancel 分別用於顯示和隱藏 Toast,它們內部是一個 IPC 過程,程式碼如下:

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }
    public void cancel() {
        mTN.hide();

        try {
            getService().cancelToast(mContext.getPackageName(), mTN);
        } catch (RemoteException e) {
            // Empty
        }
    }

可以看到,顯示和隱藏 Toast 都需要通過 NMS 來實現,TN 是一個 Binder 類,當 NMS 處理 Toast 的顯示或隱藏請求時會跨程式回撥 TN 中的方法。由於 TN 執行在 Binder 執行緒池中,所以需要通過 Handler 將其切換到當前執行緒中,這裡的當前執行緒指的是傳送 Toast 請求所在的執行緒。

程式碼在顯示 Toast 中呼叫了 NMS 的 enqueueToast 方法, enqueueToast 方法內部將 Toast 請求封裝為 ToastRecord 物件並將其新增到一個名為 mToastQueue 的佇列中,對於非系統應用來說,mToastQueue 中最多同時存在 50 個 ToastRecord,用於防止 DOS (Denial of Service 拒絕服務)。

當 ToastRecord 新增到 mToastQueue 中後,NMS 就會通過 showNextToastLocked 方法來順序顯示 Toast,但是 Toast 真正的顯示並不是在 NMS 中完成的,而是由 ToastRecord 的 callback 來完成的:

void showNextToastLocked (){
   ToastRecord record = mToastQueue.get(0);
   while(record != null){
       if(DBG) 
          Slog.d(TAG,"show pkg=" + record.pkg + "callback=" + record.callback);
       try{
          record.callback.show();
          scheduleTimeoutLocked(record);
          return;
        }

       ...

}

這個 callback 就是 Toast 中的 TN 物件的遠端 Binder,最終被呼叫的 TN 中的方法會執行在發起 Toast 請求的應用的 Binder 執行緒池中,從以上程式碼可以看出,Toast 顯示以後,NMS 還呼叫了 sheduleTimeoutLocked 方法,此方法中首先進行延時,具體的延時時長取決於 Toast 的顯示時長,延遲相應時間後,NMS 會通過 cancelToastLocked 方法來隱藏 Toast 並將它從 mToastQueue 中移除,這時如果 mToastQueue 中還有其他 Toast,那麼 NMS 就繼續顯示其他 Toast。Toast 的隱藏也是通過 ToastRecord 的 callback 來完成的,同樣也是一次 IPC 過程。

從上面的分析,可以知道 NMS 只是起到了管理 Toast 佇列及其延時的效果,Toast 的顯示和隱藏過程實際上是通過 Toast 的 TN 類來實現的,TN 類的兩個方法 show 和 hide,是被 NMS 以跨程式的方式呼叫的,因此它們執行在 Binder 執行緒池中,為了將執行環境切換到 Toast 請求所在的執行緒,在它們內部使用了 Handler。

Toast 畢竟是要在 Window 中實現的,因此它最終還是要依附於 WindowManager,TN 的 handleShow 中程式碼如下:

mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mWM.addView(mView, mParams);

TN 的 handleHide 方法同樣需要通過 WindowManager 來實現檢視的移除,這裡就不再貼出。

總結

下面讓我們再次認清一些概念:任何 View 都是附屬在一個 Window 上面的,Window 表示一個視窗的概念,也是一個抽象的概念,Window 並不是實際存在的,它是以 View 的形式存在的。WindowManager 是外界也就是我們訪問 Window 的入口,Window 的具體實現位於 WindowManagerService 中,WindowManagerService 和 WindowManager 的互動是一個 IPC 過程。

相信讀完本文後,對 Window 會有一個更加清晰的認識,同時能夠深刻理解 Window 和 View 的依賴關係。

相關文章