返回介绍

Android 应用 setContentView 与 LayoutInflater 加载解析机制源码分析

发布于 2025-02-28 12:35:44 字数 32272 浏览 0 评论 0 收藏 0

1 背景

其实之所以要说这个话题有几个原因:

  1. 理解 xml 等控件是咋被显示的原理,通常大家写代码都是直接在 onCreate 里 setContentView 就完事,没怎么关注其实现原理。
  2. 前面分析 《Android 触摸屏事件派发机制详解与源码分析三 Activity 篇》 时提到了一些关于布局嵌套的问题,当时没有深入解释。

所以接下来主要分析的就是 View 或者 ViewGroup 对象是如何添加至应用程序界面(窗口)显示的。我们准备从 Activity 的 setContentView 方法开始来说(因为默认 Activity 中放入我们的 xml 或者 Java 控件是通过 setContentView 方法来操作的,当调运了 setContentView 所有的控件就得到了显示)。

2 Android5.1.1(API 22)从 Activity 的 setContentView 方法说起

2-1 Activity 的 setContentView 方法解析

Activity 的源码中提供了三个重载的 setContentView 方法,如下:

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

可以看见他们都先调运了 getWindow() 的 setContentView 方法,然后调运 Activity 的 initWindowDecorActionBar 方法,关于

initWindowDecorActionBar 方法后面准备写一篇关于 Android ActionBar 原理解析的文章,所以暂时跳过不解释。

2-2 关于窗口 Window 类的一些关系

在开始分析 Activity 组合对象 Window 的 setContentView 方法之前请先明确如下关系(前面分析 《Android 触摸屏事件派发机制详解与源码分析三 Activity 篇》 时也有说过)。

看见上面图没?Activity 中有一个成员为 Window,其实例化对象为 PhoneWindow,PhoneWindow 为抽象 Window 类的实现类。

这里先简要说明下这些类的职责:

  1. Window 是一个抽象类,提供了绘制窗口的一组通用 API。

  2. PhoneWindow 是 Window 的具体继承实现类。而且该类内部包含了一个 DecorView 对象,该 DectorView 对象是所有应用窗口(Activity 界面) 的根 View。

  3. DecorView 是 PhoneWindow 的内部类,是 FrameLayout 的子类,是对 FrameLayout 进行功能的修饰(所以叫 DecorXXX),是所有应用窗口的根 View 。

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

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

2-3 窗口 PhoneWindow 类的 setContentView 方法

我们可以看见 Window 类的 setContentView 方法都是抽象的。所以我们直接先看 PhoneWindow 类的 setContentView(int layoutResID) 方法源码,如下:

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);
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
      cb.onContentChanged();
    }
  }

可以看见,第五行首先判断 mContentParent 是否为 null,也就是第一次调运);如果是第一次调用,则调用 installDecor() 方法,否则判断是否设置

FEATURE_CONTENT_TRANSITIONS Window 属性(默认 false),如果没有就移除该 mContentParent 内所有的所有子 View;接着 16 行

mLayoutInflater.inflate(layoutResID, mContentParent); 将我们的资源文件通过 LayoutInflater 对象转换为 View 树,并且添加至 mContentParent 视图中

(其中 mLayoutInflater 是在 PhoneWindow 的构造函数中得到实例对象的 LayoutInflater.from(context); )。

再来看下 PhoneWindow 类的 setContentView(View view) 方法和 setContentView(View view, ViewGroup.LayoutParams params) 方法源码,如下:

@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);
      final Scene newScene = new Scene(mContentParent, view);
      transitionTo(newScene);
    } else {
      mContentParent.addView(view, params);
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
      cb.onContentChanged();
    }
  }

看见没有,我们其实只用分析 setContentView(View view, ViewGroup.LayoutParams params) 方法即可,如果你在 Activity 中调运

setContentView(View view) 方法,实质也是调运 setContentView(View view, ViewGroup.LayoutParams params),只是 LayoutParams 设置为了

MATCH_PARENT 而已。

所以直接分析 setContentView(View view, ViewGroup.LayoutParams params) 方法就行,可以看见该方法与 setContentView(int layoutResID) 类似,只是少了 LayoutInflater 将 xml 文件解析装换为 View 而已,这里直接使用 View 的 addView 方法追加道了当前 mContentParent 而已。

所以说在我们的应用程序里可以多次调用 setContentView() 来显示界面,因为会 removeAllViews。

2-4 窗口 PhoneWindow 类的 installDecor 方法

回过头,我们继续看上面 PhoneWindow 类 setContentView 方法的第 6 行 installDecor();代码,在 PhoneWindow 中查看 installDecor 源码如下:

private void installDecor() {
    if (mDecor == null) {
      mDecor = generateDecor();
      mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
      mDecor.setIsRootNamespace(true);
      if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
        mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
      }
    }
    if (mContentParent == null) {
      //根据窗口的风格修饰,选择对应的修饰布局文件,并且将 id 为 content 的 FrameLayout 赋值给 mContentParent
      mContentParent = generateLayout(mDecor);
      //......
      //初始化一堆属性值
    }
  }

我勒个去!又是一个死长的方法,抓重点分析吧。第 2 到 9 行可以看出,首先判断 mDecor 对象是否为空,如果为空则调用 generateDecor() 创建一个

DecorView(该类是 FrameLayout 子类,即一个 ViewGroup 视图),然后设置一些属性,我们看下 PhoneWindow 的 generateDecor 方法,如下:

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
  }

可以看见 generateDecor 方法仅仅是 new 一个 DecorView 的实例。

回到 installDecor 方法继续往下看,第 10 行开始到方法结束都需要一个 if (mContentParent == null) 判断为真才会执行,当 mContentParent 对象不为空则调用 generateLayout() 方法去创建 mContentParent 对象。所以我们看下 generateLayout 方法源码,如下:

protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.

    TypedArray a = getWindowStyle();

    //......
    //依据主题 style 设置一堆值进行设置

    // Inflate the window decor.

    int layoutResource;
    int features = getLocalFeatures();
    //......
    //根据设定好的 features 值选择不同的窗口修饰布局文件,得到 layoutResource 值

    //把选中的窗口修饰布局文件添加到 DecorView 对象里,并且指定 contentParent 值
    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
      throw new RuntimeException("Window couldn't find content container view");
    }

    //......
    //继续一堆属性设置,完事返回 contentParent
    return contentParent;
  }

可以看见上面方法主要作用就是根据窗口的风格修饰类型为该窗口选择不同的窗口根布局文件。mDecor 做为根视图将该窗口根布局添加进去,然后获

取 id 为 content 的 FrameLayout 返回给 mContentParent 对象。所以 installDecor 方法实质就是产生 mDecor 和 mContentParent 对象。

在这里顺带提一下:还记得我们平时写应用 Activity 时设置的 theme 或者 feature 吗(全屏啥的,NoTitle 等)?我们一般是不是通过 XML 的 android:theme 属性或者 java 的 requestFeature() 方法来设置的呢?譬如:

通过 java 文件设置:

requestWindowFeature(Window.FEATURE_NO_TITLE);

通过 xml 文件设置:

android:theme="@android:style/Theme.NoTitleBar"

对的,其实我们平时 requestWindowFeature() 设置的值就是在这里通过 getLocalFeature() 获取的;而 android:theme 属性也是通过这里的

getWindowStyle() 获取的。所以这下你应该就明白在 java 文件设置 Activity 的属性时必须在 setContentView 方法之前调用 requestFeature() 方法的原因了吧。

我们继续关注一下 generateLayout 方法的 layoutResource 变量赋值情况。因为它最终通过 View in = mLayoutInflater.inflate(layoutResource, null);decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 将 in 添加到 PhoneWindow 的 mDecor 对象。为例验证这一段代码分析我们用一个实例来进行说明,如下是一个简单的 App 主要代码:

AndroidManifest.xml 文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.yanbober.myapplication" >

  <application
    ......
    //看重点,我们将主题设置为 NoTitleBar
    android:theme="@android:style/Theme.Black.NoTitleBar" >
    ......
  </application>

</manifest>

主界面布局文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <TextView android:text="@string/hello_world"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

</RelativeLayout>

APP 运行界面:

看见没有,上面我们将主题设置为 NoTitleBar,所以在 generateLayout 方法中的 layoutResource 变量值为 R.layout.screen_simple ,所以我们看下系统这个 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>

布局中,一般会包含 ActionBar,Title,和一个 id 为 content 的 FrameLayout,这个布局是 NoTitle 的。

再来看下上面这个 App 的 hierarchyviewer 图谱,如下:

看见了吧,通过这个 App 的 hierarchyviewer 和系统 screen_simple.xml 文件比较就验证了上面我们分析的结论,不再做过多解释。

然后回过头可以看见上面 PhoneWindow 类的 setContentView 方法最后通过调运 mLayoutInflater.inflate(layoutResID, mContentParent); 或者 mContentParent.addView(view, params); 语句将我们的 xml 或者 java View 插入到了 mContentParent(id 为 content 的 FrameLayout 对象)ViewGroup 中。最后 setContentView 还会调用一个 Callback 接口的成员函数 onContentChanged 来通知对应的 Activity 组件视图内容发生了变化。

2-5 Window 类内部接口 Callback 的 onContentChanged 方法

上面刚刚说了 PhoneWindow 类的 setContentView 方法中最后调运了 onContentChanged 方法。我们这里看下 setContentView 这段代码,如下:

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

看着没有,首先通过 getCallback 获取对象 cb(回调接口),PhoneWindow 没有重写 Window 的这个方法,所以到抽象类 Window 中可以看到:

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

这个 mCallback 在哪赋值的呢,继续看 Window 类发现有一个方法,如下:

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

Window 中的 mCallback 是通过这个方法赋值的,那就回想一下,Window 又是 Activity 的组合成员,那就是 Activity 一定调运这个方法了,回到 Activity

发现在 Activity 的 attach 方法中进行了设置,如下:

final void attach(Context context, ActivityThread aThread,
    ......
    mWindow.setCallback(this);
    ......
  }

也就是说 Activity 类实现了 Window 的 Callback 接口。那就是看下 Activity 实现的 onContentChanged 方法。如下:

public void onContentChanged() {
  }

咦?onContentChanged 是个空方法。那就说明当 Activity 的布局改动时,即 setContentView() 或者 addContentView() 方法执行完毕时就会调用该方法。

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

2-6 setContentView 源码分析总结

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

  1. 创建一个 DecorView 的对象 mDecor,该 mDecor 对象将作为整个应用窗口的根视图。

  2. 依据 Feature 等 style theme 创建不同的窗口修饰布局文件,并且通过 findViewById 获取 Activity 布局文件该存放的地方(窗口修饰布局文件中 id 为 content 的 FrameLayout)。

  3. 将 Activity 的布局文件添加至 id 为 content 的 FrameLayout 内。

至此整个 setContentView 的主要流程就分析完毕。你可能这时会疑惑,这么设置完一堆 View 关系后系统是怎么知道该显示了呢?下面我们就初探一下关于 Activity 的 setContentView 在 onCreate 中如何显示的(声明一下,这里有些会暂时直接给出结论,该系列文章后面会详细分析的)。

2-7 setContentView 完以后 Activity 显示界面初探

这一小部分已经不属于 sentContentView 的分析范畴了,只是简单说明 setContentView 之后怎么被显示出来的(注意:Activity 调运 setContentView 方法自身不会显示布局的)。

记得前面有一篇文章 《Android 异步消息处理机制详解及源码分析》 的 3-1-2 小节说过,一个 Activity 的开始实际是 ActivityThread 的 main 方法(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。

那在这一篇我们再直接说一个知识点(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。

当启动 Activity 调运完 ActivityThread 的 main 方法之后,接着调用 ActivityThread 类 performLaunchActivity 来创建要启动的 Activity 组件,在创建 Activity 组件的过程中,还会为该 Activity 组件创建窗口对象和视图对象;接着 Activity 组件创建完成之后,通过调用 ActivityThread 类的 handleResumeActivity 将它激活。

所以我们先看下 handleResumeActivity 方法一个重点,如下:

final void handleResumeActivity(IBinder token,
      boolean clearHide, boolean isForward, boolean reallyResume) {
    // If we are getting ready to gc after going to the background, well
    // we are back active so skip it.
    ......
    // TODO Push resumeArgs into the activity for consideration
    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if (r != null) {
      ......
      // If the window hasn't yet been added to the window manager,
      // and this guy didn't finish itself or start another activity,
      // then go ahead and add the window.
      ......
      // If the window has already been added, but during resume
      // we started another activity, then don't yet make the
      // window visible.
      ......
      // The window is now visible if it has been added, we are not
      // simply finishing, and we are not starting another activity.
      if (!r.activity.mFinished && willBeVisible
          && r.activity.mDecor != null && !r.hideForNow) {
        ......
        if (r.activity.mVisibleFromClient) {
          r.activity.makeVisible();
        }
      }
      ......
    } else {
      // If an exception was thrown when trying to resume, then
      // just end this activity.
      ......
    }
  }

看见 r.activity.makeVisible(); 语句没?调用 Activity 的 makeVisible 方法显示我们上面通过 setContentView 创建的 mDecor 视图族。所以我们看下

Activity 的 makeVisible 方法,如下:

void makeVisible() {
    if (!mWindowAdded) {
      ViewManager wm = getWindowManager();
      wm.addView(mDecor, getWindow().getAttributes());
      mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
  }

看见没有,通过 DecorView(FrameLayout,也即 View)的 setVisibility 方法将 View 设置为 VISIBLE,至此显示出来。

到此 setContentView 的完整流程分析完毕。

3 Android5.1.1(API 22)看看 LayoutInflater 机制原理

上面在分析 setContentView 过程中可以看见,在 PhoneWindow 的 setContentView 中调运了 mLayoutInflater.inflate(layoutResID, mContentParent); ,在 PhoneWindow 的 generateLayout 中调运了 View in = mLayoutInflater.inflate(layoutResource, null); ,当时我们没有详细分析,只是告诉通过 xml 得到 View 对象。现在我们就来分析分析这一问题。

3-1 通过实例引出问题

在开始之前我们先来做一个测试,我们平时最常见的就是 ListView 的 Adapter 中使用 LayoutInflater 加载 xml 的 item 布局文件,所以咱们就以 ListView 为例,如下:

省略掉 Activity 代码等,首先给出 Activity 的布局文件,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <ListView
    android:id="@+id/listview"
    android:dividerHeight="5dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent"></ListView>

</LinearLayout>

给出两种不同的 ListView 的 item 布局文件。

textview_layout.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="100dp"
  android:layout_height="40dp"
  android:text="Text Test"
  android:background="#ffa0a00c"/>

textview_layout_parent.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  android:layout_height="wrap_content"
  android:layout_width="wrap_content"
  xmlns:android="http://schemas.android.com/apk/res/android">

  <TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="40dp"
    android:text="Text Test"
    android:background="#ffa0a00c"/>

</LinearLayout>

ListView 的自定义 Adapter 文件:

public class InflateAdapter extends BaseAdapter {
  private LayoutInflater mInflater = null;

  public InflateAdapter(Context context) {
    mInflater = LayoutInflater.from(context);
  }

  @Override
  public int getCount() {
    return 8;
  }

  @Override
  public Object getItem(int position) {
    return null;
  }

  @Override
  public long getItemId(int position) {
    return 0;
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    //说明:这里是测试 inflate 方法参数代码,不再考虑性能优化等 TAG 处理
    return getXmlToView(convertView, position, parent);
  }

  private View getXmlToView(View convertView, int position, ViewGroup parent) {
    View[] viewList = {
        mInflater.inflate(R.layout.textview_layout, null),
//        mInflater.inflate(R.layout.textview_layout, parent),
        mInflater.inflate(R.layout.textview_layout, parent, false),
//        mInflater.inflate(R.layout.textview_layout, parent, true),
        mInflater.inflate(R.layout.textview_layout, null, true),
        mInflater.inflate(R.layout.textview_layout, null, false),

        mInflater.inflate(R.layout.textview_layout_parent, null),
//        mInflater.inflate(R.layout.textview_layout_parent, parent),
        mInflater.inflate(R.layout.textview_layout_parent, parent, false),
//        mInflater.inflate(R.layout.textview_layout_parent, parent, true),
        mInflater.inflate(R.layout.textview_layout_parent, null, true),
        mInflater.inflate(R.layout.textview_layout_parent, null, false),
    };

    convertView = viewList[position];

    return convertView;
  }
}

当前代码运行结果:

PS:当打开上面 viewList 数组中任意一行注释都会抛出异常(java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView)。

你指定有些蒙圈了,而且比较郁闷,同时想弄明白 inflate 的这些参数都是啥意思。运行结果为何有这么大差异呢?

那我告诉你,你现在先别多想,记住这回事,咱们先看源码,下面会告诉你为啥。

3-2 从 LayoutInflater 源码实例化说起

我们先看一下源码中 LayoutInflater 实例化获取的方法:

public static LayoutInflater from(Context context) {
    LayoutInflater LayoutInflater =
        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
      throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
  }

看见没有?是否很熟悉?我们平时写应用获取 LayoutInflater 实例时不也就两种写法吗,如下:

LayoutInflater lif = LayoutInflater.from(Context context);

  LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

可以看见 from 方法仅仅是对 getSystemService 的一个安全封装而已。

3-3 LayoutInflater 源码的 View inflate(…) 方法族剖析

得到 LayoutInflater 对象之后我们就是传递 xml 然后解析得到 View,如下方法:

public View inflate(int resource, ViewGroup root) {
    return inflate(resource, root, root != null);
  }

继续看 inflate(int resource, ViewGroup root, boolean attachToRoot) 方法,如下:

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
      Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
          + Integer.toHexString(resource) + ")");
    }

    final XmlResourceParser parser = res.getLayout(resource);
    try {
      return inflate(parser, root, attachToRoot);
    } finally {
      parser.close();
    }
  }

这个方法的第 8 行获取到 XmlResourceParser 接口的实例(Android 默认实现类为 Pull 解析 XmlPullParser)。接着看第 10 行 inflate(parser, root, attachToRoot);

,你会发现无论哪个 inflate 重载方法最后都调运了 inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) 方法,如下:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
      Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

      final AttributeSet attrs = Xml.asAttributeSet(parser);
      Context lastContext = (Context)mConstructorArgs[0];
      mConstructorArgs[0] = mContext;
      //定义返回值,初始化为传入的形参 root
      View result = root;

      try {
        // Look for the root node.
        int type;
        while ((type = parser.next()) != XmlPullParser.START_TAG &&
            type != XmlPullParser.END_DOCUMENT) {
          // Empty
        }
        //如果一开始就是 END_DOCUMENT,那说明 xml 文件有问题
        if (type != XmlPullParser.START_TAG) {
          throw new InflateException(parser.getPositionDescription()
              + ": No start tag found!");
        }
        //有了上面判断说明这里 type 一定是 START_TAG,也就是 xml 文件里的 root node
        final String name = parser.getName();

        if (DEBUG) {
          System.out.println("**************************");
          System.out.println("Creating root view: "
              + name);
          System.out.println("**************************");
        }

        if (TAG_MERGE.equals(name)) {
        //处理 merge tag 的情况(merge,你懂的,APP 的 xml 性能优化)
          //root 必须非空且 attachToRoot 为 true,否则抛异常结束(APP 使用 merge 时要注意的地方,
          //因为 merge 的 xml 并不代表某个具体的 view,只是将它包起来的其他 xml 的内容加到某个上层
          //ViewGroup 中。)
          if (root == null || !attachToRoot) {
            throw new InflateException("<merge /> can be used only with a valid "
                + "ViewGroup root and attachToRoot=true");
          }
          //递归 inflate 方法调运
          rInflate(parser, root, attrs, false, false);
        } else {
          // Temp is the root view that was found in the xml
          //xml 文件中的 root view,根据 tag 节点创建 view 对象
          final View temp = createViewFromTag(root, name, attrs, false);

          ViewGroup.LayoutParams params = null;

          if (root != null) {
            if (DEBUG) {
              System.out.println("Creating params from root: " +
                  root);
            }
            // Create layout params that match root, if supplied
            //根据 root 生成合适的 LayoutParams 实例
            params = root.generateLayoutParams(attrs);
            if (!attachToRoot) {
              // Set the layout params for temp if we are not
              // attaching. (If we are, we use addView, below)
              //如果 attachToRoot=false 就调用 view 的 setLayoutParams 方法
              temp.setLayoutParams(params);
            }
          }

          if (DEBUG) {
            System.out.println("-----> start inflating children");
          }
          // Inflate all children under temp
          //递归 inflate 剩下的 children
          rInflate(parser, temp, attrs, true, true);
          if (DEBUG) {
            System.out.println("-----> done inflating children");
          }

          // We are supposed to attach all the views we found (int temp)
          // to root. Do that now.
          if (root != null && attachToRoot) {
            //root 非空且 attachToRoot=true 则将 xml 文件的 root view 加到形参提供的 root 里
            root.addView(temp, params);
          }

          // Decide whether to return the root that was passed in or the
          // top view found in xml.
          if (root == null || !attachToRoot) {
            //返回 xml 里解析的 root view
            result = temp;
          }
        }

      } catch (XmlPullParserException e) {
        InflateException ex = new InflateException(e.getMessage());
        ex.initCause(e);
        throw ex;
      } catch (IOException e) {
        InflateException ex = new InflateException(
            parser.getPositionDescription()
            + ": " + e.getMessage());
        ex.initCause(e);
        throw ex;
      } finally {
        // Don't retain static reference on context.
        mConstructorArgs[0] = lastContext;
        mConstructorArgs[1] = null;
      }

      Trace.traceEnd(Trace.TRACE_TAG_VIEW);
      //返回参数 root 或 xml 文件里的 root view
      return result;
    }
  }

从上面的源码分析我们可以看出 inflate 方法的参数含义:

  • inflate(xmlId, null); 只创建 temp 的 View,然后直接返回 temp。

  • inflate(xmlId, parent); 创建 temp 的 View,然后执行 root.addView(temp, params);最后返回 root。

  • inflate(xmlId, parent, false); 创建 temp 的 View,然后执行 temp.setLayoutParams(params);然后再返回 temp。

  • inflate(xmlId, parent, true); 创建 temp 的 View,然后执行 root.addView(temp, params);最后返回 root。

  • inflate(xmlId, null, false); 只创建 temp 的 View,然后直接返回 temp。

  • inflate(xmlId, null, true); 只创建 temp 的 View,然后直接返回 temp。

到此其实已经可以说明我们上面示例部分执行效果差异的原因了(在此先强调一个 Android 的概念,下一篇文章我们会对这段话作一解释:我们经常使用 View 的 layout_width 和 layout_height 来设置 View 的大小,而且一般都可以正常工作,所以有人时常认为这两个属性就是设置 View 的真实大小一样;然而实际上这些属性是用于设置 View 在 ViewGroup 布局中的大小的;这就是为什么 Google 的工程师在变量命名上将这种属性叫作 layout_width 和 layout_height,而不是 width 和 height 的原因了。),如下:

  • mInflater.inflate(R.layout.textview_layout, null) 不能正确处理我们设置的宽和高是因为 layout_width,layout_height 是相对了父级设置的,而此 temp 的 getLayoutParams 为 null。
  • mInflater.inflate(R.layout.textview_layout, parent) 能正确显示我们设置的宽高是因为我们的 View 在设置 setLayoutParams 时 params = root.generateLayoutParams(attrs) 不为空。
    Inflate(resId , parent,false ) 可以正确处理,因为 temp.setLayoutParams(params);这个 params 正是 root.generateLayoutParams(attrs);得到的。
  • mInflater.inflate(R.layout.textview_layout, null, true) 与 mInflater.inflate(R.layout.textview_layout, null, false) 不能正确处理我们设置的宽和高是因为 layout_width,layout_height 是相对了父级设置的,而此 temp 的 getLayoutParams 为 null。
  • textview_layout_parent.xml 作为 item 可以正确显示的原因是因为 TextView 具备上级 ViewGroup,上级 ViewGroup 的 layout_width,layout_height 会失效,当前的 TextView 会有效而已。
  • 上面例子中说放开那些注释运行会报错 java.lang.UnsupportedOperationException:
    addView(View, LayoutParams) is not supported 是因为 AdapterView 源码中调用了 root.addView(temp, params);而此时的 root 是我们的 ListView,ListView 为 AdapterView 的子类,所以我们看下 AdapterView 抽象类中 addView 源码即可明白为啥了,如下:
/**
   * This method is not supported and throws an UnsupportedOperationException when called.
   *
   * @param child Ignored.
   *
   * @throws UnsupportedOperationException Every time this method is invoked.
   */
  @Override
  public void addView(View child) {
    throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
  }
  • 这里不再做过多解释。

咦?别急,到这里指定机智的人会问,我们在写 App 时 Activity 中指定布局文件的时候,xml 布局文件或者我们用 java 编写的 View 最外层的那个布局是可以指定大小的啊?他们最外层的 layout_width 和 layout_height 都是有作用的啊?

是这样的,还记得我们上面的分析吗?我们自己的 xml 布局通过 setContentView() 方法放置到哪去了呢?记不记得 id 为 content 的 FrameLayout 呢?所以我们 xml 或者 java 的 View 的最外层布局的 layout_width 和 layout_height 属性才会有效果,就是这么回事而已。

3-4 LayoutInflater 源码 inflate(…) 方法中调运的一些非 public 方法剖析

看下 inflate 方法中被调运的 rInflate 方法,源码如下:

void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
      boolean finishInflate, boolean inheritContext) throws XmlPullParserException,
      IOException {

    final int depth = parser.getDepth();
    int type;
    //XmlPullParser 解析器的标准解析模式
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
      //找到 START_TAG 节点程序才继续执行这个判断语句之后的逻辑
      if (type != XmlPullParser.START_TAG) {
        continue;
      }
      //获取 Name 标记
      final String name = parser.getName();
      //处理 REQUEST_FOCUS 的标记
      if (TAG_REQUEST_FOCUS.equals(name)) {
        parseRequestFocus(parser, parent);
      } else if (TAG_TAG.equals(name)) {
        //处理 tag 标记
        parseViewTag(parser, parent, attrs);
      } else if (TAG_INCLUDE.equals(name)) {
        //处理 include 标记
        if (parser.getDepth() == 0) {
          //include 节点如果是根节点就抛异常
          throw new InflateException("<include /> cannot be the root element");
        }
        parseInclude(parser, parent, attrs, inheritContext);
      } else if (TAG_MERGE.equals(name)) {
        //merge 节点必须是 xml 文件里的根节点(这里不该再出现 merge 节点)
        throw new InflateException("<merge /> must be the root element");
      } else {
        //其他自定义节点
        final View view = createViewFromTag(parent, name, attrs, inheritContext);
        final ViewGroup viewGroup = (ViewGroup) parent;
        final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
        rInflate(parser, view, attrs, true, true);
        viewGroup.addView(view, params);
      }
    }
    //parent 的所有子节点都 inflate 完毕的时候回 onFinishInflate 方法
    if (finishInflate) parent.onFinishInflate();
 }

可以看见,上面方法主要就是循环递归解析 xml 文件,解析结束回调 View 类的 onFinishInflate 方法,所以 View 类的 onFinishInflate 方法是一个空方法,如下:

/**
   * Finalize inflating a view from XML.  This is called as the last phase
   * of inflation, after all child views have been added.
   *
   * <p>Even if the subclass overrides onFinishInflate, they should always be
   * sure to call the super method, so that we get called.
   */
  protected void onFinishInflate() {
  }

可以看见,当我们自定义 View 时在构造函数 inflate 一个 xml 后可以实现 onFinishInflate 这个方法一些自定义的逻辑。

至此 LayoutInflater 的源码核心部分已经分析完毕。

4 从 LayoutInflater 与 setContentView 来说说应用布局文件的优化技巧

通过上面的源码分析可以发现,xml 文件解析实质是递归控件,解析属性的过程。所以说嵌套过深不仅效率低下还可能引起调运栈溢出。同时在解析那些 tag 时也有一些特殊处理,从源码看编写 xml 还是有很多要注意的地方的。所以说对于 Android 的 xml 来说是有一些优化技巧的(PS:布局优化可以通过 hierarchyviewer 来查看,通过 lint 也可以自动检查出来一些),如下:

尽量使用相对布局,减少不必要层级结构。不用解释吧?递归解析的原因。

使用 merge 属性。使用它可以有效的将某些符合条件的多余的层级优化掉。使用 merge 的场合主要有两处:自定义 View 中使用,父元素尽量是 FrameLayout,当然如果父元素是其他布局,而且不是太复杂的情况下也是可以使用的;Activity 中的整体布局,根元素需要是 FrameLayout。但是使用 merge 标签还是有一些限制的,具体是:merge 只能用在布局 XML 文件的根元素;使用 merge 来 inflate 一个布局时,必须指定一个 ViewGroup 作为其父元素,并且要设置 inflate 的 attachToRoot 参数为 true。(参照 inflate(int, ViewGroup, boolean) 方法);不能在 ViewStub 中使用 merge 标签;最直观的一个原因就是 ViewStub 的 inflate 方法中根本没有 attachToRoot 的设置。

使用 ViewStub。一个轻量级的页面,我们通常使用它来做预加载处理,来改善页面加载速度和提高流畅性,ViewStub 本身不会占用层级,它最终会被它指定的层级取代。ViewStub 也是有一些缺点,譬如:ViewStub 只能 Inflate 一次,之后 ViewStub 对象会被置为空。按句话说,某个被 ViewStub 指定的布局被 Inflate 后,就不能够再通过 ViewStub 来控制它了。所以它不适用 于需要按需显示隐藏的情况;ViewStub 只能用来 Inflate 一个布局文件,而不是某个具体的 View,当然也可以把 View 写在某个布局文件中。如果想操作一个具体的 view,还是使用 visibility 属性吧;VIewStub 中不能嵌套 merge 标签。

使用 include。这个标签是为了布局重用。

控件设置 widget 以后对于 layout_hORw-xxx 设置 0dp。减少系统运算次数。

如上就是一些 APP 布局文件基础的优化技巧。

5 总结

至此整个 Activity 的 setContentView 与 Android 的 LayoutInflater 相关原理都已经分析完毕。关于本篇中有些地方直接给出结论的知识点后面的文章中会做一说明。

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

  1. 创建一个 DecorView 的对象 mDecor,该 mDecor 对象将作为整个应用窗口的根视图。

  2. 依据 Feature 等 style theme 创建不同的窗口修饰布局文件,并且通过 findViewById 获取 Activity 布局文件该存放的地方(窗口修饰布局文件中 id 为 content 的 FrameLayout)。

  3. 将 Activity 的布局文件添加至 id 为 content 的 FrameLayout 内。

  4. 当 setContentView 设置显示 OK 以后会回调 Activity 的 onContentChanged 方法。Activity 的各种 View 的 findViewById() 方法等都可以放到该方法中,系统会帮忙回调。

如下就是整个 Activity 的分析简单关系图:

LayoutInflater 的使用中重点关注 inflate 方法的参数含义:

  • inflate(xmlId, null); 只创建 temp 的 View,然后直接返回 temp。

  • inflate(xmlId, parent); 创建 temp 的 View,然后执行 root.addView(temp, params);最后返回 root。

  • inflate(xmlId, parent, false); 创建 temp 的 View,然后执行 temp.setLayoutParams(params);然后再返回 temp。

  • inflate(xmlId, parent, true); 创建 temp 的 View,然后执行 root.addView(temp, params);最后返回 root。

  • inflate(xmlId, null, false); 只创建 temp 的 View,然后直接返回 temp。

  • inflate(xmlId, null, true); 只创建 temp 的 View,然后直接返回 temp。

当我们自定义 View 时在构造函数 inflate 一个 xml 后可以实现 onFinishInflate 这个方法一些自定义的逻辑。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文