傳說中的WindowManager

不洗碗工作室發表於2018-06-03

作者:不洗碗工作室 - catango

版權歸作者所有,轉載請註明出處

通常情況下,我們使用的Dialog,Activity等需要顯示到螢幕上面的內容都需要WindowManager來操作的,WindowManager是一個非常重要的子系統.其中有關的類為WindowManager,WindowManagerService,Surface和SurfaceFlinger

什麼是Window

Android系統中的Window是螢幕上的一塊用於繪製各種UI元素並能夠響應應使用者輸入的一個矩形區域和獨自佔有一個Surface例項的顯示區域。 比如Dialog、Activity的介面、桌布、狀態列以及Toast等都是Window.一般來說它是和具體的View繫結在一起的.

Window是一個抽象類,具體實現是PhoneWindow.建立Window只需要WindowManager即可,WindowManager是外界訪問Window的入口,通過WindowManager能夠操縱Window,其具體實現都在WMS中.

層級關係

Window有三種型別,應用類的Window(如Activity),子Window(如Dialog),系統Window(如Toast).Window是分層的,每個Window都是z-ordered,層級大的覆蓋在層級小的上面,這些層級對應在WindowManager.LayoutParams的type引數.而系統級別的Window需要許可權宣告.

  • 應用程式視窗
  	public static final int FIRST_APPLICATION_WINDOW = 1;//1
public static final int TYPE_BASE_APPLICATION   = 1;//視窗的基礎值,其他的視窗值要大於這個值
public static final int TYPE_APPLICATION        = 2;//普通的應用程式視窗型別
public static final int TYPE_APPLICATION_STARTING = 3;//應用程式啟動視窗型別,用於系統在應用程式視窗啟動前顯示的視窗。
public static final int TYPE_DRAWN_APPLICATION = 4;
public static final int LAST_APPLICATION_WINDOW = 99;//2

複製程式碼
  • 子視窗(必須有依賴的父視窗才可以)
public static final int FIRST_SUB_WINDOW = 1000;//子視窗型別初始值
 public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
 public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;
 public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;
 public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;
 public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW + 4; 
 public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
 public static final int LAST_SUB_WINDOW = 1999;//子視窗型別結束值
複製程式碼
  • 系統視窗
 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;//搜尋條視窗
 public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;//通話視窗
 public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;//系統ALERT視窗
 public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;//鎖屏視窗
 public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;//TOAST視窗
 ...
 public static final int LAST_SYSTEM_WINDOW      = 2999;//系統視窗型別結束值
複製程式碼

Window的屬性

Flags參數列示Window的屬性,通過 WindowManager.LayoutParams的flags來設定

  • FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 只要視窗可見,就允許在開啟狀態的螢幕上鎖屏
  • FLAG_NOT_FOCUSABLE 視窗不能獲得輸入焦點,設定該標誌的同時,FLAG_NOT_TOUCH_MODAL也會被設定
  • FLAG_NOT_\TOUCHABLE 視窗不接收任何觸控事件
  • FLAG_NOT_TOUCH_MODAL 在該視窗區域外的觸控事件傳遞給其他的Window,而自己只會處理視窗區域內的觸控事件
  • FLAG_KEEP_SCREEN_ON 只要視窗可見,螢幕就會一直亮著
  • FLAG_LAYOUT_NO_LIMITS 允許視窗超過螢幕之外
  • FLAG_FULLSCREEN 隱藏所有的螢幕裝飾視窗,比如在遊戲、播放器中的全屏顯示
  • FLAG_SHOW_WHEN_LOCKED 視窗可以在鎖屏的視窗之上顯示
  • FLAG_IGNORE_CHEEK_PRESSES 當使用者的臉貼近螢幕時(比如打電話),不會去響應此事件
  • FLAG_TURN_SCREEN_ON 視窗顯示時將螢幕點亮

Token

在原始碼中token一般代表的是Binder物件,作用於IPC程式間資料通訊。並且它也包含著此次通訊所需要的資訊,在ViewRootImpl裡,token用來表示mWindow(W類,即IWindow),並且在WMS中只有符合要求的token才能讓Window正常顯示。 在Window中,token(LayoutParams中的token)分為以下2種情況

  • 應用視窗 : token表示的是activity的mToken(ActivityRecord)
  • 子視窗 : token表示的是父視窗的W物件,也就是mWindow(IWindow)

無論是應用視窗,還是子視窗,token 不能為空。否則會丟擲異常,並且應用視窗的token 必須是某個 Activity 的 mToken,子視窗的token 必須是父視窗的 IWindow 物件。而且子視窗還需等父視窗新增成功之後才可新增到Window上。

什麼是Surface

基本上我們看到的檢視都是通過Surface來渲染的,Surface是一塊畫布,通過Canvas或者OpenGL在上面作畫.然後通過SufaceFlinger將多塊Surface按照Z-order排序後輸出到FrameBuffer中展示到螢幕上.其中surface使用了雙緩衝技術.Surface是通過WMS分配到Window上面的.

通過原始碼我們可以知道,Surface裡面定義了一個Canvas,可以這麼理解,Canvas就是其緩衝區內容的具現.最終都會儲存到Surface裡的buffer裡,最後由SurfaceFlinger合成並顯示。

什麼是WindowManagerService

在WindowManager呼叫之前,首先是獲取到WindowManagerService.各種系統級服務都會註冊到ContextImpl的一個map中維護,然後通過相應的字串來獲取,比如LayoutInflate是通過Context呼叫getSystemService,傳入LAYOUT _ INFLATER _ SERVICE字元來獲取到的.WindowManager也一樣,通過WINDOW _ SERVICE來獲取到的.

WMS就是對所有Window的Surface進行協調管理的類,WMS為全部Window分配Surface,控制Surface尺寸,大小,動畫等等.我們通過WindowManager來控制View的新增,刪除等操作,其實都是通過WMS處理的.WindowManager通過Binder來進行跨程式通訊,類似ActivityManager和AMS的關係.

什麼是WindowManager

WindowManager繼承自ViewManager,看名字就知道這個類是管理View的,而View又在Window中展示,也可以說WindowManager是管理Window(廢話)的.我們想對Window進行新增刪除等操作都可以使用WindowManager

其中ViewManager介面有三大方法

  • addView
  • updateViewLayout
  • removeView

一般來說,我們常用的方法也就這三個,新增更新和刪除,它的基本流程都是建立一個Window,並且向裡面新增View,如果想刪除一個Window,刪除它對應的View就好.所以說WindowManager操縱Window的過程就是操作對應在Window裡面View的過程.

具體的使用

BB了它的作用之後,其實最重要的還是怎麼去用了.下面簡單介紹下WindowManager的使用

	TextView view = new TextView(this);
     view.setText("Balabala");
     WindowManager.LayoutParams params =
                new WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, PixelFormat.TRANSPARENT);
        params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
        params.type = WindowManager.LayoutParams.TYPE_APPLICATION;
        params.x = 200;
        params.y = 300;
        getWindowManager().addView(view, params);
複製程式碼

這段程式碼就是將一個TextView新增到Window,螢幕座標是200,300的位置.首先建立一個TextView,然後建立一個LayoutParams設定Window相應的屬性,其中比較重要的是type和flags屬性,參考上面的值按需設定即可.

如果當Window需要變化時,比如使用者拖動懸浮框這種操作,我們只需要監聽觸控事件,然後不停的呼叫updateViewLayout方法.

內部流程

可以看到它內部的幾種方法都是針對View的,說明了View才是Window的主體,實際上我們是無法直接訪問Window的.上面也說過了,Window的操作全都是通過WindowManager來操作的,WindowManager其實也是一個介面,它的具體實現是WindowMangerImpl類.

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

    @Override
    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }
    
      @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }
複製程式碼

其中實現了這三個方法的具體實現也不是通過WindowManger的,而是通過mGlobal來實現的,這裡其實用到了傳說中的橋接模式, 那麼Window和View的具體關係我們就可以從WindowManagerGlobal的新增刪除更新的方法來分析了.WindowManagerGlobal本身是一個單例,因此它其實是管理所有Window的Manager.這點也可以從以下幾個比較重要的引數可以看到

    private final ArrayList<View> mViews = new ArrayList<View>();//儲存所有Window對應的View
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); //所有Window對應的ViewRootImpl
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>(); //儲存所有Window所對應的佈局引數
    private final ArraySet<View> mDyingViews = new ArraySet<View>(); //儲存正在被刪除但是操作還未完成的View物件
複製程式碼

下面用新增的例子來分析下Window和View的關係.

View的新增

 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
            
        //1.檢查引數合法性,如果是子Window還要去調整下佈局引數設定token等操作
        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;
        //2. parentWindow一般來說都是Activity的Window
        if (parentWindow != null) {
        //3.調整佈局,adjustLayoutParamsForSubWindow主要是設定token,如果是應用視窗,token可能為null,就會給他賦值mAppToken,這個token是Activity的attach()方法中傳入的token
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            // If there's no parent, then hardware acceleration for this view is
            // set from the application's hardware acceleration setting.
            final Context context = view.getContext();
            if (context != null
                    && (context.getApplicationInfo().flags
                            & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
                wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
            }
        }
      
        ViewRootImpl root;
        View panelParentView = null;
        
  			   synchronized (mLock) {
            // Start watching for system property changes.
            if (mSystemPropertyUpdater == null) {
                mSystemPropertyUpdater = new Runnable() {
                    @Override public void run() {
                        synchronized (mLock) {
                            for (int i = mRoots.size() - 1; i >= 0; --i) {
                                mRoots.get(i).loadSystemProperties();
                            }
                        }
                    }
                };
                SystemProperties.addChangeCallback(mSystemPropertyUpdater);
            }

  				
  			//4.從Views中尋找當前的View的index,如果>0,在將移除的View中停止移除
  			 int index = findViewLocked(view, false);
            if (index >= 0) {
                if (mDyingViews.contains(view)) {
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                } else {
                    throw new IllegalStateException("View " + view
                            + " has already been added to the window manager.");
                }
            }
            
            //5.層級的判斷,如果這是一個子Window,那麼找到它的父window,將來引用。這裡的param是其父Window呼叫adjustLayoutParamsForSubWindow方法獲取到的,本質作用是為了使用父視窗的token
                    
                    if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }
            //6,建立ViewRootImpl與view繫結,並且將View和ViewRootImpl新增到列表中()
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            //7,通過setView方法來更新介面並且完成Window新增的邏輯,內部呼叫requestLayout方法完成非同步重新整理請求
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }
複製程式碼

關鍵步驟的說明在註釋中,在最後非同步我們可以看到呼叫了ViewRootImpl的setView方法,重點傳入了view和相應的param引數,setView方法的原始碼重要部分如下:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

                int res; 

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();//View的繪製流程

                if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                    //建立InputChannel
                    mInputChannel = new InputChannel();
                }

                try {

                    //通過WindowSession進行IPC呼叫,將View新增到Window上,其中View資訊包含在mAttachINfo中
                    //mWindow即W類,用來接收WmS資訊
                    //同時通過InputChannel接收觸控事件回撥
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                }

                ......

                    //處理觸控事件回撥
                    mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                            Looper.myLooper());

                ......
    }
複製程式碼

執行requestLayout()方法完成View的繪製流程(下篇文章專門講解),並且通過WindowSession將View和InputChannel新增到WMS中,從而將View新增到Window上並且接收觸控事件。

通過mWindowSession來完成Window的新增過程 ,mWindowSession的型別是IWindowSession,是一個Bindler物件,真正的實現類是Session,也就是Window的新增是一次IPC呼叫. mWindowSession在ViewRootImpl的建構函式中通過WindowManagerGlobal.getWindowSession()方法建立

同時將mWindow(即 W extends IWindow.Stub)傳送給WMS,用來接收WMS資訊。 如此一來,Window的新增請求就交給WMS去處理了,在WMS內部會為每一個應用保留一個單獨的Session。WMS會建立一個WindowState物件用來表示當前新增的視窗。 WMS負責管理這裡些 WindowState 物件。至此,Window的新增過程就結束了。

Token

下來簡單說明下token這個東西,這個不知道不影響理解window的新增...

token的作用上面說過了,在這個流程中token是怎麼體現的呢?在WindowManagerImpl中,有個applyDefaultToken的方法,將params的token設定為預設defaultToken(activity的attach方法中會搞一個這樣的token,本質其實是一個ActivityRecord).在ViewRootImpl的構造方法中會為attachInfo設定token,並且在performTraversals會將自己的mAttachInfo關聯到所有的子view,保證所有的view都能調到token.

如果子視窗沒有父視窗或者父視窗也是子視窗,那麼程式將會崩潰,token一定不能為空,如果我們在onCreate方法中呼叫popupWindow或者dialog的context不是activity的token,那麼就會崩潰的.具體原因可以看看popupWindow的Window建立過程和dialog的window的建立過程

相關文章