1. 程式人生 > >Android Window 機制探索

Android Window 機制探索

*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

Window的概念

Android手機中所有的檢視都是通過Window來呈現的,像常用的Activity,Dialog,PopupWindow,Toast,他們的檢視都是附加在Window上的,所以可以這麼說 ——「Window是View的直接管理者。」

這裡寫圖片描述

  • Window
    一個頂級視窗檢視和行為的一個抽象基類。這個類的例項作為一個頂級View新增到Window Manager。它提供了一套標準的UI方法,比如新增背景,標題等等。當你需要用到Window的時候,你應該使用它的唯一實現類PhoneWindow
/**
 * 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.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window { `````` public abstract View getDecorView(); @Nullable public View findViewById(@IdRes int id) { return getDecorView().findViewById(id); } public abstract void setContentView(View view); `````` }
  • PhoneWindow
    每一個Activity都包含一個Window
    物件(dialog,toast 等也是新新增的window物件),而Window是一個抽象類,具體實現是PhoneWindow。在Activity中的setContentView實際上是呼叫PhoneWindowsetContentView方法。並且PhoneWindow中包含著成員變數DecorView
//Activity 
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

//getWindow獲取到的是mWindow         
//在attach方法裡,mWindow = new PhoneWindow(this, window);
  • DecorView(FrameLayout)
    作為頂級View,DecorView一般情況下它內部會包含一個豎直方向的LinearLayout,上面的標題欄(titleBar),下面是內容欄。通常我們在Activity中通過setContentView所設定的佈局檔案就是被載入到id為android.R.id.content的內容欄裡(FrameLayout),
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
        ``````
}

setContentView

//PhoneWindow


    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {

//①初始化
        //建立DecorView物件和mContentParent物件 ,並將mContentParent關聯到DecorView上
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();//Activity轉場動畫相關
        }

//②填充Layout
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);//Activity轉場動畫相關
        } else {
        //將Activity設定的佈局檔案,載入到mContentParent中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }

        //讓DecorView的內容區域延伸到systemUi下方,防止在擴充套件時被覆蓋,達到全屏、沉浸等不同體驗效果。
        mContentParent.requestApplyInsets();

//③通知Activity佈局改變
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {

        //觸發Activity的onContentChanged方法  
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
  • ①初始化 : Activity第一次呼叫setContentView,則會呼叫installDecor()方法建立DecorView物件和mContentParent物件。FEATURE_CONTENT_TRANSITIONS表示是否使用轉場動畫。如果內容已經載入過,並且不需要動畫,則會呼叫removeAllViews移除內容以便重新填充Layout。

  • ②填充Layout : 初始化完畢,如果設定了FEATURE_CONTENT_TRANSITIONS,就會建立Scene完成轉場動畫。否則使用佈局填充器將佈局檔案填充至mContentParent。到此為止,Activity的佈局檔案已經新增到DecorView裡面了,所以可以理解Activity的setContentView方法的由來,因為佈局檔案是新增到DecorViewmContentParent中,所以方法名為setContentView無可厚非。「開頭第一張圖的contentView」

installDecor

//PhoneWindow  --> setContentView()


private void installDecor() {
        if (mDecor == null) {
        //建立DecorView
            mDecor = generateDecor();

        ``````  
        }else {
            mDecor.setWindow(this);
        }

        if (mContentParent == null) {//並將mContentParent關聯到DecorView上

        //根據主題theme設定對應的xml佈局檔案以及Feature(包括style,layout,轉場動畫,屬性等)到DecorView中。
        //並將mContentParent繫結至id為ID_ANDROID_CONTENT(com.android.internal.R.id.content)的ViewGroup
        //mContentParent在DecorView新增的xml檔案中
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

                `````` 
            //新增其他資源
            //設定轉場動畫
        }

}

generateLayout

//PhoneWindow --> setContentView()  -->installDecor() 


 protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        //獲取當前的主題,載入預設資源和佈局
        TypedArray a = getWindowStyle();

        ``````
        //根據theme的設定,找到對應的Feature(包括style,layout,轉場動畫,屬性等)
        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);//無titleBar
        } 

        ``````

        if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
            setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));//設定全屏
        }

        if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,
                false)) {
            setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS
                    & (~getForcedWindowFlags()));//透明狀態列
        }

        //根據當前主題,設定不同的Feature
        ``````

        int layoutResource;
        int features = getLocalFeatures();

        //由於佈局較多,我們拿有titleBar的例子來看
        if ((features & (1 << FEATURE_NO_TITLE)) == 0) {

            ``````
                layoutResource = R.layout.screen_title;

        } 
        ``````
        else {//無titleBar
            layoutResource = R.layout.screen_simple;
        }

        mDecor.startChanging();

        //將佈局layout,新增至DecorView中
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        //從佈局中獲取`ID_ANDROID_CONTENT`,並關聯至contentParent
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

        ``````
        //配置完成,DecorView根據已有屬性調整佈局狀態
        mDecor.finishChanging();

        return contentParent;
    }
//sdk\platforms\android-25\data\res\layout\screen_simple.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
//sdk\platforms\android-25\data\res\layout\screen_title.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

首先根據當前的主題,載入預設資源和佈局,根據相關的FEATURE獲取資源佈局layout檔案。然後將佈局新增到DecorView中,並且將contentParent與佈局中id為ID_ANDROID_CONTENTFrameLayout繫結。所以我們可以通過findViewById(com.android.internal.R.id.content)獲取到contentView

這裡寫圖片描述
檢視大圖

Window的型別

新增視窗是通過WindowManagerGlobaladdView方法操作的,這裡有三個必要引數。view,params,display。
display : 表示要輸出的顯示裝置。
view : 表示要顯示的View,一般是對該view的上下文進行操作。(view.getContext())
params : 型別為WindowManager.LayoutParams,即表示該View要展示在視窗上的佈局引數。其中有一個重要的引數type,用來表示視窗的型別。

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

開啟WindowManager類,看到靜態內部類。

//WindowManager

    public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
    ``````
}

可以看到在LayoutParams中,有2個比較重要的引數: flags,type
我們簡要的分析一下flags,該引數表示Window的屬性,它有很多選項,通過這些選項可以控制Window的顯示特性,這裡主要介紹幾個比較常用的選項。

  • FLAG_NOT_FOCUSABLE
    表示Window不需要獲取焦點,也不需要接收各種輸入事件,此標記會同時啟用FLAG_NOT_TOUCH_MODAL,最終事件會直接傳遞給下層具有焦點的Window。

  • FLAG_NOT_TOUCH_MODAL
    系統會將當前Window區域以外的單擊事件傳遞給底層的Window,當前Window區域以內的單擊事件則自己處理,這個標記很重要,一般來說都需要開啟此標記,否則其他Window將無法接收到單擊事件。

  • FLAG_SHOW_WHEN_LOCKED
    開啟此模式可以讓Window顯示在鎖屏的介面上。

Type引數表示Window的型別,Window有三種類型,分別是應用Window、子Window、系統Window。應用類Window對應著一個Activity。子Window不能單獨存在,它需要附屬在特定的父Window之中,比如常見的PopupWindow就是一個子Window。有些系統Window是需要宣告許可權才能建立的Window,比如Toast和系統狀態列這些都是系統Window。

應用視窗

Activity 對應的視窗型別是應用視窗, 所有 Activity 預設的視窗型別是 TYPE_BASE_APPLICATION
WindowManagerLayoutParams 的預設型別是 TYPE_APPLICATION。 Dialog 並沒有設定type,所以也是預設的視窗型別即 TYPE_APPLICATION

//WindowManager  LayoutParams的預設構造方法

public LayoutParams() {
    super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    type = TYPE_APPLICATION;
    format = PixelFormat.OPAQUE;
}
type層級 型別
FIRST_APPLICATION_WINDOW=1 開始應用程式視窗,第一個普通應用視窗
TYPE_BASE_APPLICATION=1 所有程式視窗的base視窗,其他應用程式視窗都顯示在它上面
TYPE_APPLICATION=2 普通應用程式視窗,token必須設定為Activity的token來指定視窗屬於誰
TYPE_APPLICATION_STARTING=3 應用程式啟動時先顯示此視窗,當真正的視窗配置完成後,關閉此視窗
LAST_APPLICATION_WINDOW=99 最後一個應用視窗

子視窗

子視窗不能單獨存在,它需要附屬在特定的父Window之中,例如開篇第一張圖,綠色框框即為popupWindow,它就是子視窗,型別一般為TYPE_APPLICATION_PANEL。之所以稱為子視窗,即它的父視窗顯示時,子窗口才顯示。父視窗不顯示,它也不顯示。追隨父視窗。
那麼問題又來了,我們能否在AcitivtyonCreate()中建立popupWindow並顯示呢?

type層級 型別
FIRST_SUB_WINDOW=1000 第一個子視窗
TYPE_APPLICATION_PANEL=1000 應用視窗的子視窗,popupWindow的預設型別
TYPE_APPLICATION_MEDIA=1001 媒體視窗
TYPE_APPLICATION_SUB_PANEL=1002 TYPE_APPLICATION_PANE的子視窗
TYPE_APPLICATION_ATTACHED_DIALOG=1003 對話方塊,類似於面板視窗(OptionMenu,ContextMenu)
TYPE_APPLICATION_MEDIA_OVERLAY=1004 媒體資訊,顯示在媒體層和程式視窗之間,需要實現半透明效果
LAST_SUB_WINDOW=1999 最後一個子視窗

系統視窗

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

type層級 型別
FIRST_SYSTEM_WINDOW=2000 第一個系統視窗
TYPE_STATUS_BAR=2000 狀態列,只能有一個狀態列,位於螢幕頂端
TYPE_SEARCH_BAR =2001 搜尋欄
TYPE_PHONE=2002 電話視窗,它用於電話互動
TYPE_SYSTEM_ALERT=2003 系統警告,出現在應用程式視窗之上
TYPE_KEYGUARD=2004 鎖屏視窗
TYPE_TOAST=2005 資訊視窗,用於顯示Toast
TYPE_SYSTEM_OVERLAY=2006 系統頂層視窗,顯示在其他內容之上,此視窗不能獲得輸入焦點,否則影響鎖屏
TYPE_PRIORITY_PHONE=2007 當鎖屏時顯示的來電顯示視窗
TYPE_SYSTEM_DIALOG=2008 系統對話方塊
TYPE_KEYGUARD_DIALOG=2009 鎖屏時顯示的對話方塊
TYPE_SYSTEM_ERROR=2010 系統內部錯誤提示
TYPE_INPUT_METHOD=2011 輸入法視窗,顯示於普通應用/子視窗之上
TYPE_INPUT_METHOD_DIALOG=2012 輸入法中備選框對應的視窗
TYPE_WALLPAPER=2013 牆紙視窗
TYPE_STATUS_BAR_PANEL=2014 滑動狀態條後出現的視窗
TYPE_SECURE_SYSTEM_OVERLAY=2015 安全系統覆蓋視窗
…… ……
LAST_SYSTEM_WINDOW=2999 最後一個系統視窗





那麼,這個type層級到底有什麼作用呢?
Window是分層的,每個Window都有對應的z-ordered,(z軸,從1層層疊加到2999,你可以將螢幕想成三維座標模式)層級大的會覆蓋在層級小的Window上面。

在三類Window中,應用Window的層級範圍是1~99。子Window的層級範圍是1000~1999,系統Window的層級範圍是2000~2999,這些層級範圍對應著WindowManager.LayoutParams的type引數。如果想要Window位於所有Window的最頂層,那麼採用較大的層級即可。另外有些系統層級的使用是需要宣告許可權的。

Window的內部機制(Activity)

老樣子,靈魂畫手給你們繪製的整體關係圖…

這裡寫圖片描述

檢視大圖

Window是一個抽象的概念,每一個Window都對應著一個View和一個ViewRootImplWindowView通過ViewRootImpl來建立聯絡,因此Window並不是不存在的,它是以View的形式存在。這點從ViewManager的定義也可以看得出來,它只提供三個介面方法:addViewupdateViewLayoutremoveView,這些方法都是針對View的,所以說WindowView的直接管理者。

Window的建立過程

首先要分析Window的建立過程,就必須瞭解Activity的啟動過程
Activity的啟動過程很複雜,最終會由ActivityThread中的handleLaunchActivity()來完成整個啟動過程。
在這個方法中會通過performLaunchActivity()方法建立Activity,performLaunchActivity()內部通過類載入器建立Activity的例項物件,並呼叫其attach()方法為其關聯執行過程中所依賴的一系列上下文環境變數以及建立與繫結視窗
具體不在贅述,更多Activity啟動細節請移步Activity的啟動過程

//ActivityThread


private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ``````
    //獲取WindowManagerService的Binder引用(proxy端)。
    WindowManagerGlobal.initialize();

    //會呼叫Activity的onCreate,onStart,onResotreInstanceState方法
    Activity a = performLaunchActivity(r, customIntent);
    if (a != null) {
        ``````
        //會呼叫Activity的onResume方法.
        handleResumeActivity(r.token, false, r.isForward,
                !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

        ``````
    } 

}
//ActivityThread


   private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

        //通過類載入器建立Activity
        Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);

        ``````        

        //通過LoadedApk的makeApplication方法來建立Application物件
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);


        if (activity != 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);
            ``````

            //onCreate
            mInstrumentation.callActivityOnCreate(activity, r.state);

            //onStart
            activity.performStart();

        }
        return activity;
    }



在Activity的attach()方法裡,系統會建立Activity所屬的Window物件併為其設定回撥介面,由於Activity實現了Window的Callback介面,因此當Window接收到外界的狀態改變時就會回撥Activity的方法。Callback介面中的方法很多,下面舉幾個比較眼熟的方法。

 public interface Callback {

        public boolean dispatchTouchEvent(MotionEvent event);

        public View onCreatePanelView(int featureId);

        public boolean onMenuItemSelected(int featureId, MenuItem item);

        public void onContentChanged();

        public void onWindowFocusChanged(boolean hasFocus);

        public void onAttachedToWindow();

        public void onDetachedFromWindow();
    }
//Activity


final void attach(``````) {
        //繫結上下文
        attachBaseContext(context);

        //建立Window,PhoneWindow是Window的唯一具體實現類
        mWindow = new PhoneWindow(this, window);//此處的window==null,但不影響
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        ``````
        //設定WindowManager
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        //建立完後通過getWindowManager就可以得到WindowManager例項
        mWindowManager = mWindow.getWindowManager();//其實它是WindowManagerImpl

    }


    @Override
    public Object getSystemService(@ServiceName @NonNull String name) {
        ``````
        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        } 
        return super.getSystemService(name);
    }

等,等一下。看到這裡是不是有點懵,Activity的getSystemService根本沒有建立WindowManager,那麼mWindow.setWindowManager()設定的豈不是空的WindowManager,那這樣它的下一步mWindowManager = mWindow.getWindowManager()豈不是無線空迴圈?

因為PhoneWindow中並沒有setWindowManager()方法,所以我們開啟Window類看看。

//Window


    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }

        //在此處建立mWindowManager 
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }



//在WindowManagerImpl類中
    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

類似於PhoneWindowWindow的關係,WindowManager是一個介面,具體的實現是WindowManagerImpl



到了這裡Window已經建立完畢,在上面的performLaunchActivity()方法中我們可以看到呼叫了onCreate()方法:

//ActivityThread --> performLaunchActivity

            mInstrumentation.callActivityOnCreate(activity, r.state);

在Activity的onCreate方法裡,我們通過setContentView()將view新增到DecorView的mContentParent中,也就是將資源佈局檔案和phoneWindow關聯。
所以在PhoneWindow的最後,因為Activity實現了Callback介面,便可以通過下面程式碼通知Activity xml 佈局檔案已經新增到DecorView上。

//PhoneWindow  -->  setContentView

callback.onContentChanged();

經過了上面幾個過程,WindowDecorView已經被建立並初始化完畢,Activity的佈局檔案也成功新增到了DecorViewmContentParent中,但這個時候的DecorView還沒有被WindowManager正式新增到Window中。
.
這裡需要理解的是,Window更多表示的是一種抽象功能集合,雖然說早在Activityattach方法中window就已經被建立了,但是這個時候由於DecorView並沒有被WindowManager識別,所以這個時候的Window暫時無法提供具體功能。
.
總的來說,UI 佈局成功新增到 Window 並可用有2個標誌 :
.
①View繪製完畢,可以呈現給使用者
②View可以接收外界資訊(觸控事件等)

Window的新增過程

PhoneWindow 只是負責處理一些應用視窗通用的邏輯(設定標題欄,導航欄等)。但是真正完成把一個 View,作為視窗新增到 WmS 的過程是由 WindowManager 來完成的。WindowManager 的具體實現是 WindowManagerImpl。

下面我們繼續來分析handleLaunchActivity()方法中handleResumeActivity()的執行過程。

//ActivityThread


 final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

        //把activity資料記錄更新到ActivityClientRecord
        ActivityClientRecord r = mActivities.get(token);

        r = performResumeActivity(token, clearHide, reason);

        if (r != null) {

            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);//不可見
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;

             ``````
                if (a.mVisibleFromClient && !a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);//把decor新增到視窗上(劃重點)
                }

            } 
                //螢幕引數發生了改變
                performConfigurationChanged(r.activity, r.tmpConfig);

                WindowManager.LayoutParams l = r.window.getAttributes();

                    if (r.activity.mVisibleFromClient) {
                        ViewManager wm = a.getWindowManager();
                        View decor = r.window.getDecorView();
                        wm.updateViewLayout(decor, l);//更新視窗狀態
                    }


                ``````
                if (r.activity.mVisibleFromClient) {
                    //已經成功新增到視窗上了(繪製和事件接收),設定為可見
                    r.activity.makeVisible();
                }


            //通知ActivityManagerService,Activity完成Resumed
             ActivityManagerNative.getDefault().activityResumed(token);
        } 
    }

在上面程式碼中,首先配置ActivityClientRecord,之後將DecorView設定為INVISIBLE,因為View並未繪製完成,當前的DecorView只是一個有結構的空殼。
然後通過WindowManagerImpl將DecorView正式的新增到視窗上wm.addView(decor, l);,這一步非常非常重要,因為它包括了2個比較重要和常見的過程:Window的新增過程View的繪製流程

靈魂畫手一出手就知有沒有~~下面畫一張圖來看看整體新增流程,留一個大致的概括印象。

這裡寫圖片描述
檢視大圖

視窗的新增過程如上圖所示,我們知道 WmS 執行在單獨的程序中。這裡 IWindowSession 執行的 addtoDisplay 操作應該是 IPC 呼叫。接下來的Window新增過程,我們會知道每個應用視窗建立時,最終都會建立一個 ViewRootImpl 物件

ViewRootImpl 是一很重要的類,類似 ApplicationThread 負責跟AmS通訊一樣,ViewRootImpl 的一個重要職責就是跟 WmS 通訊,它通靜態變數 sWindowSession(IWindowSession例項)與 WmS 進行通訊。

每個應用程序,僅有一個 sWindowSession 物件,它對應了 WmS 中的 Session 子類,WmS 為每一個應用程序分配一個 Session 物件。WindowState 類有一個 IWindow mClient 引數,是由 Session 呼叫 addToDisplay 傳遞過來的,對應了 ViewRootImpl 中的 W 類的例項。

簡單的總結一下,ViewRootImpl通過IWindowSession遠端IPC通知WmS,並且由W類接收WmS的遠端IPC通知。(這個W類和ActivityThread的H類同樣精悍的命名,並且也是同樣的工作職責!)





圖解完Window的新增過程,對整個流程有一個印象和思路,那麼下面繼續分析原始碼。
在上面的handleResumeActivity()方法中,我們看到原始碼通過wm.addView(decor, l);操作DecorViewWindowManager.LayoutParams。上面講解也說過,因為WindowManager是介面,真正具體實現類是windowManagerImpl。如果Window中類的關係還不太清楚的可以再回到上面的「Window的內部機制」看圖解。

//WindowManagerImpl


    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

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

我們看到這個WindowManagerImpl原來也是一個吃空餉的傢伙!對於Window(或者可以說是View)的操作都是交由WindowManagerGlobal來處理,WindowManagerGlobal以工廠的形式向外提供自己的例項。這種工作模式是橋接模式,將所有的操作全部委託給WindowManagerGlobal來實現。

WindowManagerImpl的全域性變數中通過單例模式初始化了WindowManagerGlobal,也就是說一個程序就只有一個WindowManagerGlobal物件

//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) {
            //調整佈局引數,並設定token
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } 

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {


            int index = findViewLocked(view, false);
            if (index >= 0) {
                if (mDyingViews.contains(view)) {
                    //如果待刪除的view中有當前view,刪除它
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                }
                // The previous removeView() had not completed executing. Now it has.
                //之前移除View並沒有完成刪除操作,現在正式刪除該view
            }

            //如果這是一個子視窗個(popupWindow),找到它的父視窗。
            //最本質的作用是使用父視窗的token(viewRootImpl的W類,也就是IWindow)
            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);
                    }
                }
            }
            //建立ViewRootImpl,並且將view與之繫結
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);//將當前view新增到mViews集合中
            mRoots.add(root);//將當前ViewRootImpl新增到mRoots集合中
            mParams.add(wparams);//將當前window的params新增到mParams集合中
        }

          ``````
            //通過ViewRootImpl的setView方法,完成view的繪製流程,並新增到window上。
            root.setView(view, wparams, panelParentView);
    }

在上面程式碼中有2個比較重要的知識點

  • 1、在WindowManagerGlobal中有如下幾個重要的集合
    //儲存所有Window對應的View
    private final ArrayList<View> mViews = new ArrayList<View>();

    //儲存所有Window對應的ViewRootImpl
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();

    //儲存所有Window對應的佈局引數
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();

    //儲存正被刪除的View物件(已經呼叫removeView但是還未完成刪除操作的Window物件)     
    private final ArraySet<View> mDyingViews = new ArraySet<View>();
  • 2、token
    在原始碼中token一般代表的是Binder物件,作用於IPC程序間資料通訊。並且它也包含著此次通訊所需要的資訊,在ViewRootImpl裡,token用來表示mWindow(W類,即IWindow),並且在WmS中只有符合要求的token才能讓Window正常顯示。所以上面提出的問題
    我們能否在AcitivtyonCreate()中建立popupWindow並顯示呢?
    就與之有關,我們暫時把token放一邊,走完Window的新增過程先。(最後分析token)



言歸正傳,我們繼續看程式碼。
WindowManagerGlobaladdView()方法裡,最後呼叫ViewRootImplsetView方法,處理新增過程。

//WindowManagerGlobal  -->  addView


            //建立ViewRootImpl,並且將view與之繫結
            root = new ViewRootImpl(view.getContext(), display);

            //通過ViewRootImpl的setView方法,完成view的繪製流程,並新增到window上。
            root.setView(view, wparams, panelParentView);

通過上面這個程式碼可知,WindowManagerGlobal將View的處理操作全權交給ViewRootImpl,而且上面我們也提到了,View成功新增到Window,無非就是展現檢視和使用者互動。

①View繪製完畢,可以呈現給使用者
②View可以接收外界資訊(觸控事件等)

ViewRootImplsetView()方法中,將會完成上面2個艱鉅而又偉大的任務。

//ViewRootImpl 


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

                int res; 

                 //在 Window add之前呼叫,確保 UI 佈局繪製完成 --> measure , layout , draw
                requestLayout();//View的繪製流程 

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

                try {

                    //通過WindowSession進行IPC呼叫,將View新增到Window上
                    //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());

                ``````
    }

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

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