setContentView的时候,到底发生了什么

注:文中所有代码均来源于API 27;文中代码经过大量省略,若要知道详情,还需进入源码中细细探索。

前言

关于setContentView方法,想必大家对这个方法既熟悉又陌生,熟悉的原因是因为基本上我们每创建一个activity,都会调用这个方法,比如:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

而且这种写法已经是创建activity的模版了,新建一个activity,必然会在onCreate这里使用这个方法。陌生的原因是因为我们从来都只用这个方法,但是很少会去了解这个方法具体干了什么的,会出现这种状况的原因其实很简单,google把这个方法封装的太好用了,我们只需要调用这个方法,那么布局就会跟这个activity绑定在一起。但是功能越强大,使用越简单的东西,内部往往有很多不为人知的精密设计。本着探索的精神,我们深入setContentView内部,尝试去了解一下这个方法内部的结构。

Activity探索

Activity

我们往往是新建一个类,让这个类继承Activity,然后再在onCreate中调用setContentView,那么很显然,setContentView的本体就在Activity中,让我们进入Activity中一探究竟。

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

进来之后,却发现即便进入了Activity中的setContentView中了,但这个方法的本体却异常简单,这肯定不是我们想知道的结果,所以我们还需要接着探索。

根据参数layoutResID来进行下一步的探索,发现内部是getWindow().setContentView(layoutResID),所以其实我们是调用了getWindow()的setContentView,那么这个getWindow()得到的东西又是什么呢:

private Window mWindow;

public Window getWindow() {
    return mWindow;
}

原来是Window,这一听就感觉是个大IP,想想都觉得是顶层的抽象,所以这个mWindow到底是什么:

    final void attach(一堆参数) {
    	...
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
    }

原来这个mWindow其实就是一个PhoneWindow,而且根据调查发现,Window的子类,也只有PhoneWindow这一个类。不管怎么说,当我们在Activity中调用setContentView的时候,其实内部调用了PhoneWindowsetContentView

这里先总结下得到的信息:

  1. 一个Activity对应一个Window
  2. Window只有一个子类:PhoneWindow
  3. 在Activity中调用setContentView,其实就是调用Activity里面的PhoneWindow的setContentView方法

探索PhoneWindow

既然实质是用了PhoneWindow的setContentView,那么我们就来看看这里的setContentView长什么样吧。

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

看这这堆恶心的代码,这绝对是本体无误!Let’s read the fucking source code。

看源码之前,居然有官方的注释,兴高采烈的阅读一番:

注意:当主题属性设置完时,FEATURE_CONTENT_TRANSITIONS可以在安装窗户装饰的过程中设置。发生这种情况之前,不要检查该功能。

所以这是啥,完全看不懂,算了算了,从代码角度来理解这些方法吧。

第一个if:

if (mContentParent == null) {
        installDecor();
} 

mContentParent是啥:

ViewGroup mContentParent;

在哪里赋值的:

private void installDecor() {
	...
	mContentParent = generateLayout(mDecor);
	...
}

原来就是在installDecor里面赋值的,所以第一个if就一定会进去,让我们来看看installDecor方法:

    private void installDecor() {
        // 巨量源码
    }

installDecor进去,源码无数,可以列为研究setContentView道路上的第一个boss,俗称——新手劝退者。

当遇到巨量源码的方法的时候应该怎么办

  1. 通览整个方法,了解方法的大概结构
  2. 根据结构,大事化小
  3. 分析跳过不重要的代码(非当前研究点的代码),直捣核心
  4. 不要死钻牛角尖,学会跳过去(遇到实在看不懂的代码的时候)

以上策略虽然简单,也许大家看源码就是这样看的,但是需要意识到有这个东西,通过总结得到的经验去解决问题和通过阅历得到的经验去解决问题的感觉是不一样的。

根据以上策略,我们将installDecor方法简化为:

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

既然都简化成这个样子了,那么就来仔细的看看吧。

首先第一个if,给mDecor赋值,进generateDecor里面看看:

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

中规中矩的赋值。

我们来看第二个if,又是赋值,好吧,看看generateLayout方法:

protected ViewGroup generateLayout(DecorView decor) {
	// 巨量源码
}

二号boss出现

这个方法有返回值,直接看返回值吧:

// PhoneWindow.class
protected ViewGroup generateLayout(DecorView decor) {
	...
	ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
	...
	return contentParent;
}

所以这个PhoneWindow居然也能直接findViewById,话说这find是个啥,哪来的ID,受累看看上面那些被我忽视的代码。
简化如下:

// PhoneWindow.class
protected ViewGroup generateLayout(DecorView decor) {

	// 结构相似的requestFeature(xxx)	
	
	// 一堆妙(kan)不(qiu)可(bu)言(dong)的代码
	
	// int变量layoutResource赋值,都赋这种值R.layout.XXX(即:layoutResource = R.layout.XXX)
	
	mDecor.startChanging();
	mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    
	ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
	...
	return contentParent;
}

看到layoutResource赋值是布局,精神为之一振,findViewById肯定是find这里面的view,随便抓一个在某个条件下赋值的布局来瞅瞅。

// screen_simple.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

里面有个FrameLayout的ID是:android:id="@android:id/content"。
再看看ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

嘿嘿嘿,就是这个。看看mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);,看看这个布局做了什么处理。

DecorView.class
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
  ...
  final View root = inflater.inflate(layoutResource, null);
  if (XXX) {
    if (XXX) {
      addView(mDecorCaptionView, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mDecorCaptionView.addView(root, new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
    } else {
    addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
  }
  ...
}

用我们熟悉的LayoutInflater,将这个布局转化为一个View,然后将view直接加入DecorView。直接就加进DecorView?难不成这个DecorView是一个ViewGroup。没错:

public class DecorView extends FrameLayout implement xxx

让我们继续回到PhoneWindow的generateLayout方法中:

// PhoneWindow.class
protected ViewGroup generateLayout(DecorView decor) {

	// 结构相似的requestFeature(xxx)	
	
	// 一堆妙(kan)不(qiu)可(bu)言(dong)的代码
	
	// int变量layoutResource赋值,都赋这种值R.layout.XXX(即:layoutResource = R.layout.XXX)
	
	mDecor.startChanging();
	mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    
	ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
	...
	return contentParent;
}

要接收的东西有点多,让我们捋一捋。

  1. 在某个条件下,layoutResource = R.layout.XXX,该布局里面包含一个id为content的容器ViewGroup
  2. mDecor使用LayoutInflater,将这个布局转化成了View,并添加给了mDecor,也就是他自己
  3. 通过findViewById(ID_ANDROID_CONTENT),我们得到了R.layout.XXX布局中的那个容器ViewGroup

捋完收工,还记得我们开始是怎么研究到这里来的吗。

我们知道在activity中使用setContentView的实质其实就是使用PhoneWindow的setContentView,

// PhoneWindow.class
@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    }
    ...
}

PhoneWindow#setContentView的时候,我们遭遇了第一个if,里面是个installDecor()方法,

private void installDecor() {
    if (mDecor == null) {
        // 实质-> mDecor = new DecorView(参数),这是个ViewGroup
        mDecor = generateDecor(-1);
    }
    if (mContentParent == null) {
        // 得到一个布局,通过findViewById将布局中的一个ViewGroup得到。
        mContentParent = generateLayout(mDecor);
    }

根据对PhoneWindow内部的分析,务必让我再来总结一下installDecor方法,以及我们得到的知识:

  1. installDecor方法实质是将系统的一个布局文件转化成View,并将这个view添加到DecorView中。
  2. PhoneWindow持有DecorView
  3. 每个Activity都对应一个PhoneWidow
  4. 我们知道了在使用requestWindowFeature的时候,需要放在setContentView前面的原因(没想到吧?)

装载布局

既然已经看完了installDecor方法,让我们接着setContentView下面的内容,继续看。

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

在继续说之前,先提一个大家看到这里可能已经遗忘的一个变量,大家看上面的源码,第一个if里面的mContentParent变量大家还记得是什么吗,我这里再提一下。installDecor指定了一个布局,其中findViewById得到的一个ViewGroup就是这个mContentParent

我们接着看setContentView里面的第二个if else:

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }

那么到底走的if还是else呢,这里做了一个判断,是否设置了FEATURE_CONTENT_TRANSITIONS,这个是在哪里设置的呢,其实是在generateLayout方法中。

protected ViewGroup generateLayout(DecorView decor) {
	...
	if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) {
		requestFeature(FEATURE_CONTENT_TRANSITIONS);
	}
	if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) {
		requestFeature(FEATURE_ACTIVITY_TRANSITIONS);
	}
	...
}

这里根据判断里面的条件,好像是,如果没有设置,就默认为false,那么我们到底设置了没有呢,看源码,这里我们就不看Android的Java源码了,直接看XML文件的源码。
使用AS新建一个项目,打开res目录下的values文件夹,有一个styles.xml文件:

里面是这样的:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

我们来看看他的parent:

<style name="Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light.DarkActionBar"/>

继续看parent:

 <style name="Base.Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light">

继续:

<style name="Base.Theme.AppCompat.Light" parent="Base.V7.Theme.AppCompat.Light">

继续:

<style name="Base.V7.Theme.AppCompat.Light" parent="Platform.AppCompat.Light">

继续:

 <style name="Platform.AppCompat.Light" parent="android:Theme.Holo.Light">

继续:

<style name="Theme.Holo.Light" parent="Theme.Light">

继续:

<style name="Theme.Light">

嗯?没了,没关系,还有Theme:

<style name="Theme">
	...
	<item name="windowContentTransitions">false</item>
	<item name="windowActivityTransitions">false</item>
	...
</style>

找了那么久,原来这里设置的是false啊,所以:

protected ViewGroup generateLayout(DecorView decor) {
	...
	if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) {
		requestFeature(FEATURE_CONTENT_TRANSITIONS);
	}
	if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) {
		requestFeature(FEATURE_ACTIVITY_TRANSITIONS);
	}
	...
}

这里就会执行,那么:

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

第二个if else就会进if里面,所以我们现在来到了:

final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext());
transitionTo(newScene);

我们来看看getSceneForLayout方法:

public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {
    SparseArray<Scene> scenes = (SparseArray<Scene>) sceneRoot.getTag(
            com.android.internal.R.id.scene_layoutid_cache);
    if (scenes == null) {
        scenes = new SparseArray<Scene>();
        sceneRoot.setTagInternal(com.android.internal.R.id.scene_layoutid_cache, scenes);
    }
    Scene scene = scenes.get(layoutId);
    if (scene != null) {
        return scene;
    } else {
        scene = new Scene(sceneRoot, layoutId, context);
        scenes.put(layoutId, scene);
        return scene;
    }
}

挺多的哈,不过好在这个方法比较简单,大概就是将mContentParent和布局layoutResID都装进Scene里面了,所以我们来看看接下来transitionTo(newScene)是怎么处理newScene

private void transitionTo(Scene scene) {
    if (mContentScene == null) {
        scene.enter();
    } else {
        mTransitionManager.transitionTo(scene);
    }
    mContentScene = scene;
}

不知道是if还是else,我们来看看else的:

public void transitionTo(Scene scene) {
    changeScene(scene, getTransition(scene));
}

继续:

private static void changeScene(Scene scene, Transition transition) {
    final ViewGroup sceneRoot = scene.getSceneRoot();
    if (!sPendingTransitions.contains(sceneRoot)) {
        if (transition == null) {
            scene.enter();
        } else {
        	...
            scene.enter();
            ...
        }
    }
}

好像无论如何都会进入scene.enter();,那么我们就进去好好看看:

极简版:
public void enter() {
	...
	LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
	...
}

mLayoutId是传递通过scene的构造方法传递进来的布局ID,mSceneRoot是通过构造方法传递进来的之前的mContentParent,还记得这个吧,这节开篇强调过:

回忆模式:
在继续说之前,先提一个大家看到这里可能已经遗忘的一个变量,大家看上面的源码,第一个if里面的mContentParent变量大家还记得是什么吗,我这里再提一下。installDecor指定了一个布局,其中findViewById得到的一个ViewGroup就是这个mContentParent

好了,所以这个enter方法其实就是将我们写的布局文件通过LayoutInflater放到Android系统给我们指定的一个父容器mContentParent中。

而且mContentParent是DecorView的,现在我们写的布局也进入DecorView了,那么这个DecorView将包含整个界面,甚至可以这样说,DecorView就是我们直接看到的界面了,那么继续setContentView:

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

接下来是:

mContentParent.requestApplyInsets();

所以:

public void requestApplyInsets() {
    requestFitSystemWindows();
}

requestFitSystemWindows方法的名字听起来是将界面装载到系统窗口中,大概像这样:
在这里插入图片描述

所以关于setContentView的探索就到此为止。有读者就说了,为什么啊,看了这么久好不容易看到这里来,你一句就给我打发了?继续跟源码啊!

并不是我不愿意跟了,而是再跟就要进native方法里面了,里面牵扯了不少C语言相关的代码,就不贴在此处了,难度太大,而且内容涉及Android的显示系统,这也是一个知识面很大的内容,不是轻易就能说完的,所以适可而止是最好的。

好的,接下来我们来总结一下这一小节:

这一小节,内容看似很多,其实做的事情很简单。

  1. Scene包裹布局ID和mContentParent
  2. 通过LayoutInflater,将布局添加到mContentParent中
  3. 将整个view装载到系统界面上

这里大概说一下LayoutInflater的工作原理,其实就是解析XML布局,将这个布局转化为View。

总结

所以整个setContentView内部做了如下事:

  1. 在Activity中调用setContentView(实际调用PhoneWindow#setContentView)
  2. 新建DecorView实例
  3. 设置界面主题(requestFeature)
  4. 确定主题界面(layoutResource = R.layout.xxx
  5. 在主题界面抽取内容ViewGroup(mContentParent = findViewById)
  6. 将我们自己创建的布局界面和Android提供的内容mContentParent打包进Scene
  7. 通过LayoutInflater解析布局,将布局转化为View
  8. 将view添加到mContentParent中
  9. 将整个界面装载到系统界面中

AppCompatActivity探索

配合源码食用,效果更佳

虽然已经看完ActivitysetContentView做了什么,但是我们平时不会直接使用Activity,而是使用AppCompatActivity,所以让我们来看看AppCompatActivitysetContentView内部是怎么实现的。

我们简单跟踪一下内容的实现流程:

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

跟Activity的setContentView不一样了,没关系,我们来看看getDelegate是什么

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

看看这个mDelegate是何方神圣:

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

继续:

private static AppCompatDelegate create(Context context, Window window,
        AppCompatCallback callback) {
    if (Build.VERSION.SDK_INT >= 24) {
        return new AppCompatDelegateImplN(context, window, callback);
    } else if (Build.VERSION.SDK_INT >= 23) {
        return new AppCompatDelegateImplV23(context, window, callback);
    } else {
        return new AppCompatDelegateImplV14(context, window, callback);
    }
}

这里根据不同的SDK版本有不一样的东西,不愧是兼容的Activity,那么到底走哪一个呢,其实走哪一个都没关系,为什么呢,因为:

class AppCompatDelegateImplN extends AppCompatDelegateImplV23
class AppCompatDelegateImplV23 extends AppCompatDelegateImplV14
AppCompatDelegateImplV14 extends AppCompatDelegateImplV9

而实际调用的setContentView其实是AppCompatDelegateImplV9setContentView,所以:

// AppCompatDelegateImplV9.class
@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();
}

所以AppCompatActivity实际是调用了AppCompatDelegateImplV9setContentView方法,还记得Activity实际调用了谁的setContentView吗,是PhoneWindowsetContentView,那就继续看吧。

private void ensureSubDecor() {
	...
	mSubDecor = createSubDecor()
	...
}

看名字是不是有点类似DecorView?继续跟

// 简化版
private ViewGroup createSubDecor() {
	...
	final LayoutInflater inflater = LayoutInflater.from(mContext);
	ViewGroup subDecor = (ViewGroup) inflater.inflate(R.layout.xxx, null);
	...
	mWindow.setContentView(subDecor);
	...
	return subDecor;
}

这里我提供了简化版的源码,真正的源码不是这样写的,其中这个R.layout.xxx,和Activity里面的一样,系统也提供了各种条件下的布局文件。

我们来看看mWindow.setContentView(subDecor);,首先这个window是什么,由于window只有一个子类,所有这里的mWindow其实就是PhoneWindow了,我们来看看这个setContentView方法:

@Override
public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        view.setLayoutParams(params);
        // 此处跟Activity那里有区别
        // Activity-> final Scene newScene  = Scene.getSceneForLayout(mContentParent, layoutResID, getContext());
        final Scene newScene = new Scene(mContentParent, view);
        transitionTo(newScene);
    } else {
        // 区别: mLayoutInflater.inflate(layoutResID, mContentParent);
        mContentParent.addView(view, params);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

其实内容就是调用Activity的setContentView方法,不过这里有部分地方不一样,代码上我已标记。

这里总结下到目前为止,AppCompatActivity在setContentView的时候做了啥:

以前到这里到流程是:

  1. 调用setContentView(实际调用PhoneWindow的setContentView)
  2. 根据主题选择一个系统布局
  3. 将我们的布局放进系统布局中

现在我们的流程是:

  1. 调用setContentView(实际调用AppCompatDelegateImplV9的setContentView)
  2. 创建了一个新的ViewGroup,并叫做subDecor
  3. 调用PhoneWindow的setContentView
  4. 根据主题选择一个系统布局
  5. 将subDecor放到系统布局中

我们继续来看AppCompatDelegateImplV9setContentView方法:

@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();
}

然后将我们的布局添加到subDecor中去,接着回调onContentChanged方法。

那么这个onContentChanged方法做了什么,跟踪到最后其实这个方法里面什么也没做:

public void onContentChanged();

好了,我们来总结下AppCompatActivity在setContentView时的流程:

  1. 调用setContentView(实际调用AppCompatDelegateImplV9的setContentView)
  2. 创建了一个新的ViewGroup,并叫做subDecor
  3. 调用PhoneWindow的setContentView
  4. 根据主题选择一个系统布局
  5. 将subDecor放到系统布局中
  6. 将我们的布局放到subDecor中
  7. 回调onContentChanged()

总结

我们分别看了Activity和AppCompatActivity在setContentView内部的流程,可以得出以下结论:

Activity其实长这样:

AppCompatActivity长这样:

图片好像太过简陋了哈= =

不过总体来讲,Activity和AppCompatActivity在setContentView的时候,内部的区别并不大,只是AppCompatActivity会多一些东西。

虽然前面已经做了无数总结,但是在最后再做一次简单的总结,在setContentView的时候,系统内部做了不少复杂的操作,系统会指定根据这种的主题来确定主题布局,然后将我们设计的布局装载到主题布局中,最后将整个主题布局加载到系统窗口中。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页