深入 Activity 三部曲(1)View 繪制流程之 setContentView() 到底做了什么 ?

UI 優化系列專題,來聊一聊 Android 渲染相關知識,主要涉及 UI 渲染背景知識如何優化 UI 渲染兩部內容。


UI 優化系列專題
  • UI 渲染背景知識

View 繪制流程之 setContentView() 到底做了什么?
View 繪制流程之 DecorView 添加至窗口的過程
深入 Activity 三部曲(3)View 繪制流程
Android 之 LayoutInflater 全面解析
關于渲染,你需要了解什么?
Android 之 Choreographer 詳細分析

  • 如何優化 UI 渲染

Android 之如何優化 UI 渲染(上)
Android 之如何優化 UI 渲染(下)


setContentView() 相信大家肯定不會感到陌生,幾乎每個 Activity 都會使用該方法為其添加一個 xml 布局界面。但是你真的了解 setContentView 方法嗎?為什么通過它就可以展示出我們添加的 xml 布局界面呢?

關于 Activity 的 View 加載過程大家肯定聽說過 Window、PhoneWindow、DecorView 等內容,它們之間是什么關系?我們先通過幾個問題來了解下。

相關問題
  1. setContentView 方法到底做了什么?為什么調用后可以顯示我們設置的布局?
  • PhoneWindow 是什么?它和 Window 是什么關系?
  • DecorView 是干什么用的?和我們添加的布局又有什么關系?
  • requestFeatrue 方法為什么要在 setContentView 方法之前?
  1. Layoutinflater 到底怎么把 XML 布局文件添加到 DecorView 上?
  • <include> 標簽為什么不能作為布局的根節點?
  • <merge> 標簽為什么要作為布局資源的根節點?
  • inflate( int resource, ViewGroup root, boolean attachToRoot) 參數 root 和 attachToRoot 的作用和規則?
  1. AppComatActivity 實現原理是怎樣的?它是如何完成布局兼容的?

如果以上問題你都能夠熟練并正確的回答出來,那么恭喜你可以直接跳過該篇文章了。


1. 從 setContentView 開始

打開 Activity 源碼找到 setContentView 方法如下:

public void setContentView(@LayoutRes int layoutResID) {
    //調用getWindow
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

getWindow 方法返回一個 Window 對象:

public Window getWindow(){
    return mWindow;
}

但是 Window 本質上是一個抽象類,在 Window 的源碼中對其介紹是這樣的:

Window 是一個顯示頂層窗口的外觀,包括一些基礎行為的封裝(如 findViewById()、事件分發 dispatch 等),而且每一個 Window 實例必須添加到 WindowManager 里面,它提供了標準的 UI 策略,比如背景、標題區域等。它的唯一實現類是 PhoneWindow

此時我們需要去跟蹤下 Window 的創建過程,翻閱 Activity 源碼發現在它的 attach 方法:

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) {
     //content實際類型是ContextImpl
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);
     //可以看到Window的類型是PhoneWindow
     //創建當前 Activity 的 Window 實例。
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);

    //... 省略

}

當啟動一個 Activity 時首先創建該 Activity 實例,隨后就會調用它的 attach 方法(這部分內容主要在 ActivityThread performLaunchActivity())。在 attach 方法中我們可以看到 Window 的實際類型是 PhoneWindow。

也就是當我們通過 Activity 的 setContentView 實際是調用了 PhoneWindow 的 setContentView 方法:

public void setContentView(int layoutResID) {
    //mContentParent是ViewGroup
    //我們的setContentView設置的布局實際就是被添加到該容器中 
    //它是我們添加布局文件的直接根視圖
    if (mContentParent == null) {
        //mContentParent默認為null
        //安裝當前DecorView
        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 {
        //解析布局資源,添加到mContentParent
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        //回調Activity的onContentChanged方法
        cb.onContentChanged();
    }
    //表示已經設置過布局
    //如果此時使用requestFeature則會拋出異常
    mContentParentExplicitlySet = true;
}
  • mContentParent 是一個 ViewGroup,它的實際類型是 FrameLayout,實際我們通過 setContentView 設置的 View 就被添加到該容器。也就是它是我們布局文件的直接父視圖(下面會分析到)。

  • 方法最后 mContentParentExplicitlySet,在 setContentView 方法執行完畢后置為 true,表示當前窗口已經設置完成。后面我們會分析道,如果此后在調用 requestFeature 方法設置 Window 窗口的 Feature 將會拋出異常。

mContentParent 默認為 null,此時執行 installDecor 方法,為當前 Window 創建 DecorView 視圖,DecorView 是我們整個 Activity 的最頂級視圖,它的實際類型是 FrameLayout:

private void installDecor() {
    mForceDecorInstall = false;
    //mDecor是DecorView,繼承自FrameLayout
    if (mDecor == null) {
        //為當前Window創建DecorView
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    //mContentParent默認為null
    if (mContentParent == null) {
        //找到當前主題布局中,內容的父容器
        mContentParent = generateLayout(mDecor);

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

      //... 省略
}

每個 Window 有且僅有一個 DecorView,DecorView 用來描述窗口的視圖,看下它的創建過程 generateDecor 方法如下:

protected DecorView generateDecor(int featureId) {
    Context context;
    //mUseDecorContext構造方法中默認置為true

    //表示使用DecorContext山下文
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            //使用DecorContext
            context = new DecorContext(applicationContext, getContext().getResources());
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        //使用應用程序上下文
        context = getContext();
    }
    //直接創建DecorView
    return new DecorView(context, featureId, this, getAttributes());
}

方法的最后可以看到直接創建 DecorView 并返回(DecorView 繼承自 FrameLayout)。回到 installDecor 方法,看下 setContentView 方法的直接父容器 mContentParent 的創建過程 generateLayout 方法(注意這時候 mContentParent 與 DecorView 還沒有任何關聯):

//我們給Window設置的相關屬性就是在generateLayout時加進來的
protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.

    //獲取當前Window的Style
    //這個是不是很熟悉,
    TypedArray a = getWindowStyle();

    //Window是否是Floating
    //浮窗類型時 Dialog 就是Floating 類型
    mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
    int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)
            & (~getForcedWindowFlags());
    if (mIsFloating) {
        setLayout(WRAP_CONTENT, WRAP_CONTENT);
        setFlags(0, flagsToUpdate);
    } else {
        setFlags(FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
    }

    //是否需要標題欄
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        //對Feature狀態為進行設置
        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);
    }

    if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
        requestFeature(FEATURE_ACTION_BAR_OVERLAY);
    }

    if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
        requestFeature(FEATURE_ACTION_MODE_OVERLAY);
    }

    if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
        requestFeature(FEATURE_SWIPE_TO_DISMISS);
    }

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

    if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation,
            false)) {
        setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION
                & (~getForcedWindowFlags()));
    }

    if (a.getBoolean(R.styleable.Window_windowOverscan, false)) {
        setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN & (~getForcedWindowFlags()));
    }

    if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
        setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER & (~getForcedWindowFlags()));
    }

    if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch,
            getContext().getApplicationInfo().targetSdkVersion
                    >= android.os.Build.VERSION_CODES.HONEYCOMB)) {
        setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH & (~getForcedWindowFlags()));
    }

    a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor);
    a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor);
    if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) {
        if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue();
        a.getValue(R.styleable.Window_windowFixedWidthMajor,
                mFixedWidthMajor);
    }
    if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) {
        if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue();
        a.getValue(R.styleable.Window_windowFixedWidthMinor,
                mFixedWidthMinor);
    }
    if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) {
        if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue();
        a.getValue(R.styleable.Window_windowFixedHeightMajor,
                mFixedHeightMajor);
    }
    if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) {
        if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue();
        a.getValue(R.styleable.Window_windowFixedHeightMinor,
                mFixedHeightMinor);
    }
    if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) {
        requestFeature(FEATURE_CONTENT_TRANSITIONS);
    }
    if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) {
        requestFeature(FEATURE_ACTIVITY_TRANSITIONS);
    }

    //這個地方是不是很熟悉,Window是否是透明的
    mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);

    /**以上都是對Window一些狀態進行設置*/
    //requestFeature為什么要在setContentView之前?

    final Context context = getContext();
    final int targetSdk = context.getApplicationInfo().targetSdkVersion;
    final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB;
    final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
    final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP;
    final boolean targetHcNeedsOptions = context.getResources().getBoolean(
            R.bool.target_honeycomb_needs_options_menu);
    final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE);

    if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) {
        setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE);
    } else {
        setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_FALSE);
    }

    if (!mForcedStatusBarColor) {
        mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
    }
    if (!mForcedNavigationBarColor) {
        mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
    }

    WindowManager.LayoutParams params = getAttributes();

    // Non-floating windows on high end devices must put up decor beneath the system bars and
    // therefore must know about visibility changes of those.
    if (!mIsFloating && ActivityManager.isHighEndGfx()) {
        if (!targetPreL && a.getBoolean(
                R.styleable.Window_windowDrawsSystemBarBackgrounds,
                false)) {
            setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
                    FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~getForcedWindowFlags());
        }
        if (mDecor.mForceWindowDrawsStatusBarBackground) {
            params.privateFlags |= PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND;
        }
    }
    if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
        decor.setSystemUiVisibility(
                decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
    }

    if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion
            >= android.os.Build.VERSION_CODES.HONEYCOMB) {
        if (a.getBoolean(
                R.styleable.Window_windowCloseOnTouchOutside,
                false)) {
            setCloseOnTouchOutsideIfNotSet(true);
        }
    }

    if (!hasSoftInputMode()) {
        params.softInputMode = a.getInt(
                R.styleable.Window_windowSoftInputMode,
                params.softInputMode);
    }

    if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
            mIsFloating)) {
        /* All dialogs should have the window dimmed */
        if ((getForcedWindowFlags() & WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
            params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
        }
        if (!haveDimAmount()) {
            params.dimAmount = a.getFloat(
                    android.R.styleable.Window_backgroundDimAmount, 0.5f);
        }
    }

    if (params.windowAnimations == 0) {
        params.windowAnimations = a.getResourceId(
                R.styleable.Window_windowAnimationStyle, 0);
    }

    // The rest are only done if this window is not embedded; otherwise,
    // the values are inherited from our container.
    if (getContainer() == null) {
        if (mBackgroundDrawable == null) {
            if (mBackgroundResource == 0) {
                mBackgroundResource = a.getResourceId(
                        R.styleable.Window_windowBackground, 0);
            }
            if (mFrameResource == 0) {
                mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0);
            }
            mBackgroundFallbackResource = a.getResourceId(
                    R.styleable.Window_windowBackgroundFallback, 0);
        }
        if (mLoadElevation) {
            mElevation = a.getDimension(R.styleable.Window_windowElevation, 0);
        }
        mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false);
        mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT);
    }

    // Inflate the window decor.

    /**生成對應的Window Decor 要根據當前設置的Features屬性
     * 加載不同的 DecorView 的xml布局*/
    int layoutResource;
    //獲取當前Window的Features
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
        setCloseOnSwipeEnabled(true);
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        // XXX Remove this once action bar supports these features.
        removeFeature(FEATURE_ACTION_BAR);
        // System.out.println("Title Icons!");
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
            && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
        // Special case for a window with only a progress bar (and title).
        // XXX Need to have a no-title version of embedded windows.
        layoutResource = R.layout.screen_progress;
        // System.out.println("Progress!");
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
        // Special case for a window with a custom title.
        // If the window is floating, we need a dialog layout
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogCustomTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_custom_title;
        }
        // XXX Remove this once action bar supports these features.
        removeFeature(FEATURE_ACTION_BAR);
    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
        // If no other features and not embedded, only need a title.
        // If the window is floating, we need a dialog layout
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
            layoutResource = a.getResourceId(
                    R.styleable.Window_windowActionBarFullscreenDecorLayout,
                    R.layout.screen_action_bar);
        } else {
            layoutResource = R.layout.screen_title;
        }
        // System.out.println("Title!");
    } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        layoutResource = R.layout.screen_simple_overlay_action_mode;
    } else {
        // 這是最簡單的一個,看下 DecorView 要加載布局文件是怎樣的?
        layoutResource = R.layout.screen_simple;
        // System.out.println("Simple!");
    }

    mDecor.startChanging();
    //將DecorView的xml文件添加到DecorView
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    //在DecorView對應的布局中,查找id為content的FrameLayout,該容器便是我們布局直接父容器
    ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }

    if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
        ProgressBar progress = getCircularProgressBar(false);
        if (progress != null) {
            progress.setIndeterminate(true);
        }
    }

    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        registerSwipeCallbacks(contentParent);
    }


    //... 省略

    mDecor.finishChanging();

    //返回DecorView對應布局中的id為content的FrameLayout
    //它實際上就是我們setContentView的直接根視圖
    return contentParent;
}

generateLayout 方法雖然較長,但是工作內容并不復雜,我們首先看 getWindowStyle 方法:

public final TypedArray getWindowStyle() {
    synchronized (this) {
        if (mWindowStyle == null) {
            //styleable是不是很熟悉,在一些自定義控件時經常用到
            mWindowStyle = mContext.obtainStyledAttributes(
                    com.android.internal.R.styleable.Window);
        }
        return mWindowStyle;
    }
}

styleable 是不是很熟悉,在一些自定義控件時經常會用到。實際上我們給 Window 設置的相關屬性就是在 generateLayout 方法進行設置的,例如非常熟悉和經常使用到的 :

//Window是否是Floating狀態
//Dialog時Window就是Floating
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);

//Window是否包含標題欄
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {

//Window是否是透明的
mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);

然后它們都會調用 requestFeature 方法對當前 Window 的 Feature 狀態位進行設置。

public boolean requestFeature(int featureId) {
    if (mContentParentExplicitlySet) {
        //在setContentView方法最后會將該標志位置為true,如果
        //在setCotnentView方法后再執行requestFeature將會拋出異常。
        throw new AndroidRuntimeException("requestFeature() must be called before adding content");
    }
    final int features = getFeatures();
    final int newFeatures = features | (1 << featureId);
    if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 &&
            (newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) {
        //不能既有自定義標題欄,又有其他標題欄
        throw new AndroidRuntimeException(
                "You cannot combine custom titles with other title features");
    }
    if ((features & (1 << FEATURE_NO_TITLE)) != 0 && featureId == FEATURE_ACTION_BAR) {
        return false; // Ignore. No title dominates.
    }
    if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_NO_TITLE) {
        //沒有標題欄
        removeFeature(FEATURE_ACTION_BAR);
    }
  //... 省略
}

注意方法中 if (mContentParentExplicitlySet) 如果滿足則直接拋出異常。該標志位在上面也有分析到, setContentView 方法最后會將其置為 true。即 requestFeature 方法必須在 setContentView 方法之前。那為什么要在 setContentView 方法之前呢?下面分析到。

要根據當前 Feature 加載不同的 DecorView 的 XML 布局文件。注意查看源碼中 generateLayout 方法的下半部分,我們以最簡單的 R.layout.screen_simple 布局為例:

<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>

可以看到布局本身就是一個 LinearLayout,包含上下兩部:標題欄 ViewStub 區域,內容區域 id 為 content 的 FrameLayout(這就是 Window 中的 mContentParent,即 setContentView 的直接父容器)。

然后將DecorView 對應的 xml 布局文件添加到 DecorView 中:

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

DecorView 的 onResourcesLoaded 方法如下:

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    mStackId = getStackId();
   //判斷當前DecorView是否包含DecorCaptionView
    mDecorCaptionView = createDecorCaptionView(inflater);
    //通過LayoutInflater完成xml布局加載
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        if (mDecorCaptionView.getParent() == null) {
            //DecorCaptionView也是DecorView子視圖
            addView(mDecorCaptionView,
                    new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
         //此時直接添加到DecorCaptionView中
        mDecorCaptionView.addView(root,
                new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
    } else {
        // 添加到DecorView中
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    initializeElevation();
}

也就是說,Window 會根據當前設置的 Feature 為 DecorView 添加一個對應的 xml 布局文件,該布局文件主要劃分上下兩部分,其中包含一個 id 為 content 的 FrameLayout,它會賦值給 PhoneWindow 中的 mContentParent,表示 setContentView 的的父容器

在 DecorView 中找到對應的 mContentParent(就是 id 為 content 的 FrameLayout):

ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);

在 generateLayout 方法最后返回該 contentParent,此時賦值給 PhoneWidow 的成員 mContentParent。

重新回到 PhoneWindow 的 setContentView 方法。將我們這是的布局文件添加 contentParent 過程如下:

// 解析布局資源,添加到mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);

至此,我們可以回答文章開頭的第一個問題了,先通過一張圖了解下 Activity 加載 UI - 類圖關系和視圖結構。

Activity加載UI-類圖關系和視圖結構
  • 每個 Activity 都有一個關聯的 Window 對象(該對象實際類型為 PhoneWindow,PhoneWindow 為 Window 的唯一實現類)用來描述應用程序窗口。

  • 每個窗口內部又包含了一個 DecorView 對象,DecorView 繼承自 FrameLayout;DecorView 用來描述窗口的視圖 — xml 布局(我們通過 setContentView 方法設置的 xml 布局最終被添加到該 DecorView 對應 xml 布局中 id 為 content 的 FrameLayout 中,下面分析到)。

  • 另外 requestFeature 必須要在 setContentView 方法之前,因為要根據該 Feature 為 DecorView 添加一個對應的 xml 布局文件;該布局包含上下兩部分,標題欄和內容區域(id 為 content 的 FrameLayout)。

2. LayoutInfalter 解析過程

需要回到 setContentView 方法,看下我們設置的布局是如何添加到 mContentParent 中:

//layoutResID通過setContentView設置的布局資源id
//mContentParent就是id為content的FrameLayout  
mLayoutInflater.inflate(layoutResID, mContentParent);

關于 LayoutInflater 大家肯定不會感到陌生,它可以將我們傳入的 xml 布局文件解析成對應的 View 對象。

/**
 * resource 表示當前布局資源id
 * root 表示布局的父容器,可以為null
 * attachToRoot 是否將布局資源添加到父容器root上
 * */
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();

    final XmlResourceParser parser = res.getLayout(resource);
    try {
        //重點看下inflate方法
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

首先通過 Resources 獲取一個 XML 資源解析器,我們重點關注 inflate 方法:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

        final Context inflaterContext = mContext;
        //獲取在XML設置的屬性
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        //注意root容器在這里,在我們當前分析中該root就是mContentParent
        View result = root;

        try {
            // 查找xml布局的根節點
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            //找到起始根節點
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }
            //獲取到節點名稱
            final String name = parser.getName();

            //判斷是否是merge標簽
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    //此時如果ViewGroup==null,與attachToRoot==false將會拋出異常
                    //merge必須添加到ViewGroup中,這也是merge為什么要作為布局的根節點,它要添加到上層容器中
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // 否則創建該節點View對象
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                //如果contentParent不為null,在分析setContentView中,這里不為null
                if (root != null) {
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }

                //解析Child
                rInflateChildren(parser, temp, attrs, true);

                if (root != null && attachToRoot) {
                    //添加到ViewGroup
                    root.addView(temp, params);
                }

                if (root == null || !attachToRoot) {
                    //此時布局根節點為temp
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            final InflateException ie = new InflateException(e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw i.e
        } catch (Exception e) {
            final InflateException ie = new InflateException(parser.getPositionDescription()
                    + ": " + e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw i.e
        } finally {
            // Don't retain static reference on context.
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        return result;
    }
}

while 循環部分,找到 xml 布局文件的根節點,如果 if (type != XmlPullParser.START_TAG) 未找到根節點直接拋異常了。否則獲取到該節點名稱,判斷如果是 merge 標簽,此時需要注意參數 root 和 attachToRoot,root 必須不為null,并且 attachToRoot 必須為 true,即 merge 內容必須要添加到 root 容器中

如果不是 merge 標簽,此時根據標簽名 name 直接創建該 View 對象,rInflate 和 rInflateChildren 都是去解析子 View,rInflateChildren 方法實際也是調用到了 rInflate 方法:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    //還是調用rInflate方法
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

區別在于最后一個參數 finishInflate,它的作用是標志當前 ViewGroup 樹創建完成后回調其 onFinishInflate 方法。

如果根標簽是 merge 此時 finishInflate 為 false,這也很容易理解,此時的父容器為 inflate 中傳入的 ViewGroup,它是不需要再次回調 onFinishInflate() 的。該過程如下:

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;

    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        //獲取到節點名稱
        final String name = parser.getName();

        if (TAG_REQUEST_FOCUS.equals(name)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            //include標簽
            if (parser.getDepth() == 0) {
                //include如果為根節點則拋出異常了
                //include不能作為布局文件的根節點
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            //如果此時包含merge標簽,此時也會拋出異常
            //merge只能作為布局文件的根節點
            throw new InflateException("<merge /> must be the root element");
        } else {
            //創建該節點的View對象
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;

            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            //添加到父容器
            viewGroup.addView(view, params);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        //回調ViewGroup的onFinishInflate方法
        parent.onFinishInflate();
    }
}

while 循環部分,parser.next() 獲取下一個節點,如果獲取到節點名為 include,此時 parse.getDepth() == 0 表示根節點,直接拋出異常,即 <include /> 不能作為布局的根節點

如果此時獲取到節點名稱為 merge,也是直接拋出異常了,即 <merge /> 只能作為布局的根節點

否則創建該節點對應 View 對象,rInflateChildren 遞歸完成以上步驟。并將解析到的 View 添加到其直接父容器 viewGroup.addView()。

注意方法的最后通知調用每個 ViewGroup 的 onFinishInflate(),大家是否有注意到這其實是入棧的操作,即最頂層的 ViewGroup 最后回調 onFinishInflate()。

至此,我們可以回答文章開頭提出的第二個問題了,再來通過一張流程圖熟悉下整個解析過程:

LayoutInflater 解析過程

在 inflater 解析布局資源過程中,首先找到布局的根節點 START_TAG,如果未找到直接拋出異常。否則獲取到當前節點的名稱。

  • 如果節點名稱為 merge ,會判斷 inflate 方法參數 if ( root(ViewGroup)!= null && attachToot == true ),表示布局文件要直接添加到 root 中,否則拋出異常(<merge /> can be used only with a valid ViewGroup root and attachToRoot=true);

  • 繼續解析子節點的過程中如果再次解析到 merge 標簽,則直接拋出異常,<merge /> 標簽必須作為布局的根節點(<merge /> must be the root element)。

  • 如果解析到節點名稱為 include,會判斷當前節點深度是否為 0,0 表示當前處于根節點,此時直接拋出異常,即 <include /> 不能作為布局文件的根節點(<include /> cannot be the root element)。

3. 偷梁換柱之為兼容而生的 AppCompatActivity

在 Android Level 21 之后,Android 引入了 Material Design 的設計,為了支持 Material Color、調色版、Toolbar 等各種新特性,AppCompatActivity 就應用而生。Google 考慮到仍然有很大部分低于 5.0 版本的設備,所有將 AppCompatActivity 放在了 support v7 包內。

接下來我們就看下 AppCompatActivity 是如何實現 UI 兼容設計的。

//AppCompatActivity的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

與 Activity 有所不同,AppCompatActivity 的 setContentView 方法中首先調用 getDelegate 方法得到一個代理對象。

public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        //創建AppCompatDelegate對象
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

創建當前 AppCompatDelegate 過程如下:

//AppCompatDelegate的create方法
private static AppCompatDelegate create(Context context, Window window,
        AppCompatCallback callback) {
    final int sdk = Build.VERSION.SDK_INT;
    if (BuildCompat.isAtLeastN()) {
        return new AppCompatDelegateImplN(context, window, callback);
    } else if (sdk >= 23) {
        return new AppCompatDelegateImplV23(context, window, callback);
    } else if (sdk >= 14) {
        return new AppCompatDelegateImplV14(context, window, callback);
    } else if (sdk >= 11) {
        return new AppCompatDelegateImplV11(context, window, callback);
    } else {
        return new AppCompatDelegateImplV9(context, window, callback);
    }
}

根據當前系統版本創建對應的 AppCompatDelegate 對象,AppCompatActivity 其實就是通過引入 AppCompatDelegate 來解決兼容問題。

這里需要說明的是,各 Delegate 實際根據版本由高到低繼承關系,即 AppCompatDelegateImplN extends AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 extends AppCompatDelegateImplV11 extends AppCompatDelegateImplV9。

setContentView 實際調用到 AppCompatDelegate 的第一個實現類 AppCompatDelegateImplV9 中:

public void setContentView(int resId) {
    //創建一個SubDecor
    ensureSubDecor();
   //獲取SubDecor中content區域,此時setContentView的直接父容器為SubDecor中id為content的FrameLayout
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    //將布局添加到SubDecor的Content區域
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    //回調到Activity的onContentChanged,
    mOriginalWindowCallback.onContentChanged();
}

ensureSubDecor 方法是要創建一個 SubDecor,SubDecor 實際與 PhoneWindow 中的 DecorView 類似,它的出現就是為了兼容布局。

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        //創建SubDecor,
        mSubDecor = createSubDecor();

        //是否設置了標題
        CharSequence title = getTitle();
        if (!TextUtils.isEmpty(title)) {
            onTitleChanged(title);
        }

        applyFixedSizeWindow();
        onSubDecorInstalled(mSubDecor);
        //表示當前Window已經安裝SubDecor
        mSubDecorInstalled = true;
        PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
        if (!isDestroyed() && (st == null || st.menu == null)) {
            invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
        }
    }
}

這里主要看下 SubDecor 的創建過程 createSubDecor 方法,該方法過程與 PhoneWindow 創建 DecorView 類似,區別是 Delegate 中找的都是 AppCompat 的屬性,也就是做的兼容相關的事情。

private ViewGroup createSubDecor() {
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

    if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
        a.recycle();
        throw new IllegalStateException(
                "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
    }

    if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
        //調用到Window中requestFeature
        requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
    }
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
        requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
    }
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
        requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
    }
    mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
    a.recycle();

    /**
      * 以上根據style設置Window的Feature
      * createSubDecor 方法的上半部分
      */

    //確保Window中已經安裝DecorView
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;


    //根據style加載SubDecor的xml布局
    if (!mWindowNoTitle) {
        //不需要標題欄類型窗口
        if (mIsFloating) {
            //Floating 類型窗口
            subDecor = (ViewGroup) inflater.inflate(
                    R.layout.abc_dialog_title_material, null);

            // Floating windows can never have an action bar, reset the flags
            mHasActionBar = mOverlayActionBar = false;
        } else if (mHasActionBar) {
            //含有 ActionBar
            TypedValue outValue = new TypedValue();
            mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);

            Context themedContext;
            if (outValue.resourceId != 0) {
                themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
            } else {
                themedContext = mContext;
            }

            //解析SubDecor對應的xml布局文件
            subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                    .inflate(R.layout.abc_screen_toolbar, null);
           //同樣包含一個id為content
            mDecorContentParent = (DecorContentParent) subDecor
                    .findViewById(R.id.decor_content_parent);
            mDecorContentParent.setWindowCallback(getWindowCallback());

            /**
             * Propagate features to DecorContentParent
             */
            if (mOverlayActionBar) {
                mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
            }
            if (mFeatureProgress) {
                mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
            }
            if (mFeatureIndeterminateProgress) {
                mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
            }
        }
    } else {
        if (mOverlayActionMode) {
            //解析DecorView對應xml布局文件
            subDecor = (ViewGroup) inflater.inflate(
                    R.layout.abc_screen_simple_overlay_action_mode, null);
        } else {
            //解析DecorView對應xml布局文件
            subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
        }

    //... 省略

    }

    if (subDecor == null) {
        throw new IllegalArgumentException(
                "AppCompat does not support the current theme features: { "
                        + "windowActionBar: " + mHasActionBar
                        + ", windowActionBarOverlay: "+ mOverlayActionBar
                        + ", android:windowIsFloating: " + mIsFloating
                        + ", windowActionModeOverlay: " + mOverlayActionMode
                        + ", windowNoTitle: " + mWindowNoTitle
                        + " }");
    }

    if (mDecorContentParent == null) {
        mTitleView = (TextView) subDecor.findViewById(R.id.title);
    }

    /**
      * 以下為 createSubDecor 方法的下半部分
      * 偷梁換柱過程,將 SubDecor 對應布局中 content 替換原 DecorView 中 id 為 content 的 FrameLayout。
      */
    // Make the decor optionally fit system windows, like the window's decor
    ViewUtils.makeOptionalFitsSystemWindows(subDecor);

    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);

    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    if (windowContentView != null) {
        // There might be Views already added to the Window's content view so we need to
        // migrate them to our content view
        while (windowContentView.getChildCount() > 0) {
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        }

        //這里很關鍵,將原來Window中id為content的FrameLayout(mContentParent)設置為NO_ID
        windowContentView.setId(View.NO_ID);
        //最新創建的SubDecor中內容區域FramLayout的id設置為content
        //偷梁換柱
        contentView.setId(android.R.id.content);

        // The decorContent may have a foreground drawable set (windowContentOverlay).
        // Remove this as we handle it ourselves
        if (windowContentView instanceof FrameLayout) {
            ((FrameLayout) windowContentView).setForeground(null);
        }
    }

    // 將新創建的SubDecor添加到DecorView的內容區域(mContentParent容器)
    mWindow.setContentView(subDecor);

    //... 省略

    return subDecor;
}

可以看到方法的上半部分,獲取一系列 AppCompat 兼容屬性,設置 Window 的 Feature 屬性;然后方法的中間部分,注意 mWindow.getDecorView() 作用是創建當前 Window 的 DecorView 整個過程(文章上面已經做了分析);然后根據 Feature 加載 SubDecor 對應的 xml 布局文件,這里我們以最簡單的 abc_screen_simple.xml 布局文件為例:

<android.support.v7.widget.FitWindowsLinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/action_bar_root"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  android:fitsSystemWindows="true">

  <android.support.v7.widget.ViewStubCompat
      android:id="@+id/action_mode_bar_stub"
      android:inflatedId="@+id/action_mode_bar"
      android:layout="@layout/abc_action_mode_bar"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />

  //這里包含一個FrameLayout
  <include layout="@layout/abc_screen_content_include" />

</android.support.v7.widget.FitWindowsLinearLayout>

abc_screen_simple.xml 布局文件中 include 一個 abc_screen_content_include.xml 布局文件,如下:

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <android.support.v7.widget.ContentFrameLayout
          android:id="@id/action_bar_activity_content"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:foregroundGravity="fill_horizontal|top"
          android:foreground="?android:attr/windowContentOverlay" />

</merge>

ContentFrameLayout 繼承自 FrameLayout,注意該 FrameLayout 最后會替代原 PhoneWindow 中 DecorView 對應布局內 id 為 content 的 FrameLayout。這一過程也是 AppCompatActivity 中偷梁換柱的核心內容,一起來看下這個重要過程,注意查看 createSubDecor 方法的下半部分:

獲取到當前 SubDecor 中 content 容器,也就是上面 include 布局內 ContentFrameLayout。

final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
        R.id.action_bar_activity_content);

然后拿到 PhoneWindow 中 DecorView 內 content 容器(mContentParent),注意這個原本是我們 setContentView() 的直接父容器。

final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);

重要的偷梁換柱過程:

    // 這里很關鍵,將原來Window中id為content的FrameLayout(mContentParent)設置為NO_ID
    windowContentView.setId(View.NO_ID);
    // 最新創建的SubDecor中內容區域ContentFramLayout的id設置為content
    // 偷梁換柱
    contentView.setId(android.R.id.content);

將 DecorView 中原 content 容器 id 置為 NO_ID,將 SubDecor 中 content 容器 id 置為 content。經過前面的分析我們知道通過 setContentView 添加布局最終會被添加到一個 id 為 content 的 FrameLayout,此時該 content 實際變為 SubDecor 中 ContentFrameLayout容器。偷梁換柱完成。

然后將 SubDecor 添加到 DecorView 中,此時原 DecorView 中 content 容器實際添加的是 SubDecor。

// 將新創建的SubDecor添加到DecorView的內容區域(mContentParent容器)
mWindow.setContentView(subDecor);

最后我們重新回到 Delegate 的 setContentView 方法,看下我們設置的布局如何添加到 SubDecor 中的 content (ContentFrameLayout)容器的:

// 將布局添加到SubDecor的Content區域
LayoutInflater.from(mContext).inflate(resId, contentParent)

通過一張結構圖了解下 AppCompatActivity 整個 UI 視圖關系,注意與前面分析的 Activity 做下比較,最主要的差別在 DecorView 的 content 容器。此時已經替換成了 SubDecor。

AppCompatActivity 的 View 布局結構

至此,我們可以回答文章開頭的第三個問題了。

  • AppCompatActivity 通過引入 AppCompatDelegate 來兼容不同版本的 Material Design 支持。

  • 在 AppCompatDelegate 中做了一個巧妙的偷梁換柱操作,即在原 DecorView 的 content 區域添加一個 SubDecor(兼容布局),我們通過 setContentView 設置的布局最終被添加到該 SubDecor 的 content 容器中,這樣完成布局兼容操作。

其實我們可以看出 Google 工程師在處理兼容時也很“暴力”,這也是沒有辦法的辦法,因為之前挖的坑太多了,這么多版本,為了做兼容費了很多心思,有些心思設計的也非常巧妙。

總結

每個 Activity 都有一個關聯的 Window 對象,用來描述應用程序窗口,每個窗口內部又包含一個 DecorView 對象,DecorView 對象用來描述窗口的視圖 — xml 布局。

AppCompatActivity 在 DecorView 中又添加了一個 SubDecor 視圖 — xml 布局,解決布局兼容性問題。


以上便是個人在學習 View 的加載過程心得和體會,文中如有不妥或有更好的分析結果,歡迎大家指出。

文章如果對你有幫助,請留個贊吧!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容