【Android源碼】Activity和AppCompatActivity的setContentView方法區(qū)別

前言:

記錄一下自己看源碼的過程(別人的理解+自己的理解)

問題:一個(gè)TextView,兩種結(jié)果

我們先看一種現(xiàn)象,我們?cè)诓季治募蟹胖靡粋€(gè)TextView,然后在我們的MainActivity中去打印

override fun onCreate(savedInstanceState: Bundle?) {
    Log.d("MainActivity", tv_letter.toString())
}
//結(jié)果:
//MainActivity繼承自Activity
android.widget.TextView{4b9ea62 G.ED..... ......ID 0,0-0,0 #7f0700f6 app:id/tv_letter}

//MainActivity繼承自AppCompatActivity
androidx.appcompat.widget.AppCompatTextView{4b021e0 G.ED..... ......ID 0,0-0,0 #7f0700f6 app:id/tv_letter}

思考:布局里面明明是TextView,為什么繼承自AppCompatActivity就變成了AppCompatTextView

為了搞清楚這個(gè)現(xiàn)象,我們先看一下各自的源碼

首先我們看一下繼承自Activity的源碼,我們先從MainActivity中的setContent()方法追進(jìn)去,

1. 繼承自Activity

Activity.java

public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

public void setContentView(View view) {
    getWindow().setContentView(view);
    initWindowDecorActionBar();
}

public void setContentView(View view, ViewGroup.LayoutParams params) {
    getWindow().setContentView(view, params);
    initWindowDecorActionBar();
}

這里Activity中提供了三個(gè)重載方法,但是都調(diào)用了getWindow().setContentView(view, params);方法。這里補(bǔ)充一下Window的知識(shí)點(diǎn)

Window的知識(shí)點(diǎn)

image-20201011140336753.png
  1. Window是一個(gè)抽象類,提供了繪制窗口的一組通用API。
  2. PhoneWindow是Window的具體繼承實(shí)現(xiàn)類。而且該類內(nèi)部包含了一個(gè)DecorView對(duì)象,該DectorView對(duì)象是所有應(yīng)用窗口(Activity界面)的根View。
  3. DecorView是PhoneWindow的內(nèi)部類,是FrameLayout的子類,是對(duì)FrameLayout進(jìn)行功能的修飾(所以叫DecorXXX),是所有應(yīng)用窗口的根View 。

依據(jù)面向?qū)ο髲某橄蟮骄唧w我們可以類比上面關(guān)系就像如下:

Window是一塊電子屏,PhoneWindow是一塊手機(jī)電子屏,DecorView就是電子屏要顯示的內(nèi)容,Activity就是手機(jī)電子屏安裝位置。

由于Window是一個(gè)抽象類,我們只能從他的實(shí)現(xiàn)類中去找setContentView的源碼

1.1 PhoneWindow.java的setContentView

@Override
public void setContentView(int layoutResID) {
    //...
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    //...
    //因?yàn)閘ayoutResID是我們傳過來的activity_main,所以這里是把返回回來的系統(tǒng)布局mContentParent和activity_main綁定起來了
    mLayoutInflater.inflate(layoutResID, mContentParent);
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

在setContentView中,先判斷mContentParent是否為null,如果是第一次調(diào)用這個(gè)方法,就會(huì)進(jìn)installDecor()方法里面,以后再進(jìn)入的時(shí)候會(huì)根據(jù)是否設(shè)置FEATURE_CONTENT_TRANSITIONS Window屬性(默認(rèn)false)來決定是否移除mContentParent的所有子View。

下面的mLayoutInflater.inflate()將我們的傳來來的布局id轉(zhuǎn)換成View樹,并添加至mContentParent中。(這里的mLayoutInflater是在PhoneWindow的構(gòu)造函數(shù)中實(shí)例化的mLayoutInflater = LayoutInflater.from(context);

上面是Activity三個(gè)重載方法的一種,其余兩種也是類似,但是沒有傳layoutResID過去,所以他是將我們的傳過去的View,直接追加到了mContentView上面

@Override
public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    mContentParent.addView(view, params);
}

除了記載View外,mContentParent.removeAllViews();也值得我們注意,因?yàn)樗峁┝顺绦蛘{(diào)用多次setContentView的保證:重復(fù)調(diào)用時(shí),先移除所有子View

1.2 PhoneWondow中的installDecor方法

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
    }
}

在installDecor方法中,他先判斷了DecorView(該類是FrameLayout子類,即一個(gè)ViewGroup視圖)是否存在,如果不存在,就去創(chuàng)建+初始化他,而generateDecor方法也很簡(jiǎn)單,直接就是new一個(gè)DecorView對(duì)象返回。

protected DecorView generateDecor(int featureId) {
    return new DecorView(context, featureId, this, getAttributes());
}

從setContentView那里我們知道,installDecor方法就是在mContentParent==null的時(shí)候調(diào)用的,所以這個(gè)方法的主要作用還是創(chuàng)建mContentParent對(duì)象,我們進(jìn)到generateLayout()方法里面

1.3 generateLayout方法——?jiǎng)?chuàng)建mContentParent

protected ViewGroup generateLayout(DecorView decor) {
    //獲取我們?cè)O(shè)置的android:theme屬性
    TypedArray a = getWindowStyle();
    //做一些簡(jiǎn)單的判斷,看看是否是styleable中的一種,然后去請(qǐng)求
    //依據(jù)主題style設(shè)置一堆值進(jìn)行設(shè)置
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    } else ...
    //重點(diǎn):layoutResource
    // Inflate the window decor.填充window的decor
    int layoutResource;
    //獲取我們平時(shí)通過requestWindowFeature()設(shè)置的屬性
    int features = getLocalFeatures();
    //做各種判斷,給layoutResource賦值(用系統(tǒng)的布局給它賦值)
    //根據(jù)設(shè)定好的features值選擇不同的窗口修飾布局文件,得到layoutResource值
    if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
    }else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        //...
    } else {
        layoutResource = R.layout.screen_simple;
    }
    //解析實(shí)例化系統(tǒng)的布局
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    //找一個(gè)叫android.R.id.content的一個(gè)FrameLayout
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }
    return contentParent;
}

/**
 * The ID that the main layout in the XML layout file should have.
 */
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

前面都是根據(jù)窗口的風(fēng)格修飾類型為該窗口選擇不同的窗口根布局文件,后面mDecor調(diào)用了onResourcesLoaded()方法將該窗口根布局添加到mDecor這個(gè)根視圖中去,最后獲取一個(gè)叫android.R.id.content的一個(gè)FrameLayout返回去作為mContentParent

1.4 DecorView.java中的onResourcesLoaded方法——往mDecor中添加根布局

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    mDecorCaptionView = createDecorCaptionView(inflater);
    //實(shí)例化layoutResource(被賦予系統(tǒng)布局之后)
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        if (mDecorCaptionView.getParent() == null) {
            //將layoutResource實(shí)例化的對(duì)象添加到DecorView中,去填充DecorView
            addView(mDecorCaptionView,
                    new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
    } else {
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    initializeElevation();
}

所以,setContentView中調(diào)用的installDecor()方法就是用來產(chǎn)生mDecor和mContentParent對(duì)象。在installDecor方法之后,我們才用上了外面?zhèn)鬟M(jìn)來的layoutResID

mLayoutInflater.inflate(layoutResID, mContentParent);

補(bǔ)充

在我們平時(shí)設(shè)置Activity的theme或feature時(shí),如:

//通過java文件設(shè)置:
requestWindowFeature(Window.FEATURE_NO_TITLE);
//通過xml文件設(shè)置:
android:theme="@android:style/Theme.NoTitleBar"

其實(shí)我們平時(shí)requestWindowFeature()設(shè)置的值就是,在創(chuàng)建mContentView的generateLayout方法里面,通過getLocalFeature()獲取的,而android:theme屬性也是通過該方法里面的getWindowStyle()獲取的。所以這下就說清楚了在java文件設(shè)置Activity的屬性時(shí)必須在setContentView方法之前調(diào)用requestFeature()方法的原因了。

1.5 PhoneWindow.java的內(nèi)部接口Callback的onContentChanged方法

分析了PhoneWindow的setContentView方法,我們看他這個(gè)方法的后面還會(huì)調(diào)用一個(gè)Callback接口的成員函數(shù)onContentChanged來通知對(duì)應(yīng)的Activity組件視圖內(nèi)容發(fā)生了變化。

public void setContentView(int layoutResID) {
    //......
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

PhoneWindow并沒有重寫這個(gè)方法,在Window這個(gè)抽象類中

/**
 * Return the current Callback interface for this window.
 */
public final Callback getCallback() {
    return mCallback;
}

mCallback的賦值地方,我們可以找到

public void setCallback(Callback callback) {
    mCallback = callback;
}

那么這個(gè)方法是在哪里調(diào)用的呢?在我們上面Windows知識(shí)點(diǎn)的地方,我們知道,Window是Activity的組合成員,那么Activity中對(duì)Windows的引用中,肯定調(diào)用了這個(gè)方法,所以回到Activity中

@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) {
    //...
    mWindow.setCallback(this);
    //...
}

所以Activity也就實(shí)現(xiàn)了Callback這個(gè)接口,同時(shí)也需要實(shí)現(xiàn)其內(nèi)部的onContentChanged()方法

public void onContentChanged() {
}

onContentChanged是個(gè)空方法。那就說明當(dāng)Activity的布局改動(dòng)時(shí),即setContentView()或者addContentView()方法執(zhí)行完畢時(shí)就會(huì)調(diào)用該方法(因?yàn)閟etContentView和addContentView的最后就調(diào)用了cb.onContentChanged();,等待接口回調(diào))。

所以當(dāng)我們寫App時(shí),Activity的各種View的findViewById()方法等都可以放到該方法中,系統(tǒng)會(huì)幫忙回調(diào)。

總結(jié):

可以看出來setContentView整個(gè)過程主要是如何把Activity的布局文件或者java的View添加至窗口里,上面的過程可以重點(diǎn)概括為:

  1. 創(chuàng)建一個(gè)DecorView的對(duì)象mDecor,該mDecor對(duì)象將作為整個(gè)應(yīng)用窗口的根視圖。
  2. 依據(jù)Feature等style theme創(chuàng)建不同的窗口修飾布局文件,并且通過findViewById獲取Activity布局文件該存放的地方(窗口修飾布局文件中id為content的FrameLayout)。
  3. 將Activity的布局文件添加至id為content的FrameLayout內(nèi)。

AppCompatActivity的setContentView()方法首先我們看的是繼承自AppCompatActivity的源碼

我們從MainActivity點(diǎn)進(jìn)setContentView()

2. 繼承自AppCompatActivity

我們還是從setContentView()方法出發(fā)

2.1 AppCompatActivity.java的setContentView方法

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

這里再對(duì)setContentView追進(jìn)去,發(fā)現(xiàn)只能看到 里面的public abstract void setContentView(@LayoutRes int resId);這種抽象方法了

所以我們只能從前面的getDelegate()入手

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

這里顯示mDelegate是通過AppCompatDelegate中的create()方法創(chuàng)建的,我們進(jìn)去看一下

@NonNull
public static AppCompatDelegate create(@NonNull Activity activity,
        @Nullable AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, callback);
}

這里也僅僅是new了一個(gè)AppCompatDelegateImpl對(duì)象,所以我們的setContentView最終是調(diào)用的AppCompatActivity中的setContentView方法,我們進(jìn)去看

2.2AppCompatDelegateImpl.java中的setContentView方法

@Override
public void setContentView(View v) {
    //創(chuàng)建mDecor
    ensureSubDecor();
    //去拿android.R.id.content的Fragment
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    contentParent.addView(v);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}

這里其實(shí)就沒啥好看的了,一個(gè)一個(gè)點(diǎn)進(jìn)去,仔細(xì)看看就好了。與Activity沒啥區(qū)別

我們回到AppCompatDelegateImpl上面來,

class AppCompatDelegateImpl extends AppCompatDelegate
        implements MenuBuilder.Callback, LayoutInflater.Factory2 {
}

發(fā)現(xiàn)他繼承了LayoutInflater里面的Factory接口,我們可以查找一下他內(nèi)部使用LayoutInflater的地方

2.3 AppCompatDelegateImpl.java中的installViewFactory方法

@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    //如果他的factory為空就給他設(shè)置一個(gè)factory
    if (layoutInflater.getFactory() == null) {
        //這里的this就是把自己傳過去
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

每次創(chuàng)建View的時(shí)候,會(huì)調(diào)用onCreateView,所以我們過去看看

AppCompatDelegateImpl.java

@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return createView(parent, name, context, attrs);
}

然后繼續(xù)跳轉(zhuǎn)到createView()方法中

2.5AppCompatDelegateImpl.java中的createView方法

@Override
public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
    //...
    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
            true, /* Read read app:theme as a fallback at all times for legacy reasons */
            VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
    );
}

然后我們繼續(xù)進(jìn)到AppCompatViewInflater中的createView方法:

2.6AppCompatViewInflater.java中的createView方法

final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    final Context originalContext = context;
    //...
    View view = null;

    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
        case "TextView":
            view = createTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageView":
            view = createImageView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "Button":
            view = createButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "EditText":
            view = createEditText(context, attrs);
            verifyNotNull(view, name);
            break;
        case "Spinner":
            view = createSpinner(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageButton":
            view = createImageButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckBox":
            view = createCheckBox(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RadioButton":
            view = createRadioButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckedTextView":
            view = createCheckedTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "AutoCompleteTextView":
            view = createAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "MultiAutoCompleteTextView":
            view = createMultiAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RatingBar":
            view = createRatingBar(context, attrs);
            verifyNotNull(view, name);
            break;
        case "SeekBar":
            view = createSeekBar(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ToggleButton":
            view = createToggleButton(context, attrs);
            verifyNotNull(view, name);
            break;
        default:
            // The fallback that allows extending class to take over view inflation
            // for other tags. Note that we don't check that the result is not-null.
            // That allows the custom inflater path to fall back on the default one
            // later in this method.
            view = createView(context, name, attrs);
    }
}

@NonNull
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
    return new AppCompatTextView(context, attrs);
}

@NonNull
protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
    return new AppCompatImageView(context, attrs);
}

//....

總結(jié):

只要我們外部繼承了AppCompatActivity,那么我們創(chuàng)建任何的View都會(huì)被這里攔截,然后給你返回一個(gè)AppCompatXXX,所以才會(huì)出現(xiàn)我們最開始的那個(gè)問題,繼承自不同的父類,導(dǎo)致輸出的結(jié)果不一樣的問題

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容