从源码角度剖析 setContentView() 背后的机制

转自:https://juejin.im/post/58dcd12e61ff4b006b051908

 
 

从源码角度剖析 setContentView() 背后的机制

注:本文基于 AS 2.3,示例中的 Activity 继承自 AppcompatActivity。

示例


日常开发中,我们在 Activity 中基本上不可避免的都会使用到  setContentView() 这行代码,而理解它背后的机制能够让我们对日常的优化有更深地理解,网上也有些许文章介绍该机制,但是大部分的文章都是基于应用中 Activity 继承自 Activity,而早从 API 7 开始,google 就建议我们继承自 AppcompatActivity 而不是 Acitivity 了,虽然说从源码角度上本质上区别可能不是很大,但是还是有必要重新整理一遍思路。下面我们就从示例开始着手,一步一步理解  setContentView() 背后到底干了些什么,首先我们创建一个最普通的应用,利用 Android Studio 给我们创建的最开始就好了,MainAcitivty 代码如下 ——
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }

activity_main.xml 代码如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.joker.delete.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</android.support.constraint.ConstraintLayout>

对于 Theme 我们也不做任何的更改 ——

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

然后我们启动应用就行了,启动好应用后我们就来看一看我们当前应用的视图结构,如果你是老版本的 SDK,在 SDK 的 tools 目录下还存在有 hierarchyviewer.bat 这个文件的话,你就打开它来查看我们的应用视图,而如果你是新版本的 SDK 的话,需要使用 Layout Inspector 来查看当前应用的视图结构(如果你还不知道 Layout Inspector 的话,可以看一下我的另一篇博客Layout Inspector —— Android Studio 替代 Hierarchy Viewer 的新方案),而笔者的 SDK 是最新版本,所以笔者使用的是 Layout Inspector。打开后,我们可以看到类似如下的目录结构 ——

Layout Inspector

左侧就是我们的应用视图结构了,而点击相应的控件后,右侧会出现控件的相关属性,有一个行叫做 mID,顾名思义,他就是该控件的 id 值,这里先提一下,我们后面会用到。我们可以看到左边的视图结构是这样的 ——

应用视图目录

其中第七行的 ConstrainLayout 是 activity_main.xml 最外层的控件,为了更直观一些,我画了如下一张图 ——

应用视图目录

源码解析

AppCompatDelegateImplV7


代码剖析的起点就是我们 MainActivity 中的  setContentView(),点进去我们可以看到它进入了 AppcompatActivity 中的  setContentView() 方法中 —— AppcompatActivity#setContentView()

我们可以看到,其内部是调用了 AppcompatDeletegate 的 setContentView 方法,我们再不妨点进去 ——

AppcompatDeletegate#setContentView()

原来 AppcompatDeletegate 是一个抽象类,setContentView() 又是一个抽象方法,那么我们就来看看它的实现类有哪些 ——

AppcompatDeletegate 实现类

看来它的子类还是挺多的,实际上 setContentView() 方法在 AppcompatDelegateImplV7 中实现的,另外这里透露一个小技巧,我们可以看到 AppcompatDeletegate 的抽象方法 setContentView() 方法前面有一个绿色的向下箭头,它实际上就是告诉你有那些子类实现了这个方法,就像下面这样 ——

快捷方式

我们可以直接点击绿色的箭头这样就进入了 AppcompatDelegateImplV7 中的 setContentView() 方法,源码如下 ——

AppcompatDelegateImplV7#setContentView()

我们可以看到代码非常的简单,其关键点也就是在277、278、280三行,277行代码看方法名就是要确定能够构造出一个 SubDecor,这样就能确保 278 行的代码不会抛出异常,而 280 行代码就可以看得出来是将我们传入的参数,也就是 resId 所引用的那个布局放入 contentParent 中,而经过上面的分析,我们应该清楚,这个 contentParent 就是 ContentFrameLayout,我们也可以看到,这里的 contentParent 是通过 findViewId() 这个方法获取到的,而这个 id 正是 android.R.id.content,这和我们上面的分析图中所说的 ContentFrameLayout 的 id 是 content 相符合,而 mSubDecor 应该是一个内部包含有一个 ContentFrameLayout 的 ViewGroup。那么现在我们就来看看 ensureSubDecor() 方法,看看它内部都做了些什么 ——

AppcompatDelegateImplV7#ensureSubDecor()

很明显我们会进入到 312 行的 createSubDecor() 方法中(事实上我们也只需要研究这个方法) ——

private ViewGroup createSubDecor() {
    //...

    // Now let's make sure that the Window has installed its decor by retrieving it
    // [1]
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;


    if (!mWindowNoTitle) { if (mIsFloating) { // ... } else if (mHasActionBar) { // Now inflate the view using the themed context and set it as the content view // [2] subDecor = (ViewGroup) LayoutInflater.from(themedContext) .inflate(R.layout.abc_screen_toolbar, null); // [3] mDecorContentParent = (DecorContentParent) subDecor .findViewById(R.id.decor_content_parent); // ... } } // [4] 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) { // There might be Views already added to the Window's content view so we need to // migrate them to our content view while (windowContentView.getChildCount() > 0) { final View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } // Change our content FrameLayout to use the android.R.id.content id. // Useful for fragments. windowContentView.setId(View.NO_ID); // [5] contentView.setId(android.R.id.content); // The decorContent may have a foreground drawable set (windowContentOverlay). // Remove this as we handle it ourselves if (windowContentView instanceof FrameLayout) { ((FrameLayout) windowContentView).setForeground(null); } } // Now set the Window's content view with the decor // [6] mWindow.setContentView(subDecor); // ... return subDecor; }

由于源码太长,笔者截取了比较重要的一部分代码,我们先看到12行的 if 语句判断处,我们示例的代码会进入第15行的 else if 判断语句中,如果不够确定的话,我们也可以 debug 看一下 ——

debug

那么进入了这个 if 判断语句中做了什么事情呢?我们可以看到 [2] 处,它通过 inflate() 方法引入一个布局文件创建了 subDecor 并最终返回了这个 subDecor,那么根据前面的分析很明显这个 subDecor 应该是一个包含有一个 ContentFrameLayout 的 ViewGroup,事实上是不是这样的呢?我们不妨打开这个名为 R.layout.abc_screen_toolbar.xml 的布局文件,如下 ——

R.layout.abc_screen_toolbar.xml

好像没有 ContentFrameLayout,不要急,我们看到其中使用了一个 include 标签引入了一个 abc_screen_content_include.xml 的布局,那么它又长什么样呢?如下 ——

abc_screen_content_include.xml

没错,这就是上面所说的 ContentFrameLayout,与此同时我们发现上面视图结构中的前三个布局文件 ActionBarContainer、ContentFrameLayout、ActionBarOverlayLayout 都在这,原来我们应用中引入的就是这个布局文件,但是细心的小伙伴也发现了,此处的 ActionBarContainer 和 ActionBarOverlayLayout 的布局 id 与上面的图中是相同的,但是 ContentFrameLayout 的布局 id 与上图中不符,布局文件中叫做 action_bar_activity_content,而上面的视图结构中的是 content —— 原因就在于 [4]、[5] 处 —— [4] 处我们可以看到 subDecor 通过 findViewById(R.id.action_bar_activity_content) 拿到 ContentFrameLayot,将它赋给一个名为 contentView 的局部变量,而在 [5] 处我们又调用了 contentView.setId(android.R.id.content) 将 ContentFrameLayout 的 id 更改成了 content!所以 ActionBarOverlayLayout 及其底层控件我们都已经解析完了。源码解析到这里,我们一直是停留在 AppCompatDelegateImplV7 这个类中,目前我们接触到最高层的 ViewGroup 是 ActionBarOverlayLayout,而它的上层是 FrameLayout,再上层是 LinearLayout,再上层是 PhoneWindow$DecorView,这三层涉及到的是 PhoneWindow 内部的机制,下面我们继续来剖析 ——

PhoneWindow


再贴一次  createSubDecor() 的源码:
private ViewGroup createSubDecor() {
    //...

    // Now let's make sure that the Window has installed its decor by retrieving it
    // [1]
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;


    if (!mWindowNoTitle) { if (mIsFloating) { // ... } else if (mHasActionBar) { // Now inflate the view using the themed context and set it as the content view // [2] subDecor = (ViewGroup) LayoutInflater.from(themedContext) .inflate(R.layout.abc_screen_toolbar, null); // [3] mDecorContentParent = (DecorContentParent) subDecor .findViewById(R.id.decor_content_parent); // ... } } else { // ... } // [4] final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById( R.id.action_bar_activity_content); // [7] final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content); if (windowContentView != null) { // There might be Views already added to the Window's content view so we need to // migrate them to our content view while (windowContentView.getChildCount() > 0) { final View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } // Change our content FrameLayout to use the android.R.id.content id. // Useful for fragments. windowContentView.setId(View.NO_ID); // [5] contentView.setId(android.R.id.content); // The decorContent may have a foreground drawable set (windowContentOverlay). // Remove this as we handle it ourselves if (windowContentView instanceof FrameLayout) { ((FrameLayout) windowContentView).setForeground(null); } } // Now set the Window's content view with the decor // [6] mWindow.setContentView(subDecor); // ... return subDecor; }

首先我们先看到 [1] 处的 mWindow.getDecorView() 方法,实际上这个方法就是初始化 DecorView 内部的控件,可想而知,如果没有这个方法的话那么 [7] 处的代码将会报空指针,我们打开它的源码看一下 ——

Window#getDecorView()

Window 类是一个抽象类,而在 Android 中它只有一个实现类 —— PhoneWindow,毫无疑问我们应该打开 PhoneWindow 类查看它的 getDecorView() 方法 ——

PhoneWindow#getDecorView()

mDecor 是什么?实际上 mDecor 就是 DecorView,初始情况下 mDecor 一定是为空的,那么就是会进入installDecor() 方法,那么我们再点进去看一下这个方法 ——

private void installDecor() {
    if (mDecor == null) {
        // [1]
        // new DecorView(getContext(), -1); mDecor = generateDecor(); } if (mContentParent == null) { // [2] mContentParent = generateLayout(mDecor); // [3] final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( R.id.decor_content_parent); if (decorContentParent != null) { [4] mDecorContentParent = decorContentParent; if (mDecorContentParent.getTitle() == null) { mDecorContentParent.setWindowTitle(mTitle); } } else { mTitleView = (TextView)findViewById(R.id.title); if (mTitleView != null) { // ... } } } }

老规矩,由于源码太长,此处就不截图了,我将源码简化成如上的形式,首先 [1] 处实际上就是调用了 new DecorView(getContext(), -1); 实例化了一个 DecorView。然后我们看到第二个 if 判断语句,同样地,初始情况下 mContentParent 也是为空的,那么 mContentParent 又是什么东西呢 ——

mContentParent 声明

我们看到源码注释上写到 —— 这是放置 Window 内容的 View,它是 mDecor 本身或 mDecor 的子内容所应该填充的地方。它本身又是一个 ViewGroup 类型,所以我们这里就可以大胆的猜测,它是前面我们所提到的 ActionBarOverlayLayout 的父控件 FrameLayout,而根据方法名 generateLayout() 我们也就知道了,它就是加载 mDecor 布局的方法 ——

protected ViewGroup generateLayout(DecorView decor) {
    // // Inflate the window decor.

    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { // ... } else { // Embedded, so no decoration is needed. // [1] layoutResource = R.layout.screen_simple; // System.out.println("Simple!"); } // [2] View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in; // [3] ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); return contentParent; }

依旧由于源码太长,笔者只截取了部分源码,首先源码会进入 [1] 处,然后到达 [2] 处加载 screen_simple.xml 布局文件,接着就是将它添加进 DecorView,然后再将 screen_simple.xml中 id 为 content 的控件返回,那么关键点就是这个 screen_simple.xml 布局文件长什么样子了 ——

screen_simple.xml

没错!这就是 ActionBarOverlayLayout 上层的布局文件!而 id 为 content 的布局文件也正是 FrameLayout,它就是这个函数的返回值,那么我们再回到 installDecor() 方法中看到它是赋给 mContentParent 的,这也就正好验证了我们前面的猜想,mContentParent 就是 FrameLayout!到这里的话源码解析的差不多了,我们来重新整理一下整体的思路 ——

流程一览


MainActivity#setContentView(int resId) ->  AppCompatActivity#setContentView(int resId) -> AppCompatDelegate#setContentView(int resId) ->  AppCompatDelegateImplV7#setContentView(int resId)AppCompatDelegateImplV7#setContentView(int resId) 方法中做了主要做了两件事,首先是生成 DecorView,然后将 resId 引入的布局文件添加进 DecorView 布局中 id 为  content 的那个控件(事实上也就是 ContentFrameLayout),那么最重要的就是如何生成 DecorView 的  ensureSubDecor() 方法了——  ensureSubDecor() - > createSubDecor() ——
private ViewGroup createSubDecor() {
    //...

    // Now let's make sure that the Window has installed its decor by retrieving it
    // [1]
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;


    if (!mWindowNoTitle) { if (mIsFloating) { // ... } else if (mHasActionBar) { // Now inflate the view using the themed context and set it as the content view // [2] subDecor = (ViewGroup) LayoutInflater.from(themedContext) .inflate(R.layout.abc_screen_toolbar, null); // [3] mDecorContentParent = (DecorContentParent) subDecor .findViewById(R.id.decor_content_parent); // ... } } else { // ... } // [4] final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById( R.id.action_bar_activity_content); // [7] final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content); if (windowContentView != null) { // There might be Views already added to the Window's content view so we need to // migrate them to our content view while (windowContentView.getChildCount() > 0) { final View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } // Change our content FrameLayout to use the android.R.id.content id. // Useful for fragments. windowContentView.setId(View.NO_ID); // [5] contentView.setId(android.R.id.content); // The decorContent may have a foreground drawable set (windowContentOverlay). // Remove this as we handle it ourselves if (windowContentView instanceof FrameLayout) { ((FrameLayout) windowContentView).setForeground(null); } } // Now set the Window's content view with the decor // [6] mWindow.setContentView(subDecor); // ... return subDecor; }
  • [1]:引入了 screen_simple.xml 布局文件,它其中就是最顶层的 LinearLayout、ViewStub、FrameLayout(id 为 content),除了引入布局文件还有一件事就是通过 generateDecor()方法将 FrameLayout 的引用赋给了 mContentParent。
  • [2]:引入了 abc_screen_toolbar.xml 布局文件,它是下层的 ActionBarOverlayLayout、ActionBarContainer、ContentFrameLayout(最开始 id 为 action_bar_activity_content,后来被更改成 content) 等
  • [4]、[7]、[5]:将上面 id 为 content 的 FrameLayout 的 id 置空,将 id 为 action_bar_activity_content 的 ContentFrameLayout 的 id 置为 content

分析到这里我们好像漏了一个地方,应用的视图结构被我们分成了两个部分:上层的 LinearLayout、ViewStub、FrameLayout;下层的 ActionBarOverlayLayout、ActionBarContainer、ContentFrameLayout。它们如何连接起来的呢?答案就是 [6] 处的 mWindow.setContentView(),我们跟踪进去 ——

Window#setContentView()

意料之中,Window 是抽象类,而 PhoneWindow 是它的唯一实现类,所以我们打开 PhoneWindow 的 setContentView() 方法看一下 ——

PhoneWindow#setContentView()

关键点在第423行代码处,我们给 mContentParent 添加了传入的 view,mContentParent 就是我们在 [1] 处所提及的 FrameLayout,而传入的 view 就是 abc_screen_toolbar.xml 实例化的那个布局文件,所以这样它们就桥接起来了!

猜你喜欢

转载自www.cnblogs.com/ryq2014/p/9207240.html