淺析Android的窗口

7363只雞 7年前發布 | 15K 次閱讀 安卓開發 Android開發 移動開發

一、窗口的概念

在開發過程中,我們經常會遇到,各種跟窗口相關的類,或者方法。但是,在 Android 的框架設計中,到底什么是窗口?窗口跟 Android Framework 中的 Window 類又是什么關系?以手Q 的主界面為例,如下圖所示,上面的狀態欄是一個窗口,手Q 的主界面自然是一個窗口,而彈出的 PopupWindow 也是一個窗口,我們經常使用的 Toast 也是一個窗口。像 Dialog,ContextMenu,以及 OptionMenu 等等這些都是窗口。這些窗口跟 Window 類的關系是什么,或者窗口跟 Window 類描述的是同一個概念嗎?

其實窗口的概念,從不同的角度來看,其含義是不一樣的。我們知道,WindowManagerService(后面簡稱 WmS)管理所有的窗口。但是對于WmS來講,一個窗口其實就是一個 View 類,而不是 Window 類。WmS 負責管理這些 View 的 Z-order,顯示區域,以及把消息派發到對應的 View 中。View 本身并不能直接從 WmS 中接收消息,而是通過實現了 IWindow 接口的 ViewRootImpl.W 類來實現,以下是這些類的關系:

所以這里窗口分為兩層概念:

(1)WmS 眼中的,窗口是可以顯示用來顯示的 View。對于 WmS 而言,所謂的窗口就是一個通過 WindowManagerGlobal.addView()添加的 View 罷了;

(2)Window 類是一個針對窗口交互的抽象,也就是對于 WmS 來講所有的用戶消息是直接交給 View/ViewGroup 來處理的。而 Window 類把一些交互從 View/ViewGroup 中抽離出來,定義了一些窗口的行為,例如菜單,以及處理系統按鈕,如“Home”,“Back”等等。由此可見,Window 描述的窗口只是在通用窗口的基礎上,再抽象了一層,把符合某種規范的窗口統一了一下。Window 所描述的窗口,應該是通用窗口的一個子集。例如 PopupWindow 是一個窗口,但是分析其源碼可以知道,該類并沒有創建任何 Window 對象。而 Dialog 則是通過 PolicyManager.makeNewWindow(mContext) 創建了一個 Window 對象來管理窗口。當一個 Dialog 顯示時,我們可以通過按 back 把它 dismiss 了,但是 PopupWindow 則不行,需要自己去處理。

二、窗口類型

添加一個窗口是通過 WindowManagerGlobal.addView()來完成的,分析 addView 方法的參數,有三個參數是必不可少的,view,params,以及 display。而 display 一般直接取 WindowMnagerImpl 中的 mDisplay,表示要輸出的顯示設備。view 自然表示要顯示的 View,而 params 是 WindowManager.LayoutParams,用來描述這個 view 的些窗口屬性,其中一個重要的參數 type,用來描述窗口的類型。

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

分析 WindowManager 對于 type 可賦值類型的描述可知,Framework 中定義了三種類型的窗口:

(1)應用窗口 Activity 對應的窗口就是應用窗口, 所有 Activity 默認的窗口類型是 TYPE _BASE _APPLICATION。WindowManager 的 LayoutParams 的默認構建方法的實現,可以看到默認類型是 TYPE _ APPLICATION。 Dialog 的窗口類型是 TYPE _ APPLICATION,而很多 Dialog 的子類,修改了窗口類似,如 ContextMenu,本質是用 Dialog 來實現的,但是在添加窗口前,修改了 type 類型,賦值為 TYPE _ APPLICATION _ ATTACHED _ DIALOG。從這個我們可以看到,WmS 并沒有把應用窗口與子窗口區分得那么清楚。

public LayoutParams() {
    super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    type = TYPE_APPLICATION;
    format = PixelFormat.OPAQUE;
}

(2)子窗口子窗口是指該窗口必須要有一個父窗口,父窗口可以是一個應用類型窗口,也可以是任何其他類型的窗口。例如前面手Q 界面中,點擊右上角的按鈕顯示一個 PopupWindow,它就是一個子窗口,其類型一般 TYPE _ APPLICATION _ PANEL。既然稱為子窗口,其與父窗口的關系是比較容易理解的。B 是 A 的子窗口,當 A 不可見時,B 也會不可見的。如果A不可見時添加B,B 也是不可見的,直到 A 可見為止,B 跟隨一起可見。

(3)系統窗口 系統窗口跟應用窗口不同,不需要對應 Activity。跟子窗口不同,不需要有父窗口。一般來講,系統窗口應該由系統來創建的,例如發生異常,ANR時的提示框,又如系統狀態欄,屏保等。但是,Framework 還是定義了一些,可以被應用所創建的系統窗口,如 TYPE_ TOAST,TYPE _INPUT _ METHOD,TYPE _WALLPAPTER 等等。

token 的含義

相信大家對于 token 這個詞并不陌生,在開發過程中經常遇到,例如 Bad Token 的異常。到底在 Android 框架中,token 代表什么?分析源碼,我們發現,大多數 token 的對象,都表示一個 IBinder 對象。提到 IBinder,大家一點也不陌生,就是 Android 的 IPC 通信機制。在創建窗口過程中,涉及到的 IPC 通信,無非包含兩方面,一個是 WmS 用來跟應用所在的進程進行通信的 ViewRootImpl.W 類的對象,另一個是指向一個 ActivityRecord 的對象,自然應該是WmS用來跟 AmS 進行通信的了。我們梳理了一下,token 以下幾處的定義,分別來講講這里的 token 代表什么。

分析一下 View 的 AttachInfo 的賦值。ViewRootImpl 在構建方法里,會初始化一個 AttachInfo 實例,把它的 Session,以及 W類對象賦值給 AttachInfo。分析可以看到,AttachInfo 中的 mWindowToken,與mWindow 都是指向 ViewRootImpl 中的 mWindow(W類實例)。當一個 View attach 到窗口后,ViewRootImpl會執行performTraversals,如果發現是首次調用會,會把自己的 mAttachInfo 傳遞給根 View(通過dispatchAttachedToWindow),告訴 View 樹現在已經 attch to Window 了,馬上可以顯示了。根 View(一般是 ViewGroup)會把這個信息,遍歷地傳遞給 View 樹中的每一個子 View,這樣每個 View 的 mAttachInfo 都被賦值為 ViewRootImp 的 mAttachInfo了。

//分析一下 View 中的 AttachInfo 的賦值,以及 ViewRootImpl 中的 mAttachInfo
    public ViewRootImpl(Context context, Display display) {
        ...
        mWindow = new W(this);
        ...
         mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
        ...
    }
    //看到mWindowToken其實就是IWindow實例
    AttachInfo(IWindowSession session, IWindow window, Display display,
                 ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {
           mSession = session;
           mWindow = window;
           mWindowToken = window.asBinder();
           mDisplay = display;
           mViewRootImpl = viewRootImpl;
           mHandler = handler;
           mRootCallbacks = effectPlayer;
    }
    // ViewRootImpl在第一次執行performTraversals時,會把自己的mAttachInfo傳遞給根View,然后由根View逐級傳遞下去
    private void performTraversals() {
       ...
        if (mFirst) {
            ...
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            ...
        }else{
            ...
        }
    }
    //ViewGroup.java
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
       mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
       super.dispatchAttachedToWindow(info, visibility);
       mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
       final int count = mChildrenCount;
       final View[] children = mChildren;
       for (int i = 0; i < count; i++) {
           final View child = children[i];
           child.dispatchAttachedToWindow(info,
                   visibility | (child.mViewFlags & VISIBILITY_MASK));
      }
    }
    //View.java
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
       mAttachInfo = info;
       ...
     }

WindowManager.LayoutParams 中的 type 與 token

WindowManager.LayoutParams 用來描述一個窗口的特性,最終在添加窗口時,會傳遞給 WmS。而且 WmS 會保存在 WindowState 的 mAttrs 中。LayoutParams 有很多參數,但是跟窗口創建相關的參數,最重要的就是 type 與 token 了,這里我們可以通過分析 WmS 的 addWindow 代碼的可以知道:

[i]
    //WindowManagerService.java addWindow 
    ...
    //權限檢查,需要用到type類型,會檢查窗口類型是否合法,如果是系統窗口類型
    //還需要進行權限檢查,詳見PhoneWindowManager.java
    int res = mPolicy.checkAddPermission(attrs, appOp);
    ...
    //如果是子窗口類型,還會檢查其父窗口是否存在,如果父窗口不存在,直接拋出異常
    if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
                attachedWindow = windowForClientLocked(null, attrs.token, false);
                if (attachedWindow == null) {
                     Slog.w(TAG, "Attempted to add window with token that is not a window: " + attrs.token + ".  Aborting.");
                     return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
                 }
                 if (attachedWindow.mAttrs.type >= FIRST_SUB_WINDOW
                       && attachedWindow.mAttrs.type <= LAST_SUB_WINDOW) {
                    Slog.w(TAG, "Attempted to add window with token that is a sub-window: " + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
                 }
      }
      ...
     //如果是類型是TYPE_PRIVATE_PRESENTATION ,還會檢查相應的顯示設備
     if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
            Slog.w(TAG, "Attempted to add private presentation window to a non-private display.  Aborting.");
            return WindowManagerGlobal.ADD_PERMISSION_DENIED;
        }
     ...    
    //根據LayoutParams中的token,會檢索WmS中保存的WindowToken,
    //由引可見,不同的窗口類型,其對應的token是有區別的。WmS要根據窗口類型來檢查其傳遞過來的token是否合法。
    boolean addToken = false;
    WindowToken token = mTokenMap.get(attrs.token);
    if (token == null) {
       if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                Slog.w(TAG, "Attempted to add application window with unknown token " + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
    if (type == TYPE_INPUT_METHOD) {
                Slog.w(TAG, "Attempted to add input method window with unknown token " + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
    if (type == TYPE_VOICE_INTERACTION) {
                Slog.w(TAG, "Attempted to add voice interaction window with unknown token "+ attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
    if (type == TYPE_WALLPAPER) {
                Slog.w(TAG, "Attempted to add wallpaper window with unknown token " + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
    if (type == TYPE_DREAM) {
                Slog.w(TAG, "Attempted to add Dream window with unknown token " + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
    if (type == TYPE_ACCESSIBILITY_OVERLAY) {
                Slog.w(TAG, "Attempted to add Accessibility overlay window with unknown token "  + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
            token = new WindowToken(this, attrs.token, -1, false);
            addToken = true;
    } else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
            AppWindowToken atoken = token.appWindowToken;
            if (atoken == null) {
                Slog.w(TAG, "Attempted to add window with non-application token " + token + ".  Aborting.");
                return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
            } else if (atoken.removed) {
                Slog.w(TAG, "Attempted to add window with exiting application token "  + token + ".  Aborting.");
                return WindowManagerGlobal.ADD_APP_EXITING;
            }
            if (type == TYPE_APPLICATION_STARTING && atoken.firstWindowDrawn) {
                // No need for this guy!
                if (localLOGV) Slog.v(
                        TAG, "**** NO NEED TO START: " + attrs.getTitle());
                return WindowManagerGlobal.ADD_STARTING_NOT_NEEDED;
            }
        } else if (type == TYPE_INPUT_METHOD) {
            if (token.windowType != TYPE_INPUT_METHOD) {
                Slog.w(TAG, "Attempted to add input method window with bad token "  + attrs.token + ".  Aborting.");
                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
        } else if (type == TYPE_VOICE_INTERACTION) {
            if (token.windowType != TYPE_VOICE_INTERACTION) {
                Slog.w(TAG, "Attempted to add voice interaction window with bad token "  + attrs.token + ".  Aborting.");
                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
        } else if (type == TYPE_WALLPAPER) {
            if (token.windowType != TYPE_WALLPAPER) {
                Slog.w(TAG, "Attempted to add wallpaper window with bad token "   + attrs.token + ".  Aborting.");
                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
        } else if (type == TYPE_DREAM) {
            if (token.windowType != TYPE_DREAM) {
                Slog.w(TAG, "Attempted to add Dream window with bad token " + attrs.token + ".  Aborting.");
                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
        } else if (type == TYPE_ACCESSIBILITY_OVERLAY) {
            if (token.windowType != TYPE_ACCESSIBILITY_OVERLAY) {
                Slog.w(TAG, "Attempted to add Accessibility overlay window with bad token " + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
        } else if (token.appWindowToken != null) {
            Slog.w(TAG, "Non-null appWindowToken for system window of type=" + type);
            // It is not valid to use an app token with other system types; we will
            // instead make a new token for it (as if null had been passed in for the token).
            attrs.token = null;
            token = new WindowToken(this, null, -1, false);
            addToken = true;
        }   
      ...
    //經過一系列的檢查之后,最后會生成窗口在WmS中的表示WindowState,并且把LayoutParams賦值給WindowState的mAttrs
    win = new WindowState(this, session, client, token, attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);  
    ...  
    if (addToken) {
     mTokenMap.put(attrs.token, token);
    } 
    ...
     mWindowMap.put(client.asBinder(), win);   
[/i]

總結一下:

(1)窗口類型必須是指定合法范圍內的,即應用窗口,子窗口,系統窗口中的一種,否則檢查會失敗; (2)如果是系統,需要進行權限檢查 以下類型不需要特別聲明權限 TYPE _ TOAST,TYPE _ DREAM,TYPE _ INPUT _ METHOD,TYPE _ WALLPAPER,TYPE _ PRIVATE _ PRESENTATION,TYPE _ VOICE _ INTERACTION,TYPE _ ACCESSIBILITY _ OVERLAY 以下類型需要聲明使用權限:android.permission.SYSTEM _ ALERT _ WINDOW TYPE _ PHONE,TYPE _ PRIORITY _ PHONE,TYPE _ SYSTEM _ ALERT,TYPE _ SYSTEM _ ERROR,TYPE _ SYSTEM _ OVERLAY 其他的系統窗口,需要聲明權限:android.permission.INTERNAL _ SYSTEM _ WINDOW (3)如果是應用窗口,通過 token 檢索出來的 WindowToken,一定不能為空,而且還必須是 Activity 的 mAppToken,同時對應的 Activity 還必須是沒有被 finish。之前分析 Activity 的啟動過程我們知道,Activity 在啟動過程中,會先通過 WmS 的 addAppToken( )添加一個 AppWindowToken 到 mTokenMap 中,其中 key 就用了 IApplicationToken token。而 Activity 中的 mToken,以及 Activity 對應的 PhoneWindow 中的 mAppToken 就是來自 AmS 的 token (代碼見 Activity 的 attach 方法)。 (4)如果是子窗口,會通過 attrs.token 去通過 windowForClientLocked 查找其父窗口,如果找不到其父窗口,會拋出異常。或者如果找到的父窗口的類型還是子窗口類型,也會拋出異常。這里查找父窗口的過程,是直接取了 attrs.token 去 mWindowMap 中找對應的 WindowState,而 mWindowMap 中的 key 是 IWindow。所以,由此可見,創建一個子窗口類型,token 必須賦值為其父窗口的 ViewRootImpl 中的 W 類對象 mWindow。 (5)如果是如下系統窗口,TYPE _ INPUT _ METHOD,TYPE _ VOICE _ INTERACTION,TYPE _ WALLPAPER,TYPE _ DREAM,TYPE _ ACCESSIBILITY _ OVERLAY,token 不能為空,而且通過 token 檢索到的 WindowToken 的類型不能是其本身對應的類型。 (6)如果是其他系統窗口,會直接把 attrs 中的 token 給清除了,不需要 token。因此其他類型的系統窗口,LayoutParams 中 token 是可以為空的。 (7)檢查通過后,如果需要創建新的 WindowToken,會以 attrs.token 為 key,add 到 mTokenMap 中。 (8)WindowState 創建后,會以 IWindow 為 key (對應應用進程中的 ViewRootImpl.W 類對象 mWindow,重要的事強調多遍!!),添加到 mWindowMap 中。

由此可見,我們要成功添加一個窗口,對于 type 與 token 的賦值是有要求的,否則先不說能否正確顯示,直接就創建失敗了。那 type 與 token 是如何賦值的呢?最直接來講,就是在調用 WindowManagerImpl 的 addView 方法前,把值賦好就可以了。但是,分析 FrameWork 所提供的一些窗口的顯示,如 Dialog 等,并沒有看到在調用 addView 之前,對 token 賦值呢。其窗口類型是應用窗口,根據前面所描述的檢查,token 肯定不能為 null 的,而且還必須是 Activity 的 mAppToken,否則創建失敗的。這里我們分析一下,在 token 沒有賦值的情況下,調用 addView 會做哪些處理。代碼就要回到 WindowManagerImpl 開始了。

[i]
    //WindowManagerImpl.java addView
    //applyDefaultToken會檢查,有沒有設置默認token,如果有設置,而且沒有設置父窗口的情況下,token又是null的話,直接把默認token賦值給token吧。
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
       applyDefaultToken(params);
       mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    //WindowManagerGlobal.java addView
    //如果有設置父窗口,會通過adjustLayoutParamsForSubWindow來調整params。
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
    //Window.java adjustLayoutParamsForSubWindow
    if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
        wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        //如果是子窗口類型,而且token為null,直接取父窗口的AttachInfo中的mWindowToken,其實就是父窗口對應的ViewRootImpl中的W類對象mWindow。
        if (wp.token == null) {
            View decor = peekDecorView();
            if (decor != null) {
                wp.token = decor.getWindowToken();
            }
        }
        ...
    }else {
    //如果token為null,直接取父窗口的mAppToken吧。
    if (wp.token == null) {
            wp.token = mContainer == null ? mAppToken :           mContainer.mAppToken;
      }
    }
[/i]

 

來自:http://dev.qq.com/topic/5923ef85bdc9739041a4a798

 

 本文由用戶 7363只雞 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!