一文讀懂 View & Window 機制

公衆號:字節數組,但願對你有所幫助 😇😇java

Android 系統中,Window 在代碼層次上是一個抽象類,在概念上表示的是一個窗口。Android 中全部的視圖都是經過 Window 來呈現的,例如 Activity、Dialog 和 Toast 等,它們實際上都是掛載在 Window 上的。大部分狀況下應用層開發者不多須要來和 Window 打交道,Activity 已經隱藏了 Window 的具體實現邏輯了,但我以爲來了解 Window 機制的一個比較大的好處是能夠加深咱們對 View 繪製流程以及事件分發機制的瞭解,這兩個操做就涉及到咱們的平常開發了,實現自定義 View 和解決 View 的滑動衝突時都須要咱們掌握這方面的知識點,而這兩個操做和 Window 機制有很大的關聯。視圖樹只有被掛載到 Window 後纔會觸發視圖樹的繪製流程,以後視圖樹纔有機會接收到用戶的觸摸事件。也就是說,視圖樹被掛載到了 Window 上是 Activity 和 Dialog 可以展現到屏幕上且和用戶作交互的前置條件git

本文就以 Activity 爲例子,展開講解 Activity 是如何掛載到 Window 上的,基於 Android API 30 進行分析,但願對你有所幫助 😇😇github

1、Window

Window 存在的意義是什麼呢?數組

大部分狀況下,用戶都是在和應用的 Activity 作交互,應用在 Activity 上接收用戶的輸入並在 Activity 上向用戶作出交互反饋。例如,在 Activity 中顯示了一個 Button,當用戶點擊後就會觸發 OnClickListener,這個過程當中用戶就是在和 Activity 中的視圖樹作交互,此時尚未什麼問題。但是,當須要在 Activity 上彈出 Dialog 時,系統須要確保 Dialog 是會覆蓋在 Activity 之上的,有觸摸事件時也須要確保 Dialog 是先於 Activity 接收到的;當啓動一個新的 Activity 時又須要覆蓋住上一個 Activity 以及其顯示的 Dialog;在彈出 Toast 時,又須要確保 Toast 是覆蓋在 Activity 和 Dialog 之上的markdown

這種種要求就涉及到了一個層次管理問題,系統須要對當前屏幕上顯示的多個視圖樹進行統一管理,這樣才能來決定不一樣視圖樹的顯示層次以及在接收觸摸事件時的優先級。系統就經過 Window 這個概念來實現上述目的app

想要在屏幕上顯示一個 Window 並不算多複雜,代碼大體以下所示ide

private val windowManager by lazy {
        context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    }

    private val floatBallView by lazy {
        FloatBallView(context)
    }

    private val floatBallWindowParams: WindowManager.LayoutParams by lazy {
        WindowManager.LayoutParams().apply {
            width = FloatBallView.VIEW_WIDTH
            height = FloatBallView.VIEW_HEIGHT
            gravity = Gravity.START or Gravity.CENTER_VERTICAL
            flags =
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
            type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
                WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
            } else {
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            }
        }
    }

    fun showFloatBall() {
        windowManager.addView(floatBallView, floatBallWindowParams)
    }
複製代碼

顯示一個 Window 最基本的操做流程有:函數

  1. 聲明但願顯示的 View,即本例子中的 floatBallView,其承載了咱們但願用戶看到的視圖界面
  2. 聲明 View 的位置參數和交互邏輯,即本例子中的 floatBallWindowParams,其規定了 floatBallView 在屏幕上的位置以及和用戶的交互邏輯
  3. 經過 WindowManager 來添加 floatBallView,從而將 floatBallView 掛載到 Window 上,WindowManager 是外界訪問 Window 的入口

當中,WindowManager.LayoutParams 的 flags 屬性就用於控制 Window 的顯示特性和交互邏輯,常見的有如下幾個:oop

  1. WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE。表示當前 Window 不須要獲取焦點,也不須要接收各類按鍵輸入事件,按鍵事件會直接傳遞給下層具備焦點的 Window佈局

  2. WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL。表示當前 Window 區域的單擊事件但願本身處理,其它區域的事件則傳遞給其它 Window

  3. WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED。表示當前 Window 但願顯示在鎖屏界面

此外,WindowManager.LayoutParams 的 type 屬性就用於表示 Window 的類型。Window 有三種類型:應用 Window、子 Window、系統 Window。應用類Window 對應 Activity。子 Window 具備依賴關係,不能單獨存在,須要附屬在特定的父 Window 之中,好比 Dialog 就是一個子 Window。系統 Window 是須要權限才能建立的 Window,好比 Toast 和 statusBar 都是系統 Window

從這也能夠看出,系統 Window 是處於最頂層的,因此說 type 屬性也用於控制 Window 的顯示層級,顯示層級高的 Window 就會覆蓋在顯示層級低的 Window 之上。應用 Window 的層級範圍是 1~99,子 Window 的層級範圍是 1000~1999,系統 Window 的層級範圍是 2000~2999。若是想要讓咱們建立的 Window 位於其它 Window 之上,那麼就須要使用比較大的層級值了,但想要顯示自定義的系統級 Window 的話就必須向系統申請權限了

WindowManager.LayoutParams 內就聲明瞭這些層級值,咱們能夠擇需選取。例如,系統狀態欄自己也是一個 Window,其 type 值就是 TYPE_STATUS_BAR

public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {

        public int type;

        //應用 Window 的開始值
        public static final int FIRST_APPLICATION_WINDOW = 1;
        //應用 Window 的結束值
        public static final int LAST_APPLICATION_WINDOW = 99;

        //子 Window 的開始值
        public static final int FIRST_SUB_WINDOW = 1000;
        //子 Window 的結束值
        public static final int LAST_SUB_WINDOW = 1999;

        //系統 Window 的開始值
        public static final int FIRST_SYSTEM_WINDOW = 2000;
        //系統狀態欄
        public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
        //系統 Window 的結束值
        public static final int LAST_SYSTEM_WINDOW = 2999;
        
    }
複製代碼

2、WindowManager

每一個 Window 都會關聯一個 View,想要顯示 Window 也離不開 WindowManager,WindowManager 就提供了對 View 進行操做的能力。WindowManager 自己是一個接口,其又繼承了另外一個接口 ViewManager,WindowManager 最基本的三種操做行爲就由 ViewManager 來定義,即添加 View、更新 View、移除 View

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 的實現類是 WindowManagerImpl,其三種基本的操做行爲都交由了 WindowManagerGlobal 去實現,這裏使用到了橋接模式

public final class WindowManagerImpl implements WindowManager {
    
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                mContext.getUserId());
    }

    @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);
    }
    
}
複製代碼

這裏主要看下 WindowManagerGlobal 是如何實現 addView 方法的便可

首先,WindowManagerGlobal 會對入參參數進行校驗,並對 LayoutParams 作下參數調整。例如,若是當前要顯示的是子 Window 的話,那麼就須要使其 LayoutParams 遵循父 Window 的要求才行

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow, int userId) {
        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);
        } 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;
            }
        }
        ···
    }
複製代碼

以後就會爲當前的視圖樹(即 view)構建一個關聯的 ViewRootImpl 對象,經過 ViewRootImpl 來繪製視圖樹並完成 Window 的添加過程。ViewRootImpl 的 setView方法會觸發啓動整個視圖樹的繪製流程,即完成視圖樹的 Measure、Layout、Draw 流程,具體流程能夠看個人另外一篇文章:一文讀懂 View 的 Measure、Layout、Draw 流程

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow, int userId) {
        ···

        ViewRootImpl root;
        View panelParentView = null;

        ···

        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
        try {
            //啓動和 view 關聯的整個視圖樹的繪製流程
            root.setView(view, wparams, panelParentView, userId);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
        }
    }
複製代碼

ViewRootImpl 內部最終會經過 WindowSession 來完成 Window 的添加過程,mWindowSession 是一個 Binder 對象,真正的實現類是 Session,也就是說,Window 的添加過程涉及到了 IPC 調用。後面就比較複雜了,能力有限就不繼續看下去了

mOrigWindowType = mWindowAttributes.type;
        mAttachInfo.mRecomputeGlobalAttributes = true;
        collectViewAttributes();
        adjustLayoutParamsForCompatibility(mWindowAttributes);
        res = mWindowSession.addToDisplayAsUser(
            mWindow, mSeq, mWindowAttributes,
            getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
            mAttachInfo.mDisplayCutout, inputChannel,
            mTempInsets, mTempControls
        );
        setFrame(mTmpFrame);
複製代碼

須要注意的是,這裏所講的視圖樹表明的是不少種不一樣的視圖形式。在啓動一個 Activity 或者顯示一個 Dialog 的時候,咱們都須要爲它們指定一個佈局文件,佈局文件會經過 LayoutInflater 加載映射爲一個具體的 View 對象,即最終 Activity 和 Dialog 都會被映射爲一個 View 類型的視圖樹,它們都會經過 WindowManager 的 addView 方法來顯示到屏幕上,WindowManager 對於 Activity 和 Dialog 來講具備統一的操做行爲入口

3、Activity & Window

這裏就以 Activity 爲例子來展開講解 Window 相關的知識點,因此也須要先對 Activity 的組成結構作個大體的介紹。Activity 和 Window 之間的關係能夠用如下圖片來表示

  1. 每一個 Activity 均包含一個 Window 對象,即 Activity 和 Window 是一對一的關係

  2. Window 是一個抽象類,其惟一的實現類是 PhoneWindow

  3. PhoneWindow 內部包含一個 DecorView,DecorView 是 FrameLayout 的子類,其內部包含一個 LinearLayout,LinearLayout 中又包含兩個自上而下的 childView,即 ActionBar 和 ContentParent。咱們平時在 Activity 中調用的 setContentView 方法實際上就是在向 ContentParent 執行 addView 操做

Window 這個抽象類裏定義了多個和 UI 操做相關的方法,咱們平時在 Activity 中調用的setContentViewfindViewById方法都會被轉交由 Window 來實現,Window 是 Activity 和視圖樹系統交互的入口。例如,其 getDecorView() 方法就用於獲取內嵌的 DecorView,findViewById() 方法就會將具體邏輯轉交由 DecorView 來實現,由於 DecorView 纔是真正包含 contentView 的容器類

public abstract class Window {
    
    public Window(Context context) {
        mContext = context;
        mFeatures = mLocalFeatures = getDefaultFeatures(context);
    }
    
    public abstract void setContentView(@LayoutRes int layoutResID);

    @Nullable
    public <T extends View> T findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }
    
    public abstract void setTitle(CharSequence title);
    
    public abstract @NonNull View getDecorView();
    
    ···
    
}
複製代碼

4、Activity # setContentView

每一個 Activity 內部都包含一個 Window 對象 mWindow,在 attach 方法中完成初始化,這說明 Activity 和 Window 是一對一的關係。mWindow 對象對應的是 PhoneWindow 類,這也是 Window 的惟一實現類

public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2, Window.OnWindowDismissedCallback, AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {
            
    @UnsupportedAppUsage
    private Window mWindow;

    @UnsupportedAppUsage
    private WindowManager mWindowManager;
               
    @UnsupportedAppUsage
    final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        //初始化 mWindow
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(mWindowControllerCallback);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        ···
    }
    
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
              
}
複製代碼

Activity 的attach 方法又是在 ActivityThread 的 performLaunchActivity 方法中被調用的,在經過反射生成 Activity 實例後就會調用attach 方法,且能夠看到該方法的調用時機是早於 Activity 的 onCreate 方法的。因此說,在生成 Activity 實例後不久其 Window 對象就已經被初始化了,並且早於各個生命週期回調函數

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ···
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                        "Unable to instantiate activity " + component
                                + ": " + e.toString(), e);
            }
        }

        ···
            
        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, r.configCallback,
                r.assistToken);

        ···
            
        if (r.isPersistable()) {
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else {
            mInstrumentation.callActivityOnCreate(activity, r.state);
        }
        return activity;
    }
複製代碼

此外,從 Activity 的setContentView 的方法簽名來看,具體邏輯都交由了 Window 的同名方法來實現,傳入的 layoutResID 就是咱們但願在屏幕上呈現的佈局,那麼 PhoneWindow 天然就須要去加載該佈局文件生成對應的 View。而爲了可以有一個對 View 進行統一管理的入口,View 應該要包含在一個指定的 ViewGroup 中才行,該 ViewGroup 指的就是 DecorView

5、PhoneWindow # setContentView

PhoneWindow 的 setContentView 方法的邏輯能夠總結爲:

  1. PhoneWindow 內部包含一個 DecorView 對象 mDecor。DecorView 是 FrameLayout 的子類,其內部包含兩個咱們常常會接觸到的 childView:actionBar 和 contentParent,actionBar 即 Activity 的標題欄,contentParent 即 Activity 的視圖內容容器
  2. 若是 mContentParent 爲 null 的話則調用 installDecor() 方法來初始化 DecorView,從而同時初始化 mContentParent;不爲 null 的話則移除 mContentParent 的全部 childView,爲 layoutResID 騰出位置(不考慮轉場動畫,實際上最終的操做都同樣)
  3. 經過LayoutInflater.inflate生成 layoutResID 對應的 View,並將其添加到 mContentParent 中,從而將咱們的目標視圖掛載到一個統一的容器中(不考慮轉場動畫,實際上最終的操做都同樣)
  4. 當 ContentView 添加完畢後會回調 Callback.onContentChanged 方法,咱們能夠經過重寫 Activity 的該方法從而獲得佈局內容改變的通知

因此說,Activity 的 setContentView 方法實際上就是在向 DecorView 的 mContentParent 執行 addView 操做,因此該方法才叫setContentView而非setView

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    
    private DecorView mDecor;

    ViewGroup mContentParent;
    
    @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            //將 layoutResID 對應的 View 添加到 mContentParent 中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            //回調通知 contentView 發生變化了
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
    
    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

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

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                ···
            } else {
                ···
            }
            ···
        }
    }
    
}
複製代碼

mContentParent 經過 generateLayout 方法來完成初始化,該方法的邏輯能夠分爲三步:

  1. 讀取咱們爲 Activity 設置的 theme 屬性,以此配置基礎的 UI 風格。例如,若是咱們設置了 <item name="windowNoTitle">true</item>的話,那麼就會執行 requestFeature(FEATURE_NO_TITLE) 來隱藏標題欄
  2. 根據 features 來選擇合適的佈局文件,獲得 layoutResource。之因此會有多種佈局文件,是由於不一樣的 Activity 會有不一樣的顯示要求,有的要求顯示 title,有的要求顯示 leftIcon,而有的可能全都不須要,爲了不控件冗餘就須要來選擇合適的佈局文件。而雖然每種佈局文件結構上略有不一樣,但均會包含一個 ID 名爲content的 FrameLayout,mContentParent 就對應該 FrameLayout
  3. DecorView 會拿到 layoutResource 生成對應的 View 對象並添加爲本身的 childView,對應 DecorView 中的 mContentRoot,後續執行的 findViewById(ID_ANDROID_CONTENT) 操做就都是交由 DecorView 來實現的了,而正常來講每種 layoutResource 都會包含一個 ID 爲 content的 FrameLayout,若是發現找不到的話就直接拋出異常,不然就成功返回拿到 mContentParent
protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        TypedArray a = getWindowStyle();

        ···
		
        //第一步
        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }

        ···

        //第二步
        int layoutResource;
        ···
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        //第三步
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        ···
        return contentParent;
    }
複製代碼

6、DecorView

DecorView 是 FrameLayout 的子類,其 onResourcesLoaded 方法在拿到 PhoneWindow 傳遞過來的 layoutResource 後,就會生成對應的 View 並添加爲本身的 childView,就像普通的 ViewGroup 執行 addView 方法同樣,該 childView 就對應 mContentRoot,咱們能夠在 Activity 中經過(window.decorView as ViewGroup).getChildAt(0)來獲取到 mContentRoot

因此 DecorView 能夠看作是 Activity 中整個視圖樹的根佈局

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    
    @UnsupportedAppUsage
    private PhoneWindow mWindow;
    
    ViewGroup mContentRoot;
    
    DecorView(Context context, int featureId, PhoneWindow window,
            WindowManager.LayoutParams params) {
        ···
    }
    
    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        if (mBackdropFrameRenderer != null) {
            loadBackgroundDrawablesIfNeeded();
            mBackdropFrameRenderer.onResourcesLoaded(
                    this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
                    getCurrentColor(mNavigationColorViewState));
        }
        mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {
            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }
    
}
複製代碼

7、ActivityThread

完成以上步驟後,此時其實還只是完成了 Activity 整個視圖樹的加載工做,雖然 Activity 的 attach方法已經建立了 Window 對象,但還須要將 DecorView 提交給 WindowManager 後才能正式將視圖樹展現到屏幕上

DecorView 具體的提交時機還須要看 ActivityThread 的 handleResumeActivity 方法,該方法用於回調 Activity 的 onResume 方法,裏面還會回調到 Activity 的makeVisible 方法,從方法名能夠猜出來makeVisible方法就用於令 Activity 變爲可見狀態

@Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
        ···
        r.activity.makeVisible();    
        ···    
    }
複製代碼

makeVisible 方法會判斷當前 Activity 是否已經將 DecorView 提交給 WindowManager 了,若是還沒的話就進行提交,最後將 DecorView 的可見狀態設爲 VISIBLE,至此才創建起 Activity 和 WindowManager 之間的關聯關係,以後 Activity 才正式對用戶可見

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

8、作下總結

對以上流程作下總結

  1. 每一個 Activity 內部都包含一個 Window 對象,Activity 的 setContentViewfindViewById 等操做都會交由 Window 來實現,Window 是 Activity 和整個 View 系統交互的入口
  2. PhoneWindow 是 Window 這個抽象類的的惟一實現類,Activity 和 Dialog 內部都是經過 PhoneWindow 來加載視圖樹,將具體的視圖樹處理邏輯交由 PhoneWindow 實現,並經過 PhoneWindow 和視圖樹進行交互,所以 PhoneWindow 成爲了上層類和視圖樹系統之間的交互入口,從而也將 Activity 和 Dialog 共同的視圖邏輯給抽象出來了,減輕了上層類的負擔,這也是 Window 機制存在的好處之一
  3. PhoneWindow 根據 theme 和 features 得知 Activity 的基本視圖屬性,由此來選擇合適的根佈局文件 layoutResource,每種 layoutResource雖然在佈局結構上略有不一樣,可是均會包含一個 ID 名爲content的 FrameLayout,contentParent 即該 FrameLayout。咱們能夠經過 Window.ID_ANDROID_CONTENT來拿到該 ID,也能夠在 Activity 中經過 findViewById<View>(Window.ID_ANDROID_CONTENT) 來獲取到contentParent
  4. PhoneWindow 並不直接管理視圖樹,而是交由 DecorView 去管理。DecorView 會根據layoutResource來生成對應的 rootView 並將開發者指定的 contentView 添加爲contentParent的 childView,因此能夠將 DecorView 看作是視圖樹的根佈局。正由於如此,Activity 的 findViewById 操做實際上會先交由 Window,Window 再交由 DecorView 去完成,由於 DecorView 纔是實際持有 contentView 的容器類
  5. View 和 ViewGroup 共同組成一個具體的視圖樹,視圖樹的根佈局則是 DecorView,DecorView 的存在使得視圖樹有了一個統一的容器,有利於統一系統的主題樣式並對全部 childView 進行統一管理
  6. Activity 的 DecorView 是在makeVisible 方法裏提交給 WindowManager 的,以後 WindowManagerImpl 會經過 ViewRootImpl 來完成整個視圖樹的繪製流程,以後 Activity 才正式對用戶可見

9、一個 Demo

這裏我也提供一個自定義 Window 的 Demo,實現了基本的拖拽移動和點擊事件,代碼點擊這裏:AndroidOpenSourceDemo

10、一文系列

最近比較傾向於只用一篇文章來寫一個知識點,也懶得老是想文章標題,就一直沿用一開始用的一文讀懂XXX,寫着寫着也攢了蠻多篇文章了,以前也已經寫了幾篇關於 View 系統的文章,但願對你有所幫助 😇😇

相關文章
相關標籤/搜索