Android inflate解析

Android inflate解析

对于inflate,我相信大家都不陌生,它的作用是将一个layout.xml布局文件变为一个View对象。尤其在ListView、GridView、RecyclerView的Adapter还有组合自定义控件中,我们都会使用inflate()方法去加载一个布局,作为每个Item的布局。这篇博客就来分析一下Android中inflate是怎样将xml文件变为View的。

使用inflate,一般是调用两个类中的方法:

  • 使用View中的静态方法View.inflate()
    public static View inflate(Context context, @LayoutRes int resource, ViewGroup root)

  • 使用LayoutInflater中的inflate()方法,在LayoutInflater类中有几个重载方法

      public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
      public View inflate(XmlPullParser parser, @Nullable ViewGroup root)
      public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
      public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
    

inflate()方法内部调用过程

  • View#inflate()

      public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
          LayoutInflater factory = LayoutInflater.from(context);
          return factory.inflate(resource, root);
      }
    

    View.inflate() 调用了LayoutInflater中的inflate()方法,所以上面的两个类中的方法实际是一样的。下面看一下LayoutInflater的调用过程。

  • LayoutInflater#inflate()

      // 内部调用 inflate(resource, root, root != null)
      public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
          return inflate(resource, root, root != null);
      }
    
      // 内部调用 inflate(parser, root, attachToRoot)
      public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
          final Resources res = getContext().getResources();
          final XmlResourceParser parser = res.getLayout(resource);
          try {
              // 调用方法
              return inflate(parser, root, attachToRoot);
          } finally {
              parser.close();
          }
      }
    
      // 内部调用 inflate(resource, root, root != null)
      public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
          return inflate(parser, root, root != null);
      }
    
      // 最终调用方法
      public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot){
          // 解析xml文件,返回View对象
      }
    

通过上面的调用过程可以看到,不管是View#inflate()方法还是LayoutInflater#inflate(),最终都是调用了同一个方法,那么我们就只需要接着看这个方法的解析过程就可以了。

获取 LayoutInflater 对象

在说 inflate() 的解析过程之前,我们先来看一下外部怎么调用LayoutInflater#inflate()方法,这个方法不是静态的。不能通过类直接调用,我们需要创建 LayoutInflater对象才行,但是查看源码我们发现 LayoutInflater 类是抽象的 public abstract class LayoutInflater,并且构造方法也是protected的,是不能被实例化的,那么我们看一下它的子类,发现只有一个子类,是属于AsyncLayoutInflater的私有静态内部类: private static class BasicInflater extends LayoutInflater,那么这肯定也是不能被实例化的,那么我们该怎样获取 LayoutInflater 对象了?可以通过以下几个方式。

// 1. 通过 LayoutInflater 的静态方法 from
public static LayoutInflater from(Context context)

// 2. 通过系统服务方式获取
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)

// 3. 在Activity中,直接通过方法获取
public LayoutInflater getLayoutInflater()

Activity 调用 getLayoutInflater() 方法追踪

Activity#getLayoutInflater() 方法:

    @NonNull
    public LayoutInflater getLayoutInflater() {
        return getWindow().getLayoutInflater();
    }

Activity中调用getLayoutInflater()方法,调用的是继承至WindowPhoneWindow中的getLayoutInflater()方法,在PhoneWindowLayoutInflater对象的初始化在PhoneWindow的构造方法中,调用的是LayoutInflate.form(context)方法:

// PhoneWindow.java
public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
    mRenderShadowsInCompositor = Settings.Global.getInt(context.getContentResolver(),
            DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR, 1) != 0;
}

@Override
public LayoutInflater getLayoutInflater() {
    return mLayoutInflater;
}

在构造方法中也是也是通过 LayoutInflater 的静态方法 from() 获取:

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对象最终都是通过getSystemService()方法获取的。

LayoutInflater#inflate(XmlPullParser, ViewGroup, boolean) 源码分析

// parser:XML文档解析器对象,通过pull解析方式解析xml文档
// Resources类中的XmlResourceParser getLayout(@LayoutRes int id)方法获取
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
        // 初始化操作
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        // 返回结果,默认返回root对象,下面的代码会对result进行操作
        View result = root;
 
        try {
            // 调用 advanceToRootNode(parser) 方法,直接将方法中的代码移动到这里来,具体代码如下
            // ================= advanceToRootNode(parser) ================= //
            // 将给定的解析器推进到第一个 START_TAG。如果没有找到开始标签,则抛出 InflateException
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }
            // ================= advanceToRootNode(parser) ================= //

            // 获取第一个节点名称
            final String name = parser.getName();
            if (TAG_MERGE.equals(name)) {
                // 如果节点是merge并且root为空或者attachToRoot为false,抛出异常
                // (因为merge标签能够将该标签中的所有控件直接连在上一级布局上面,
                // 从而减少布局层级,假如一个线性布局替换为merge标签,那么原线性布局下的多个控件将直接连在上一层结构上)
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
 
                // 实例化 root 的子视图,并且将实例化后的View添加到root中(root.addView()方法)。
                // 然后调用 root的onFinishInflate()
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // 通过createViewFromTag()方法用于根据节点名来创建View对象,这里是创建根视图。
                // 在方法内部调用了createView()方法,createView()方法中通过反射创建View对象并返回。
                // 这里的 name 就是第一个节点名称,也就是xml布局的根节点,temp 就表示根布局的View
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
            
                ViewGroup.LayoutParams params = null;
 
                if (root != null) {
                    // root不为null时,获取root中的布局参数
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // root不为null并且attachToRoot为false时,将root的布局参数设置给temp
                        temp.setLayoutParams(params);
                    }
                }
                // 调用rInflateChildren()方法递归创建每一个孩子视图,rInflateChildren()方法内部会调用rInflate()方法。
                // 在SDK版本小于23当中就是直接调用rInflate()方法
                rInflateChildren(parser, temp, attrs, true);
 
                // 如果root不为null 并且 attachToRoot为true时,就把temp加到root上,相当于给temp增加一个父节点
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
 
                // 如果root为null 或者attachToRoot为false,那么就将temp赋值给result作为结果返回
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }
        }
        // 返回 result
        return result;
    }
}

代码中已经有详细注释了,接着看 rInflateChildren()rInflate() 的源码

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
    // 调用 rInflate() 方法
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
 
void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    final int depth = parser.getDepth();
    int type;
 
    // 遍历所有节点(不是结束标签或文档结束就继续遍历)
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
 
        // 先找到开始标签
        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        // 获取开始标签名称
        final String name = parser.getName();
        // 判断是否一些特定类型的标签,单独处理
        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 调用createViewFromTag()方法创建View对象
            final View view = createViewFromTag(parent, name, context, attrs);
            // 父节点
            final ViewGroup viewGroup = (ViewGroup) parent;
            // 获取布局参数
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            // 递归调用rInflateChildren()方法,继续往下层inflate
            rInflateChildren(parser, view, attrs, true);
            // 将通过createViewFromTag()方法创建View对象添加到父节点中
            viewGroup.addView(view, params);
        }
    }
 
    // 判断是否完成inflate,如果完成了,调用onFinishInflate()方法,在自定义控件时可以重写这个方法获取相关参数
    if (finishInflate) {
        parent.onFinishInflate();
    }
}

这样,就把整个布局文件都解析完成,形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()过程全部结束。

inflate过程总结

inflate()的总结,主要是root参数和attachToRoot参数设置不同值的总结:

  1. 如果rootnullattachToRoot不管是true还是false,返回的result都是tempattachToRoot没有意义(都是走result = temp语句);
  2. 如果root不为nullattachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root(会走root.addView(temp, params)这条语句,将temp增加到root中);
  3. 如果root不为nullattachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效(会走temp.setLayoutParams(params)这条语句,将最外层的属性设置给temp);
  4. 在不设置attachToRoot参数的情况下,attachToRoot = (root != null); rootnullattachToRootalseroot不为nullattachToRoottrue

其实这个总结并不需要记住,通过查看inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)这个方法的源码就很容易得到。

扩展:Activity#setContentView(resId) 方法过程

// Activity#setContentView(@LayoutRes int layoutResID)
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
}

调用 WindowsetContentView(int layoutResID) 方法,Window就是PhoneWindow

// PhoneWindow#setContentView(int layoutResID)
@Override
public void setContentView(int layoutResID) {
    // 省略其他无关代码,
    // mLayoutInflater 就是 LayoutInflater,在构造方法创建
    // mContentParent 就是 FrameLayout
    mLayoutInflater.inflate(layoutResID, mContentParent);
}

mContentParentFrameLayout 的原因,可以在 《Activity 的组成》中知道。

最终调用了 LayoutInflater#inflate(XmlPullParser parser, @Nullable ViewGroup root) 方法,然后将我们的实际布局变为 View 对象,最后添加到表示页面内容的 FrameLayout 中。

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

推荐阅读更多精彩内容