【Android源码】Activity和AppCompatActivity的setContentView方法区别

前言:

记录一下自己看源码的过程(别人的理解+自己的理解)

问题:一个TextView,两种结果

我们先看一种现象,我们在布局文件中放置一个TextView,然后在我们的MainActivity中去打印

override fun onCreate(savedInstanceState: Bundle?) {
    Log.d("MainActivity", tv_letter.toString())
}
//结果:
//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

为了搞清楚这个现象,我们先看一下各自的源码

首先我们看一下继承自Activity的源码,我们先从MainActivity中的setContent()方法追进去,

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中提供了三个重载方法,但是都调用了getWindow().setContentView(view, params);方法。这里补充一下Window的知识点

Window的知识点

image-20201011140336753.png
  1. Window是一个抽象类,提供了绘制窗口的一组通用API。
  2. PhoneWindow是Window的具体继承实现类。而且该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View。
  3. DecorView是PhoneWindow的内部类,是FrameLayout的子类,是对FrameLayout进行功能的修饰(所以叫DecorXXX),是所有应用窗口的根View 。

依据面向对象从抽象到具体我们可以类比上面关系就像如下:

Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装位置。

由于Window是一个抽象类,我们只能从他的实现类中去找setContentView的源码

1.1 PhoneWindow.java的setContentView

@Override
public void setContentView(int layoutResID) {
    //...
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    //...
    //因为layoutResID是我们传过来的activity_main,所以这里是把返回回来的系统布局mContentParent和activity_main绑定起来了
    mLayoutInflater.inflate(layoutResID, mContentParent);
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

在setContentView中,先判断mContentParent是否为null,如果是第一次调用这个方法,就会进installDecor()方法里面,以后再进入的时候会根据是否设置FEATURE_CONTENT_TRANSITIONS Window属性(默认false)来决定是否移除mContentParent的所有子View。

下面的mLayoutInflater.inflate()将我们的传来来的布局id转换成View树,并添加至mContentParent中。(这里的mLayoutInflater是在PhoneWindow的构造函数中实例化的mLayoutInflater = LayoutInflater.from(context);

上面是Activity三个重载方法的一种,其余两种也是类似,但是没有传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();也值得我们注意,因为他提供了程序调用多次setContentView的保证:重复调用时,先移除所有子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子类,即一个ViewGroup视图)是否存在,如果不存在,就去创建+初始化他,而generateDecor方法也很简单,直接就是new一个DecorView对象返回。

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

从setContentView那里我们知道,installDecor方法就是在mContentParent==null的时候调用的,所以这个方法的主要作用还是创建mContentParent对象,我们进到generateLayout()方法里面

1.3 generateLayout方法——创建mContentParent

protected ViewGroup generateLayout(DecorView decor) {
    //获取我们设置的android:theme属性
    TypedArray a = getWindowStyle();
    //做一些简单的判断,看看是否是styleable中的一种,然后去请求
    //依据主题style设置一堆值进行设置
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    } else ...
    //重点:layoutResource
    // Inflate the window decor.填充window的decor
    int layoutResource;
    //获取我们平时通过requestWindowFeature()设置的属性
    int features = getLocalFeatures();
    //做各种判断,给layoutResource赋值(用系统的布局给它赋值)
    //根据设定好的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;
    }
    //解析实例化系统的布局
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    //找一个叫android.R.id.content的一个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;

前面都是根据窗口的风格修饰类型为该窗口选择不同的窗口根布局文件,后面mDecor调用了onResourcesLoaded()方法将该窗口根布局添加到mDecor这个根视图中去,最后获取一个叫android.R.id.content的一个FrameLayout返回去作为mContentParent

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

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    mDecorCaptionView = createDecorCaptionView(inflater);
    //实例化layoutResource(被赋予系统布局之后)
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        if (mDecorCaptionView.getParent() == null) {
            //将layoutResource实例化的对象添加到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中调用的installDecor()方法就是用来产生mDecor和mContentParent对象。在installDecor方法之后,我们才用上了外面传进来的layoutResID

mLayoutInflater.inflate(layoutResID, mContentParent);

补充

在我们平时设置Activity的theme或feature时,如:

//通过java文件设置:
requestWindowFeature(Window.FEATURE_NO_TITLE);
//通过xml文件设置:
android:theme="@android:style/Theme.NoTitleBar"

其实我们平时requestWindowFeature()设置的值就是,在创建mContentView的generateLayout方法里面,通过getLocalFeature()获取的,而android:theme属性也是通过该方法里面的getWindowStyle()获取的。所以这下就说清楚了在java文件设置Activity的属性时必须在setContentView方法之前调用requestFeature()方法的原因了。

1.5 PhoneWindow.java的内部接口Callback的onContentChanged方法

分析了PhoneWindow的setContentView方法,我们看他这个方法的后面还会调用一个Callback接口的成员函数onContentChanged来通知对应的Activity组件视图内容发生了变化。

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

PhoneWindow并没有重写这个方法,在Window这个抽象类中

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

mCallback的赋值地方,我们可以找到

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

那么这个方法是在哪里调用的呢?在我们上面Windows知识点的地方,我们知道,Window是Activity的组合成员,那么Activity中对Windows的引用中,肯定调用了这个方法,所以回到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也就实现了Callback这个接口,同时也需要实现其内部的onContentChanged()方法

public void onContentChanged() {
}

onContentChanged是个空方法。那就说明当Activity的布局改动时,即setContentView()或者addContentView()方法执行完毕时就会调用该方法(因为setContentView和addContentView的最后就调用了cb.onContentChanged();,等待接口回调)。

所以当我们写App时,Activity的各种View的findViewById()方法等都可以放到该方法中,系统会帮忙回调。

总结:

可以看出来setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,上面的过程可以重点概括为:

  1. 创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。
  2. 依据Feature等style theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)。
  3. 将Activity的布局文件添加至id为content的FrameLayout内。

AppCompatActivity的setContentView()方法首先我们看的是继承自AppCompatActivity的源码

我们从MainActivity点进setContentView()

2. 继承自AppCompatActivity

我们还是从setContentView()方法出发

2.1 AppCompatActivity.java的setContentView方法

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

这里再对setContentView追进去,发现只能看到 里面的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()方法创建的,我们进去看一下

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

这里也仅仅是new了一个AppCompatDelegateImpl对象,所以我们的setContentView最终是调用的AppCompatActivity中的setContentView方法,我们进去看

2.2AppCompatDelegateImpl.java中的setContentView方法

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

这里其实就没啥好看的了,一个一个点进去,仔细看看就好了。与Activity没啥区别

我们回到AppCompatDelegateImpl上面来,

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

发现他继承了LayoutInflater里面的Factory接口,我们可以查找一下他内部使用LayoutInflater的地方

2.3 AppCompatDelegateImpl.java中的installViewFactory方法

@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    //如果他的factory为空就给他设置一个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");
        }
    }
}

每次创建View的时候,会调用onCreateView,所以我们过去看看

AppCompatDelegateImpl.java

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

然后继续跳转到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 */
    );
}

然后我们继续进到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);
}

//....

总结:

只要我们外部继承了AppCompatActivity,那么我们创建任何的View都会被这里拦截,然后给你返回一个AppCompatXXX,所以才会出现我们最开始的那个问题,继承自不同的父类,导致输出的结果不一样的问题

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343