從原始碼角度剖析 setContentView() 背後的機制

jokermonn發表於2019-02-26

注:本文基於 AS 2.3,示例中的 Activity 繼承自 AppcompatActivity。

示例


日常開發中,我們在 Activity 中基本上不可避免的都會使用到 setContentView() 這行程式碼,而理解它背後的機制能夠讓我們對日常的優化有更深地理解,網上也有些許文章介紹該機制,但是大部分的文章都是基於應用中 Activity 繼承自 Activity,而早從 API 7 開始,google 就建議我們繼承自 AppcompatActivity 而不是 Acitivity 了,雖然說從原始碼角度上本質上區別可能不是很大,但是還是有必要重新整理一遍思路。下面我們就從示例開始著手,一步一步理解 setContentView() 背後到底幹了些什麼,首先我們建立一個最普通的應用,利用 Android Studio 給我們建立的最開始就好了,MainAcitivty 程式碼如下 ——

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}複製程式碼

activity_main.xml 程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.joker.delete.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</android.support.constraint.ConstraintLayout>複製程式碼

對於 Theme 我們也不做任何的更改 ——

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>複製程式碼

然後我們啟動應用就行了,啟動好應用後我們就來看一看我們當前應用的檢視結構,如果你是老版本的 SDK,在 SDK 的 tools 目錄下還存在有 hierarchyviewer.bat 這個檔案的話,你就開啟它來檢視我們的應用檢視,而如果你是新版本的 SDK 的話,需要使用 Layout Inspector 來檢視當前應用的檢視結構(如果你還不知道 Layout Inspector 的話,可以看一下我的另一篇部落格Layout Inspector —— Android Studio 替代 Hierarchy Viewer 的新方案),而筆者的 SDK 是最新版本,所以筆者使用的是 Layout Inspector。開啟後,我們可以看到類似如下的目錄結構 ——

從原始碼角度剖析 setContentView() 背後的機制
Layout Inspector

左側就是我們的應用檢視結構了,而點選相應的控制元件後,右側會出現控制元件的相關屬性,有一個行叫做 mID,顧名思義,他就是該控制元件的 id 值,這裡先提一下,我們後面會用到。我們可以看到左邊的檢視結構是這樣的 ——

從原始碼角度剖析 setContentView() 背後的機制
應用檢視目錄

其中第七行的 ConstrainLayout 是 activity_main.xml 最外層的控制元件,為了更直觀一些,我畫了如下一張圖 ——

從原始碼角度剖析 setContentView() 背後的機制
應用檢視目錄

原始碼解析

AppCompatDelegateImplV7


程式碼剖析的起點就是我們 MainActivity 中的 setContentView(),點進去我們可以看到它進入了 AppcompatActivity 中的 setContentView() 方法中 ——

從原始碼角度剖析 setContentView() 背後的機制
AppcompatActivity#setContentView()

我們可以看到,其內部是呼叫了 AppcompatDeletegate 的 setContentView 方法,我們再不妨點進去 ——

從原始碼角度剖析 setContentView() 背後的機制
AppcompatDeletegate#setContentView()

原來 AppcompatDeletegate 是一個抽象類,setContentView() 又是一個抽象方法,那麼我們就來看看它的實現類有哪些 ——

從原始碼角度剖析 setContentView() 背後的機制
AppcompatDeletegate 實現類

看來它的子類還是挺多的,實際上 setContentView() 方法在 AppcompatDelegateImplV7 中實現的,另外這裡透露一個小技巧,我們可以看到 AppcompatDeletegate 的抽象方法 setContentView() 方法前面有一個綠色的向下箭頭,它實際上就是告訴你有那些子類實現了這個方法,就像下面這樣 ——

從原始碼角度剖析 setContentView() 背後的機制
快捷方式

我們可以直接點選綠色的箭頭這樣就進入了 AppcompatDelegateImplV7 中的 setContentView() 方法,原始碼如下 ——

從原始碼角度剖析 setContentView() 背後的機制
AppcompatDelegateImplV7#setContentView()

我們可以看到程式碼非常的簡單,其關鍵點也就是在277、278、280三行,277行程式碼看方法名就是要確定能夠構造出一個 SubDecor,這樣就能確保 278 行的程式碼不會丟擲異常,而 280 行程式碼就可以看得出來是將我們傳入的引數,也就是 resId 所引用的那個佈局放入 contentParent 中,而經過上面的分析,我們應該清楚,這個 contentParent 就是 ContentFrameLayout,我們也可以看到,這裡的 contentParent 是通過 findViewId() 這個方法獲取到的,而這個 id 正是 android.R.id.content,這和我們上面的分析圖中所說的 ContentFrameLayout 的 id 是 content 相符合,而 mSubDecor 應該是一個內部包含有一個 ContentFrameLayout 的 ViewGroup。那麼現在我們就來看看 ensureSubDecor() 方法,看看它內部都做了些什麼 ——

從原始碼角度剖析 setContentView() 背後的機制
AppcompatDelegateImplV7#ensureSubDecor()

很明顯我們會進入到 312 行的 createSubDecor() 方法中(事實上我們也只需要研究這個方法) ——

private ViewGroup createSubDecor() {
    //...

    // Now let's make sure that the Window has installed its decor by retrieving it
    // [1]
    mWindow.getDecorView();

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


    if (!mWindowNoTitle) {
        if (mIsFloating) {
            // ...
        } else if (mHasActionBar) {
            // Now inflate the view using the themed context and set it as the content view
            // [2]
            subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                    .inflate(R.layout.abc_screen_toolbar, null);

            // [3]
            mDecorContentParent = (DecorContentParent) subDecor
                    .findViewById(R.id.decor_content_parent);

            // ...
        }
    }

    // [4]
    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);
        }

        // Change our content FrameLayout to use the android.R.id.content id.
        // Useful for fragments.
        windowContentView.setId(View.NO_ID);
        // [5]
        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);
        }
    }

    // Now set the Window's content view with the decor
    // [6]
    mWindow.setContentView(subDecor);

    // ...

    return subDecor;
}複製程式碼

由於原始碼太長,筆者擷取了比較重要的一部分程式碼,我們先看到12行的 if 語句判斷處,我們示例的程式碼會進入第15行的 else if 判斷語句中,如果不夠確定的話,我們也可以 debug 看一下 ——

從原始碼角度剖析 setContentView() 背後的機制
debug

那麼進入了這個 if 判斷語句中做了什麼事情呢?我們可以看到 [2] 處,它通過 inflate() 方法引入一個佈局檔案建立了 subDecor 並最終返回了這個 subDecor,那麼根據前面的分析很明顯這個 subDecor 應該是一個包含有一個 ContentFrameLayout 的 ViewGroup,事實上是不是這樣的呢?我們不妨開啟這個名為 R.layout.abc_screen_toolbar.xml 的佈局檔案,如下 ——

從原始碼角度剖析 setContentView() 背後的機制
R.layout.abc_screen_toolbar.xml

好像沒有 ContentFrameLayout,不要急,我們看到其中使用了一個 include 標籤引入了一個 abc_screen_content_include.xml 的佈局,那麼它又長什麼樣呢?如下 ——

從原始碼角度剖析 setContentView() 背後的機制
abc_screen_content_include.xml

沒錯,這就是上面所說的 ContentFrameLayout,與此同時我們發現上面檢視結構中的前三個佈局檔案 ActionBarContainer、ContentFrameLayout、ActionBarOverlayLayout 都在這,原來我們應用中引入的就是這個佈局檔案,但是細心的小夥伴也發現了,此處的 ActionBarContainer 和 ActionBarOverlayLayout 的佈局 id 與上面的圖中是相同的,但是 ContentFrameLayout 的佈局 id 與上圖中不符,佈局檔案中叫做 action_bar_activity_content,而上面的檢視結構中的是 content —— 原因就在於 [4]、[5] 處 —— [4] 處我們可以看到 subDecor 通過 findViewById(R.id.action_bar_activity_content) 拿到 ContentFrameLayot,將它賦給一個名為 contentView 的區域性變數,而在 [5] 處我們又呼叫了 contentView.setId(android.R.id.content) 將 ContentFrameLayout 的 id 更改成了 content!所以 ActionBarOverlayLayout 及其底層控制元件我們都已經解析完了。原始碼解析到這裡,我們一直是停留在 AppCompatDelegateImplV7 這個類中,目前我們接觸到最高層的 ViewGroup 是 ActionBarOverlayLayout,而它的上層是 FrameLayout,再上層是 LinearLayout,再上層是 PhoneWindow$DecorView,這三層涉及到的是 PhoneWindow 內部的機制,下面我們繼續來剖析 ——

PhoneWindow


再貼一次 createSubDecor() 的原始碼:

private ViewGroup createSubDecor() {
    //...

    // Now let's make sure that the Window has installed its decor by retrieving it
    // [1]
    mWindow.getDecorView();

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


    if (!mWindowNoTitle) {
        if (mIsFloating) {
            // ...
        } else if (mHasActionBar) {
            // Now inflate the view using the themed context and set it as the content view
            // [2]
            subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                    .inflate(R.layout.abc_screen_toolbar, null);

            // [3]
            mDecorContentParent = (DecorContentParent) subDecor
                    .findViewById(R.id.decor_content_parent);

            // ...
        }
    } else {
        // ...
    }

    // [4]
    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);

    // [7]
    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);
        }

        // Change our content FrameLayout to use the android.R.id.content id.
        // Useful for fragments.
        windowContentView.setId(View.NO_ID);
        // [5]
        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);
        }
    }

    // Now set the Window's content view with the decor
    // [6]
    mWindow.setContentView(subDecor);

    // ...

    return subDecor;
}複製程式碼

首先我們先看到 [1] 處的 mWindow.getDecorView() 方法,實際上這個方法就是初始化 DecorView 內部的控制元件,可想而知,如果沒有這個方法的話那麼 [7] 處的程式碼將會報空指標,我們開啟它的原始碼看一下 ——

從原始碼角度剖析 setContentView() 背後的機制
Window#getDecorView()

Window 類是一個抽象類,而在 Android 中它只有一個實現類 —— PhoneWindow,毫無疑問我們應該開啟 PhoneWindow 類檢視它的 getDecorView() 方法 ——

從原始碼角度剖析 setContentView() 背後的機制
PhoneWindow#getDecorView()

mDecor 是什麼?實際上 mDecor 就是 DecorView,初始情況下 mDecor 一定是為空的,那麼就是會進入installDecor() 方法,那麼我們再點進去看一下這個方法 ——

private void installDecor() {
    if (mDecor == null) {
        // [1]
        // new DecorView(getContext(), -1);
        mDecor = generateDecor();
    }
    if (mContentParent == null) {
        // [2]
        mContentParent = generateLayout(mDecor);

        // [3]
        final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                R.id.decor_content_parent);

        if (decorContentParent != null) {
            [4]
            mDecorContentParent = decorContentParent;
            if (mDecorContentParent.getTitle() == null) {
                mDecorContentParent.setWindowTitle(mTitle);
            }
        } else {
            mTitleView = (TextView)findViewById(R.id.title);
            if (mTitleView != null) {
                // ...
            }
        }
    }
}複製程式碼

老規矩,由於原始碼太長,此處就不截圖了,我將原始碼簡化成如上的形式,首先 [1] 處實際上就是呼叫了 new DecorView(getContext(), -1); 例項化了一個 DecorView。然後我們看到第二個 if 判斷語句,同樣地,初始情況下 mContentParent 也是為空的,那麼 mContentParent 又是什麼東西呢 ——

從原始碼角度剖析 setContentView() 背後的機制
mContentParent 宣告

我們看到原始碼註釋上寫到 —— 這是放置 Window 內容的 View,它是 mDecor 本身或 mDecor 的子內容所應該填充的地方。它本身又是一個 ViewGroup 型別,所以我們這裡就可以大膽的猜測,它是前面我們所提到的 ActionBarOverlayLayout 的父控制元件 FrameLayout,而根據方法名 generateLayout() 我們也就知道了,它就是載入 mDecor 佈局的方法 ——

protected ViewGroup generateLayout(DecorView decor) {
    // // Inflate the window decor.

    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        // ...
    } else {
        // Embedded, so no decoration is needed.
        // [1]
        layoutResource = R.layout.screen_simple;
        // System.out.println("Simple!");
    }

    // [2]
    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;

    // [3]
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

    return contentParent;
}複製程式碼

依舊由於原始碼太長,筆者只擷取了部分原始碼,首先原始碼會進入 [1] 處,然後到達 [2] 處載入 screen_simple.xml 佈局檔案,接著就是將它新增進 DecorView,然後再將 screen_simple.xml 中 id 為 content 的控制元件返回,那麼關鍵點就是這個 screen_simple.xml 佈局檔案長什麼樣子了 ——

從原始碼角度剖析 setContentView() 背後的機制
screen_simple.xml

沒錯!這就是 ActionBarOverlayLayout 上層的佈局檔案!而 id 為 content 的佈局檔案也正是 FrameLayout,它就是這個函式的返回值,那麼我們再回到 installDecor() 方法中看到它是賦給 mContentParent 的,這也就正好驗證了我們前面的猜想,mContentParent 就是 FrameLayout!到這裡的話原始碼解析的差不多了,我們來重新整理一下整體的思路 ——

流程一覽


MainActivity#setContentView(int resId) -> AppCompatActivity#setContentView(int resId) ->AppCompatDelegate#setContentView(int resId) -> AppCompatDelegateImplV7#setContentView(int resId)AppCompatDelegateImplV7#setContentView(int resId) 方法中做了主要做了兩件事,首先是生成 DecorView,然後將 resId 引入的佈局檔案新增進 DecorView 佈局中 id 為 content 的那個控制元件(事實上也就是 ContentFrameLayout),那麼最重要的就是如何生成 DecorView 的 ensureSubDecor() 方法了—— ensureSubDecor() - >createSubDecor() ——

private ViewGroup createSubDecor() {
    //...

    // Now let's make sure that the Window has installed its decor by retrieving it
    // [1]
    mWindow.getDecorView();

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


    if (!mWindowNoTitle) {
        if (mIsFloating) {
            // ...
        } else if (mHasActionBar) {
            // Now inflate the view using the themed context and set it as the content view
            // [2]
            subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                    .inflate(R.layout.abc_screen_toolbar, null);

            // [3]
            mDecorContentParent = (DecorContentParent) subDecor
                    .findViewById(R.id.decor_content_parent);

            // ...
        }
    } else {
        // ...
    }

    // [4]
    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);

    // [7]
    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);
        }

        // Change our content FrameLayout to use the android.R.id.content id.
        // Useful for fragments.
        windowContentView.setId(View.NO_ID);
        // [5]
        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);
        }
    }

    // Now set the Window's content view with the decor
    // [6]
    mWindow.setContentView(subDecor);

    // ...

    return subDecor;
}複製程式碼
  • [1]:引入了 screen_simple.xml 佈局檔案,它其中就是最頂層的 LinearLayout、ViewStub、FrameLayout(id 為 content),除了引入佈局檔案還有一件事就是通過 generateDecor() 方法將 FrameLayout 的引用賦給了 mContentParent。
  • [2]:引入了 abc_screen_toolbar.xml 佈局檔案,它是下層的 ActionBarOverlayLayout、ActionBarContainer、ContentFrameLayout(最開始 id 為 action_bar_activity_content,後來被更改成 content) 等
  • [4]、[7]、[5]:將上面 id 為 content 的 FrameLayout 的 id 置空,將 id 為 action_bar_activity_content 的 ContentFrameLayout 的 id 置為 content

分析到這裡我們好像漏了一個地方,應用的檢視結構被我們分成了兩個部分:上層的 LinearLayout、ViewStub、FrameLayout;下層的 ActionBarOverlayLayout、ActionBarContainer、ContentFrameLayout。它們如何連線起來的呢?答案就是 [6] 處的 mWindow.setContentView(),我們跟蹤進去 ——

從原始碼角度剖析 setContentView() 背後的機制
Window#setContentView()

意料之中,Window 是抽象類,而 PhoneWindow 是它的唯一實現類,所以我們開啟 PhoneWindow 的 setContentView() 方法看一下 ——

從原始碼角度剖析 setContentView() 背後的機制
PhoneWindow#setContentView()

關鍵點在第423行程式碼處,我們給 mContentParent 新增了傳入的 view,mContentParent 就是我們在 [1] 處所提及的 FrameLayout,而傳入的 view 就是 abc_screen_toolbar.xml 例項化的那個佈局檔案,所以這樣它們就橋接起來了!

相關文章