Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> 帶你徹底理解 Android 中的 Window 和 WindowManager

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

編輯:Android資訊

有時候我們需要在桌面上顯示一個類似懸浮窗的東西,這種效果就需要用 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 的依賴關系。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved