夜夜使用的setContentView()裏面到底做了什麼?

一、寫在前面

我們都知道繼承Activity的onCreate()方法然後setContentView(R.layout.xxx)可以設置我們自己的佈局文件,但是佈局文件生成的View到底添加到哪裏去了呢?首先在Activity這個類中我們可以看到有個mWindow變量,這個變量是在attach()方法裏面創建的:mWindow = new PhoneWindow(...),然後在我們setContentView()的時候會創建一個mDecor變量,這就是我們Activity的第一個View,然後再往裏面添加各種View,包括我們自己佈局的View。此文就是分析下這些View都是怎麼添加的。

以前的以前我們自己的Activity都是直接繼承Activity這個類的,從API21開始,我們一般都繼承AppCompatActivity了,下面分別分析。

注:以下源碼版本爲API28。

二、繼承Activity的時候

// 這是我們普通的Activity,繼承onCreate,添加我們的佈局
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.xxx);
}

// 類:Activity
public void setContentView(@LayoutRes int layoutResID) {
	// 跟蹤getWindow()可以看到mWindow = new PhoneWindow(...),返回的是一個PhoneWindow
	// 所以這裏跳到PhoneWindow的setContentView(...)
	// IDEA裏面可以直接按ctrl+alt+單擊彈出實現此方法的類,選擇PhoneWindow的進入就行
    getWindow().setContentView(layoutResID);
    // ...
}

// 類:PhoneWindow
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    }
    // ...
    // 把我們自己的佈局文件inflate後添加到mContentParent中
    // mContentParent就是我們自己的佈局的父View
    mLayoutInflater.inflate(layoutResID, mContentParent);
    // ...
}

// 類:PhoneWindow
private void installDecor() {
    // ...
    if (mDecor == null) {
    	// 1、生成DecorView
    	// mDecor是PhoneWindow的一個變量,同時PhoneWindow也是mDecor的一個變量
        mDecor = generateDecor(-1);
        // ...
    }
    // ...
    if (mContentParent == null) {
    	// 2、生成mContentParent
    	// 從前面我們知道這就是我們自己的佈局的父類
        mContentParent = generateLayout(mDecor);
        // ...
    }    
}
1、生成DecorView
// 類:PhoneWindow
protected DecorView generateDecor(int featureId) {
    // ...
    // 直接創建了一個DecorView,點擊去查看DecorView是繼承了FrameLayout
    return new DecorView(context, featureId, this, getAttributes());
}
2、生成mContentParent
// 類:PhoneWindow
protected ViewGroup generateLayout(DecorView decor) {
	// 獲取窗口屬性,也就是AndroidManifest.xml中設置的主題
	// View的屬性在layout.xml裏面配置,Window的屬性在主題裏面配置
    TypedArray a = getWindowStyle();
	// 這裏是一系列的主題設置
	// ...

    // 根據上面設置的屬性,加載不同的根佈局,所有佈局都一定會有一個View的id是ID_ANDROID_CONTENT,
    // 也就是mContentParent,也就是我們setContentView()傳入的View的父View
    // 這裏隨便選一個佈局,比如:R.layout.screen_simple
    // 該文件在源碼中的路徑:frameworks/base/core/res/res/layout/screen_simple.xml
    // 下載源碼方法:https://blog.csdn.net/qiantujava/article/details/102847414
    // 在線源碼地址:http://aospxref.com/android-9.0.0_r45/xref/frameworks/base/core/res/res/layout/screen_simple.xml
    layoutResource = R.layout.screen_simple;

    // 1、解釋上面選定的layout修飾佈局文件
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

	// 這裏findViewById其實調用的是getDecorView().findViewById(id)
	// 上面所有佈局都會有ID_ANDROID_CONTENT這個id,也就是:android:id="@android:id/content"
	// 我們自己的佈局文件就是添加到這個ViewGroup中的
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
	// ...
    return contentParent;
}

1、解釋修飾佈局文件

// 類:DecorView
// 解釋layout文件
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    // ...
	// Caption有標題的意思
    mDecorCaptionView = createDecorCaptionView(inflater);
    // 解釋剛纔那個佈局screen_simple.xml,裏面會有ID_ANDROID_CONTENT
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        // 沒搞清楚mDecorCaptionView是個什麼東西,先不管這個
    } else {
	    // 沒有mDecorCaptionView的時候,直接把root添加到DecorView中
	    addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    // ...
}

上面有幾個重點關注的變量:
mWindow是PhoneWindow的對象,是Activity的一個變量,在Activity.attach()方法裏面就創建了;
mDecor是DecorView的對象,是PhoneWindow的一個變量;
mContentRoot是修飾佈局文件inflate得到的View,添加到了mDecor中;
mContentParent是mContentRoot中id爲ID_ANDROID_CONTENT的View;
最後我們setContentView傳入的View是添加到mContentParent的。
所以我們可以得到個大概的佈局關係圖:

三、繼承AppCompatActivity

// 這是我們普通的Activity,繼承onCreate,添加我們的佈局
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.xxx);
}

// 類:AppCompatActivity
public void setContentView(@LayoutRes int layoutResID) {
	// 點進去getDelegate()可以看到返回的是AppCompatDelegateImpl
	// 所以這裏調用的是AppCompatDelegateImpl.setContentView()
    getDelegate().setContentView(layoutResID);
}

// 類:AppCompatDelegateImpl
public void setContentView(int resId) {
	// 確保mSubDecor是否已經生成
	// 這裏還不知道mSubDecor是個什麼東西,下面再看
    ensureSubDecor();
    // 把我們自己的佈局添加到contentParent
    // 其實從ensureSubDecor()裏面可以知道這個android.R.id.content對應的View已經不是前面分析的mContentParent了
    // 而是mSubDecor裏面的一個ContentFrameLayout
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    // ...
}

// 類:AppCompatDelegateImpl
private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
		// ...
    }
}

// 類:AppCompatDelegateImpl
private ViewGroup createSubDecor() {
    // 設置各種主題
    // ...
	// 跟蹤這個mWindow,可以看到是從Activity傳過來的,所以這個也是PhoneWindow
	// getDecorView()裏面就一個判斷,如果mDecor==null,就installDecor()
	// installDecor()在上面已經分析了,一毛一樣的
	// installDecor()後就生成了mDecor和mContentParent了
    mWindow.getDecorView();

	// 根據不同的主題inflate不同的佈局文件,然後賦值給subDecor
	// 所以,subDecor就是一個普通的ViewGroup
    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;
    // ...
    subDecor = (ViewGroup) inflater.inflate(R.layout.abc_dialog_title_material, null);
	// ...
	// 上面所有的佈局都會有個id=R.id.action_bar_activity_content的ContentFrameLayout
    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);

	// 上面mWindow.getDecorView()已經生成mContentParent了,id就是android.R.id.content
	// 所以這裏windowContentView其實就是mContentParent
    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    if (windowContentView != null) {
        // 把windowContentView的所有子view都添加到contentView中
        while (windowContentView.getChildCount() > 0) {
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        }

        // 把windowContentView的id設爲了NO_ID,把contentView的id設成了android.R.id.content
        // 此後各個view的關係還是:DecorView->mContentRoot->mContentParent(就是windowContentView)
        // 但是mContentParent裏面的view已經全部移到contentView中了,同時contentView還沒添加到DecorView
        windowContentView.setId(View.NO_ID);
        contentView.setId(android.R.id.content);
		// ...
    }

    // 把contentView添加到DecorView中
    mWindow.setContentView(subDecor);
	// ...
    return subDecor;
}

// 類:PhoneWindow
public void setContentView(View view, ViewGroup.LayoutParams params) {
    // 前面createSubDecor()中調了mWindow.getDecorView()已經生成了mContentParent,所以直接跳過
    if (mContentParent == null) {
        installDecor();
    }
    // ...
    // 這個view是前面傳過來的subDecor
    mContentParent.addView(view, params);
	// ...
}

由上面的分析,我們同樣可以得到以下總結:
mWindow是PhoneWindow的對象,是Activity的一個變量,在Activity.attach()方法裏面就創建了;
mDecor是DecorView的對象,是PhoneWindow的一個變量;
mContentRoot是修飾佈局文件inflate得到的View,添加到了mDecor中;
mContentParent是mContentRoot中id爲ID_ANDROID_CONTENT的View,但是被設置爲NO_ID了;
mSubDecor是含有R.id.action_bar_activity_content的佈局,mSubDecor會被添加到mContentParent中;
contentView是ContentFrameLayout,是mSubDecor中id=R.id.action_bar_activity_content的view,最後會被設成id=android.R.id.content。
最後我們setContentView()傳入的view是會被添加到id=android.R.id.content的view中的,
所以我們可以得到個大概的佈局關係圖:

四、繼承Activity和繼承AppCompatActivity的區別

從上面的分析可以看出來,繼承AppCompatActivity的時候,佈局中多了個mSubDecorcontentView,之前我們的佈局是添加到mContentParent的,現在是添加到contentView了,其實換湯不換藥,只是在中間插入了一層view。爲什麼要這樣呢?因爲在API28的時候官方大力推薦MaterialDesign,在中間插入了一層mSubDecor就可以爲所欲爲的加入MaterialDesign的東西了,應該是醬紫吧。

五、寫在最後

其實看源碼也沒有那麼難嘛,一開始看可能會覺得超複雜,跳來跳去的,谷歌什麼破工程師,寫出來的代碼亂七八糟。但是當你看的多了,這裏的多包括看的次數多和看的內容多,看的次數多了你會對這段源碼印象更深刻,下次再回來看的時候不會再那麼陌生了,而看的內容多的話,你會發現越來越清晰了,各個點都是有關係的,之前那個點可能沒看懂,看到這個點的時候忽然明白了上個點,都是有聯繫的。
PS. 標題裏面的夜夜並沒有說假話,現在是凌晨4點、

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章