【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,所以才会出现我们最开始的那个问题,继承自不同的父类,导致输出的结果不一样的问题

你可能感兴趣的:(【Android源码】Activity和AppCompatActivity的setContentView方法区别)