03 - [甜湯] - AppCompatActivity.setContentView 流程

01 - [開胃菜] - Activity.setContentView 涉及到的類及相關(guān)概念
02 - [正菜] - Activity.setContentView流程
03 - [甜湯] - AppCompatActivity.setContentView 流程
04 - [完結(jié)] - setContentView 流程總結(jié)

前面兩個(gè)章節(jié)學(xué)習(xí)了 Activity 的 setContentView 流程, 復(fù)習(xí)一下大概流程.

  1. Activity 調(diào)用 setContentView .
  2. 執(zhí)行PhonwWindow.setContentView
  3. PhonwWindow.setContentView 中調(diào)用 installDecor 初始化DecorView ( installDecor中調(diào)用generateDecor() )
  4. 初始化 mContentParent ( installDecor 調(diào)用generateLayout())
    • 在初始化 mContentParent的同時(shí), 又根據(jù)Window 不同屬性加載不同的布局, 然后添加到 DecorView 中.
    • 最后獲取DecorView 中的一個(gè) ViewGroup 作為 mContentParent 并返回.
  5. PhoneWindow.setContentView 中, 調(diào)用 mLayoutInflater.inflate(layoutResID, mContentParent); 把我們要設(shè)置的布局文件ID, 加載到 mContentParent 中.
  6. 流程結(jié)束

可是目前我們所使用的 Activity , 都是直接繼承自 AppCompatActivity, 而 AppCompatActivity 最終也是繼承自 Activity, 那么今天來看一下 AppCompatActivity.setContentView 的流程

( 以下分析基于 Android 10.0 )

首先在 MainActivity 中點(diǎn)擊 setContentView 進(jìn)入內(nèi)部

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

跳轉(zhuǎn)到了 AppCompatActivity 的 setContentView

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

看到這里獲取了一個(gè)代理, 然后調(diào)用代理的 setContentView 方法, 繼續(xù)跟進(jìn), 看一下獲取的是什么代理, 點(diǎn)擊 getDelegate 進(jìn)入.


private AppCompatDelegate mDelegate;

@NonNull
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

看看究竟 create 創(chuàng)建了什么.

public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}

進(jìn)入 create內(nèi). 發(fā)現(xiàn)創(chuàng)建的是 AppCompatDelegateImpl ,
原來 AppCompatDelegate 是一個(gè)抽象類, 而 AppCompatDelegateImpl 繼承自 AppCompatDelegate .

( 注: AppCompatDelegate 是一個(gè)可以放在任意Activity中,并且回調(diào)相應(yīng)生命周期的類,在不使用 AppCompatActivity 的情況下, 也可以得到一致的主題與顏色 )

那么現(xiàn)在就清楚了. AppCompatActivity.setContentView 真正調(diào)用了 AppCompatDelegateImpl 中的 setContentView 方法.

進(jìn)入 AppCompatDelegateImpl.setContentView

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

先來看第一個(gè)方法 ensureSubDecor, 進(jìn)入

// true if we have installed a window sub-decor layout.
private boolean mSubDecorInstalled;
private ViewGroup mSubDecor;

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        ...
    }
}

上面省去了一些暫時(shí)不需要關(guān)注的代碼, 重點(diǎn)就是這個(gè) mSubDecor 的初始化.
DecorView 知道, 因?yàn)樯弦徽抡f到了, 可是這個(gè) mSubDecor 是什么鬼. 就是一個(gè)簡(jiǎn)單的 ViewGroup ?

進(jìn)入 AppCompatDelegateImpl.createSubDecor()

private ViewGroup createSubDecor() {
//---------------------------------第一部分----------------------------------
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
    ...
    ...
    a.recycle();

//---------------------------------第二部分----------------------------------
    // Now let's make sure that the Window has installed its decor by retrieving it
    mWindow.getDecorView();

//---------------------------------第三部分----------------------------------
    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;

    if (!mWindowNoTitle) {
        if (mIsFloating) {
            // If we're floating, inflate the dialog title decor
            subDecor = (ViewGroup) inflater.inflate( R.layout.abc_dialog_title_material, null);
            ...
        } else if (mHasActionBar) {
            ...
            ...
            // Now inflate the view using the themed context and set it as the content view
            subDecor = (ViewGroup) LayoutInflater.from(themedContext).inflate(R.layout.abc_screen_toolbar, null);
            ...
            ...
            ...
        }
    } else {
        if (mOverlayActionMode) {
            subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple_overlay_action_mode, null);
        } else {
            subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
        }

        if (Build.VERSION.SDK_INT >= 21) {
            ...
        } else {
            ...
        }
    }
    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
                            + " }");
      }
   ...
   ...
   ...
//---------------------------------第四部分----------------------------------
    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) {
        while (windowContentView.getChildCount() > 0) {
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        }
        windowContentView.setId(View.NO_ID);
        contentView.setId(android.R.id.content);
        ...
        ...
    }

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

    ...
    ...

    return subDecor;
}

代碼量有點(diǎn)多, 我已省去了, 目前我們不需要關(guān)注的代碼. 只留下和流程相關(guān)的關(guān)鍵性代碼.
如上代碼所示, 我將 createSubDecor 方法整體分為了 4個(gè)部分.
?
?
createSubDecor() 第一部分
又見到了 TypedArray, 上一章也有這個(gè), 還記得嗎? 不記得的回去翻一下加深下印象.
上章的 TypedArray 是取的是 Window的, 然后賦值給 PhoneWindow 的.
這里的 TypedArray 取的是 AppCompatTheme 的, 然后賦值給 AppCompatDelegateImpl 的.
兩者之間不要弄混淆了.

所以說, 第一部分的主要是從 AppCompatTheme 獲取 style, 然后逐個(gè)進(jìn)行判斷, 然后賦值給 AppCompatDelegateImpl .

?
?

createSubDecor() 第二部分
mWindow.getDecorView();
看到這句, 熟悉嗎? 我猜測(cè)這里是初始化了DecorView 和 mContentParnt,
為了驗(yàn)證. 我們直接進(jìn)入 PhoneWindow 的 getDecorView() 方法.
PhoneWindow.getDecorView()

    @Override
    public final @NonNull View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }

果不其然, 確實(shí)是去初始化了 DecorView 和 mContentParent. ( installDecor() 調(diào)用. installDecor方法具體流程, 這里不再說明, 上一章 [02-正菜] 中已經(jīng)詳細(xì)解釋 )

第二部分就是初始化 PhoneWindow 中的 DecorView 和 mContentParent

?
?
createSubDecor() 第三部分
看到第三部分是不是還是有點(diǎn)熟悉?
是不是和在 PhoneWindow 初始化 mContentParent 并且加載一個(gè) LinearLayout 的邏輯一樣? 只是這里是給 subDecor 賦值.
那既然這樣, 我們還是先找到一個(gè)最簡(jiǎn)單的布局文件看一下.
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
在我電腦上的路徑如下:
D:\SDK\extras\android\support\v7\appcompat\res\layout\R.layout.abc_screen_simple (家里的是 windows 系統(tǒng))

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

    <include layout="@layout/abc_screen_content_include" />

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

繼續(xù)尋找 include 的, abc_screen_content_include, 還是在上面的那個(gè)路徑下.

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

思考一下, PhoneWindow 中給 DecorView 加載了一個(gè) LinearLayout, 里面有兩部分, 其中一部分是一個(gè) FrameLayout.
而這里呢, 給 subDecor 賦值了一個(gè) FitWindowsLinearLayout 布局 , 里面也有兩部分, 其中一部分是一個(gè) ContentFrameLayout .
這兩者看起來是不是很相似? 有沒有聯(lián)想到什么? 別著急繼續(xù)往下看.

第三部分根據(jù)條件給 subDecor 賦值. (加載不同的布局文件)

?
?
createSubDecor() 第四部分. 重點(diǎn)
為了方便, 我把第四部分的代碼拿下來放到這里.

//---------------------------------第四部分----------------------------------
    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) {
        while (windowContentView.getChildCount() > 0) {
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        }
        windowContentView.setId(View.NO_ID);
        contentView.setId(android.R.id.content);
        ...
        ...
    }

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

    ...
    ...

    return subDecor;
  1. 先是從 subDecor 中取出一個(gè) ID 為 action_bar_activity_content 的控件賦值給 contentView 變量.
    這個(gè) ID 就是上面布局文件中的 ContentFrameLayout

  2. 接著從 PhoneWindow 中的 DecorView 中獲取一個(gè) ID 為 content 的 ViewGroup. 賦值給 windowContentView .
    回想一下, 這個(gè)ID 不就是 DecorView 中 LinearLayout 里面的 FrameLayout 嘛.

  3. 循環(huán)遍歷 windowContentView, 取出 windowContentView 中每一個(gè)子 View, 然后 windowContentView 先刪除這個(gè)View, 再添加到 ContentFrameLayout 中. 直至windowContentView 為空.

  4. 把 DecorView 中 LinearLayout 里面的 FrameLayout ID 改為 NO_ID.

  5. 把 subDecor 中 ContentFrameLayout ID 改為 R.id.content.

  6. 調(diào)用 PhoneWindow 的 setContentView 把 subDecor 加載到 PhoneWindow 中的 mContentParent 中.

  7. 返回 subDecor.

第四部分總的來說就是, 將 PhoneWindow --> DecorView 布局中 id 為 content 的 FrameLayout 所有子 View 遍歷添加到 subDecor 中 id 為 action_bar_activity_content 的 ContentFrameLayout 中去. 添加一個(gè)刪除一個(gè). 遍歷完成后, 改變兩個(gè) ViewGroup 的 id, 最后將 subDecor 添加到 PhoneWindow 的 mContentParent 中.

?
?
現(xiàn)在代碼繼續(xù)回到 AppCompatDelegateImpl.setContentView

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

ensureSubDecor(); 已經(jīng)執(zhí)行完成, 初始化了 mSubDecor .
接著從 mSubDecor 中獲取 id 為 content 的控件, 并轉(zhuǎn)為 ViewGroup.
然后把我們傳入的資源文件 添加到 這個(gè) ViewGroup 中去.
至此, 這整個(gè)流程分析完成.

在下一章, 會(huì)有一個(gè)總結(jié), 從網(wǎng)上找了幾張圖來總結(jié)和對(duì)比.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容