深入理解LayoutInflater

深入解析LayoutInflater的工作原理,包括获取方式、inflate方法使用及源码分析,理解布局文件如何转换为View实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LayoutInflater在我们的日常开发中非常常见,通过它可以将我们的布局文件加载成一个View实例,那么LayoutInflater是如何将一个布局文件加载成View实例的呢?

一、LayoutInflater的获取

LayoutInflater的获取有三种方式:

  1. LayoutInflater.from(context)。
  2. context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)。
  3. 在Activity种还可以通过getLayoutInflater()方法获取。

第一种方式和第二种本质上其实是同一种。只是系统给我们封装了一下方便我们调用。

    /**
     * Obtains the LayoutInflater from the given context.
     */
    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        //。。。
        return LayoutInflater;
    }

对于第三种Activity#getLayoutInflater()方法:

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

可以看到它其实调用了Window的getLayoutInflater方法,Window的唯一实现类是PhoneWindow,点进去看看。

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

enmmm直接返回了mLayoutInflater对象,那么mLayoutInflater在哪里初始化的呢?找找…

 public PhoneWindow(Context context) {
        super(context);
        mLayoutInflater = LayoutInflater.from(context);
    }

可以看到在PhoneWindow构造方法中初始化mLayoutInflater,原来还是通过LayoutInflater.from(context)获取的啊!所以不管通过哪种方式最终都是通过context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)获取的。

二、LayoutInflater的inflate方法

为了更好的理解源码,先来理解inflate方法中下面这几个入参的含义:

inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

这里写个demo做测试,demo的Activity根布局就是一个LinearLayout,其id为ll_content。然后在创建一个布局文件layout_inflate_test.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:background="@android:color/darker_gray"
    android:text="Hello World!">
</TextView>

然后在代码中通过LayoutInflater的inflate方法将layout_inflate_test.xml这个布局文件加载到id为ll_content的LinearLayout中。

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

        LinearLayout ll_content=findViewById(R.id.ll_content);

        LayoutInflater inflater= LayoutInflater.from(this);
        //方式1
        inflater.inflate(R.layout.layout_inflate_test,ll_content);
    }

在这里插入图片描述
一切正常,我们再换种方式加载。

        //方式2
        TextView textView= (TextView) inflater.inflate(R.layout.layout_inflate_test,ll_content,false);
        ll_content.addView(textView);

这里将第三个参数传一个false,此时inflate方法不会将加载后的布局add到ll_content中,而是返回了加载后得到的View实例。然后需要我们自己调用addView添加到ll_content中。
在这里插入图片描述
一切正常,如果我们调用inflate方法时不传第二个参数会如何?

        //方式3
        TextView textView= (TextView) inflater.inflate(R.layout.layout_inflate_test,null);
        ll_content.addView(textView);

在这里插入图片描述
当第二个参数传入null时。layout_inflate_test.xml中TextView的高好像没起作用。
下面具体看下inflate方法的源码,搞清楚究竟时怎么回事?

三、inflate方法源码分析

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

当调用inflate的两个参数的重载方法时,内部会调用三参的,并且如果第二个参数root != null,那么调用3参数的inflate方法时,第三个参数就是true。

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        //XmlResourceParser 用来解析XML
        final XmlResourceParser parser = res.getLayout(resource);
       //.....
        return inflate(parser, root, attachToRoot);
    }

这里获取到parser对象,通过它可以解析传入的布局文件,它使用的是Pull解析。然后又调用了inflate的重载,不过这时传入的第一个参数就是parser了。

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
           
            View result = root;

            try {
               //查找布局文件的根节点
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                //获取到布局文件的根节点的名称
                final String name = parser.getName();
               
                //处理根节点是merge标签的情况
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        //从异常可以看出,如果被加载布局的根节点是merge,那么必须满足root !=null && attachToRoot==true。
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //调用rInflate递归加载,注意这里传入的第二个参数是root。等会看
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                   //创建name标签对应的View。(现在是根标签)
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                       //重点:如果传入的root不为空,会通过root的generateLayoutParams为当前布局生成LayoutParams
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            //如果attachToRoot为false就会为生成的temp这个View设置tLayoutParams
                            temp.setLayoutParams(params);
                        }
                    }

                   //加载完根View后就会去递归加载它下面所有的孩子。
                    rInflateChildren(parser, temp, attrs, true);
                   
                   //如果root != null && attachToRoo==true就调用root.addView方法将temp这个View添加进去,并且传入生成的布局参数。
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
       
                   //如果root == null || attachToRoot==false  就将temp赋值给result然后返回。
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            return result;
        }
    }

通过这段源码我们知道:

  • 如果root不为空,那么会借助root的generateLayoutParams来为生成的view获取其LayoutParams,包含了layout_width和layout_height。
  • 如果root不为空 并且attachToRoot是false,会通过root.generateLayoutParams(attrs)为这个view生成LayoutParams,并且将其设置到view中。最后返回这个view而不是root。
  • 如果root不为空 并且attachToRoot是true,会通过root.generateLayoutParams(attrs)为这个view生成LayoutParams,并且会调用root的addView(temp, params)方法将temp加入到root中,同时添加的还有params。
  • 如果root为空,不会为生成的View生成LayoutParams,最后返回的是这个view而不是root。

到这里应该能理解上面的demo中TextView高度未生效的原因了吧。因为root为空就没有为这个view设置LayoutParams,因此我们自己调用addView时,采用的是默认的LayoutParams。
在这里插入图片描述
ok,继续回归源码,注意到这行:

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

根据节点的名称创建对应的view,比如我们传入的标签名称是TextView那么就会根据这个名称生成一个TextView实例。

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        //...如果标签名称是blink 就生成一个BlinkLayout布局,blink 标签很少使用,它可以达到一闪一闪的效果,不重点看了。
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        try {
            View view;
            if (mFactory2 != null) {
                //如果mFactory2 不为空就调用它的onCreateView来创建View。
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
               //如果mFactory不为空就调用它的onCreateView来创建View。
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

            if (view == null && mPrivateFactory != null) {
                //如果上面没有创建View 并且mPrivateFactory不为空就调用它的onCreateView来创建View。
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            if (view == null) {
               //如果经过上面的步骤view没有创建,就调用它自己的方法来创建view
                try {
                    if (-1 == name.indexOf('.')) {
                        //如果标签名中不包含.比如TextView,ImageView就调用onCreateView方法
                        view = onCreateView(parent, name, attrs);
                    } else {
                        //如果包含.比如support库中的View或者我们自定义的View都是全路径xx.xxx.xxx。就调用createView方法。
                        view = createView(name, null, attrs);
                    }
                } 
            }

            return view;
        } 
    }

从createViewFromTag源码可以看出,它会先尝试通过mFactory2、mFactory的onCreateView方法来创建View实例。从这里我们可以看出这个Factory其实是系统给我们预留的钩子,我们可以调用setFactory、setFactory2的方法来设置mFactory和mFactory2拦截LayoutInflate自带的createView方式。常见的换肤,统一字体大小等都可以通过这种手段实现。
LayoutInflate在创建View分为两种情况调用分别调用了onCreateView、createView。

  1. 如果这个标签名不含.这说明这个View是系统自带的,比如TextView、ImageView等,就调用onCreateView方法
  2. 如果是support中的View或者我们自定义的View都是全路径的,包含.的。就调用createView方法

LayoutInflater是一个抽象类,它的实现类是PhoneLayoutInflater,实现类重写了onCreateView方法看看它做了啥?

public class PhoneLayoutInflater extends LayoutInflater {
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };
    //。。。
    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }
        return super.onCreateView(name, attrs);
    }
    //。。。
}

遍历sClassPrefixList,然后还是调用了LayoutInflate的createView方法,并且将sClassPrefixList 中保存的一前缀传进去了。如果遍历完都没有创建View实例,就调用super.onCreateView

protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
          //哈哈,原来是前面的前缀都不行,就用android.view再试试
        return createView(name, "android.view.", attrs);
    }

最终都会走到createView方法,点进去看看:

 public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
           //1、先尝试从缓存中获取
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            //2、缓存中获取不到
            if (constructor == null) {
                //通过前缀和标签名拼接获取全路径,然后获取到该类的class
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                  //。。。
                //通过class获取到构造
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                //保存到缓存中
                sConstructorMap.put(name, constructor);
            } else {
              //。。。
            }
            //。。。
            //通过反射创建View实例
            final View view = constructor.newInstance(args);
            //。。。
            return view;

        } 
    }

通过分析源码知道了createViewFromTag方法是如何通过节点的name实例化出一个具体的View,当然还有一些细节没说,不过不难自己看看就能明白。分析完createViewFromTag再接着分析inflate中的另一个方法,再大致回顾下inflate方法。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
           
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
  
                final String name = parser.getName();
                
                if (TAG_MERGE.equals(name)) {
                   //。。。
                } else {
                    // 根据布局根节点的name创建View
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                    //去加载根节点下面的孩子,注意这里传入了temp,是我们当前节点代表的view。
                    rInflateChildren(parser, temp, attrs, true);

                    if (root != null && attachToRoot) {
                        //将布局根节点代表的View添加到传进来的root中
                        root.addView(temp, params);
                    }

                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            return result;
        }
    }

rInflateChildren又去调用了rInflate方法,这个r应该就是递归的意思吧。

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

重点分析下rInflate方法,这个方法用来解析parent下的子节点。

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        //。。。。。
        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)) {
                //处理节点名称是requestFocus的情况(PS:没用过,不看了)
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                //处理节点是tag的情况
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
               //处理节点是include的情况
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                //如果是merge标签就抛异常,可以看出merge标签只能作为布局文件的根节点。
                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);
                //继续递归加载当前节点下的子View
                rInflateChildren(parser, view, attrs, true);
                加载完成后将当前节点添加到parent中。(PS:此时当前节点下的子View已经add到当前节点了)
                viewGroup.addView(view, params);
            }
        }

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if (finishInflaiewte) {
            //如果parent下所有的子View都递归加载添加完成就调用onFinishInflate方法。(又看到一个熟悉的方法)
            parent.onFinishInflate();
        }
    }

可以看到整个过程其实就是一个递归的过程,调用rInflate加载入参有个parent,加载的过程中parent下的所以子节点都会被创建,又去调用rInflateChildren去加载子节点下的子节点,这个过程会被递归,这些节点都会被add到自己的parent中。最终返回一个包含了所有解析好的子View的布局根View,这个根View的数据结构已经是一个view tree了。

整体流程看完了,回过头看下如果根节点是merge的情况。

在最开始加载布局文件的根节点时如果根节点是一个merge标签:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
                // 查找布局文件的根节点
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                //获取节点的名称
                final String name = parser.getName();

           
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //可以看到这里调用了rInflate方法 并且parent传入的参数的root
                    rInflate(parser, root, inflaterContext, attrs, false);
                } 
            return result;
        }

如果是merge标签,还是会调用rInflate去递归加载merge标签下所有的子View,不过最终是被add到root的,这样就知道了merge标签是如何减少布局嵌套了吧,因为在merge标签根本没被创建view它的直接子节点都被直接add到root了。并且此时rInflate函数最后一个参数是false,这意味着直接子view都添加到root后,不会调用root的onFinishInflate。

       if (finishInflate) {
            parent.onFinishInflate();
        }

千万不要把root和根节点搞混淆了,这里的root是调用LayoutInflater的inflate方法传入的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值