Android Q LayoutInflater布局生成View源码详解

LayoutInflater是什么?


LayoutInflater是Android系统的一个服务,我们可以通过它,把布局文件动态生成View,实现View视图的动态添加。

LayoutInflater在Android日常开发工作中经常使用到,并且我们经常调用的Activity的setContentView方法,它的内部实现就用到了LayoutInflater。

LayoutInflater对象的获取方式

我们可以通过多种方式来调用LayoutInflater服务。

方法一:LayoutInflater.from(context)

我们可以通过LayoutInflater.from(context)方法来返回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;
    }
逻辑解析:
  1. 这里可以看到,LayoutInflater确实是一个系统服务,服务名称是Context.LAYOUT_INFLATER_SERVICE,可以通过context.getSystemService来获得。
  2. 如果获取失败,则抛出错误。

方法二:通过context.getSystemService方法来获取

其实方法1只是方法2的简单封装而已,这里我们直接通过getSystemService来获取:

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

方法三:在Activity中,调用getLayoutInflater方法

这个方法最简单,我们可以直接在Activity中调用getLayoutInflater方法来获取。

Activity的getLayoutInflater方法源码:

    public LayoutInflater getLayoutInflater() {
        return getWindow().getLayoutInflater();
    }
逻辑解析:
  1. 这里直接返回的是getWindow()的getLayoutInflater()方法。
  2. getWindow()返回的是Activity所对应的PhoneWindow对象。
  3. PhoneWindow对象,在初始化时,通过LayoutInflater.from(context)获取了LayoutInflater对象。

关于Activity显示以及内部视图的创建等逻辑,我们会在今后的文章中进行源码分析。

LayoutInflater的使用

我们获取LayoutInflater实例之后,通过它的inflate方法来使布局生效。

layoutInflater.inflate(R.layout.activity_main, viewGroup);

示例中,使用layoutInflater.inflate将布局R.layout.activity_main作为子视图添加到viewGroup视图容器中。

接下来我们来分析inflate方法的源码实现。

LayoutInflater布局解析分析


LayoutInflater的inflate方法负责解析布局并生成View对象,我们接下来分析它的源码实现。

来看inflate方法:

frameworks/base/core/java/android/view/LayoutInflater.java

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

    
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }

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

        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
逻辑解析:
  1. 可以看到inflate方法有多个方法重载。
  2. 首先尝试从预编译缓存中获取View对象,如果成功,则直接返回,这里的预编译是什么,稍后我们介绍。
  3. 如果预编译中获取失败,则调用res的getLayout方法获取XML的资源解析器,这里用的是PULL解析器(前一章中有介绍)。
  4. 最后调用inflate方法进行解析。

tryInflatePrecompiled方法(预编译选项)

tryInflatePrecompiled是Android 10(Android Q)中新增的方法,用来根据布局文件的xml预编译生成dex,然后通过反射来生成对应的View,从而减少XmlPullParser解析Xml的时间。它是一个编译优化选项,我们下面来分析。

    private @Nullable
    View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
        boolean attachToRoot) {
        //如果mUseCompiledView是false,则表示预编译优化开个没有打开,直接返回
        if (!mUseCompiledView) {
            return null;
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");

        // 获得布局资源对应的packageName和layout名称
        String pkg = res.getResourcePackageName(resource);
        String layout = res.getResourceEntryName(resource);

        try {
            //获得 "包名" + ".CompiledView"的类
            Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
            //获得以layout名称命名的方法
            Method inflater = clazz.getMethod(layout, Context.class, int.class);
            //执行该方法,返回View对象。
            View view = (View) inflater.invoke(null, mContext, resource);
            //最后进行View的布局设置并根据条件判断是否添加到父视图中
            if (view != null && root != null) {
                // We were able to use the precompiled inflater, but now we need to do some work to
                // attach the view to the root correctly.
                XmlResourceParser parser = res.getLayout(resource);
                try {
                    AttributeSet attrs = Xml.asAttributeSet(parser);
                    advanceToRootNode(parser);
                    ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);

                    if (attachToRoot) {
                        root.addView(view, params);
                    } else {
                        view.setLayoutParams(params);
                    }
                } finally {
                    parser.close();
                }
            }

            return view;
        } catch (Throwable e) {
            if (DEBUG) {
                Log.e(TAG, "Failed to use precompiled view", e);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        return null;
    }
逻辑解析:
  1. 首先根据mUseCompiledView判断预编译是否是开启状态,如果mUseCompiledView是false,则表示预编译优化开个没有打开,直接返回。
  2. 获得布局资源对应的packageName和layout名称。
  3. 预加载编译资源,存储在以"包名" + ".CompiledView"的类中,并且布局资源对应了一个以layout名为名称的方法。
  4. 使用反射调用,执行该方法,就会返回一个资源布局对应的View视图对象。
  5. 最后进行View的布局设置并根据条件判断是否添加到父视图中。

预编译选项开关

我们来看预编译选项的开关,mUseCompiledView变量是何时设置的。

    private boolean mUseCompiledView;
    
    private void initPrecompiledViews() {
        // Precompiled layouts are not supported in this release.
        boolean enabled = false;
        initPrecompiledViews(enabled);
    }

    private void initPrecompiledViews(boolean enablePrecompiledViews) {
        mUseCompiledView = enablePrecompiledViews;

        if (!mUseCompiledView) {
            mPrecompiledClassLoader = null;
            return;
        }
    」

它是在initPrecompiledViews方法中进行设置的,其中无参的默认为false,表示不开启。

我们来看initPrecompiledViews调用的地方:

    protected LayoutInflater(Context context) {
        mContext = context;
        initPrecompiledViews();
    }
    /**
     * @hide for use by CTS tests
     */
    @TestApi
    public void setPrecompiledLayoutsEnabledForTesting(boolean enablePrecompiledLayouts) {
        initPrecompiledViews(enablePrecompiledLayouts);
    }

可以看到,系统只调用了initPrecompiledViews()无参的方法,表示默认关闭预编译优化。带参数的initPrecompiledViews方法只在内部测试时使用。

inflate方法

我们继续来看inflate方法:

    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;
            View result = root;

            try {
                //将给定的解析器推进到第一个开始标记
                advanceToRootNode(parser);
                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }
                //如果根标签是merge标签
                if (TAG_MERGE.equals(name)) {
                    //如果根节点是merge,并且root是null或者attachToRoot == false,则抛出异常。
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //遍历布局并生成View
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // 创建根标签的View对象
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    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
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    //遍历布局文件,生成所有子View
                    rInflateChildren(parser, temp, attrs, 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.addView(temp, params);
                    }

                    //如果父容器root是空,或者attachToRoot==false,则直接返回布局文件的根View。
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(
                        getParserStateDescription(inflaterContext, attrs)
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

通过分析我们可以看到,该方法实现了布局文件生成View的过程。createViewFromTag()方法实现了布局文件根View的创建,rInflateChildren(parser, temp, attrs, true)会调用rInflate()方法,递归实例化子View,过程中也会使用createViewFromTag()方法创建具体的子View。

rInflate和rInflateChildren方法都是通过递归解析xml中的布局,创建View,并添加到parent中,逻辑比较简单我们就不做具体分析了。

参数含义及注意事项

我们从以上分析中可以总结出该方法及参数使用的几个特点:

  • 参数root为默认父视图,如果为null,表示布局文件生成的View不会添加到默认视图中。attachToRoot属性将没有意义。
  • 参数root如果不为null,参数attachToRoot表示是否添加到父视图中
    • 当 attachToRoot为true时,则会把布局View添加到root中,作为root的子视图。
    • 当 attachToRoot为false时,不会把布局View添加到默认父视图root中,但是会把layout参数设置给布局View,当布局View被添加到父view当中时,这些layout属性会自动生效。
  • 当布局文件根节点是merge标签时,root必须不为null,并且attachToRoot必须为true,否则就会抛出InflateException异常。
  • attachToRoot默认值,当root != null时为true,root == null时为false。

总结


  1. LayoutInflater是Android系统的一个服务,我们可以通过它,把布局文件动态生成View,实现View视图的动态添加。
  2. Activity的setContentView方法,它的内部实现就用到了LayoutInflater。
  3. 可以通过多种方式来调用LayoutInflater:LayoutInflater.from(context)、context.getSystemService方法、在Activity中,调用getLayoutInflater方法。其实本质上都是获取LayoutInflater系统服务。
  4. LayoutInflater的inflate方法负责解析布局并生成View对象。首先尝试从预编译缓存中获取View对象,如果成功,则直接返回;如果预编译中获取失败,则会使用PULL解析器进行解析。
  5. 预编译优化是Android Q的新增逻辑,默认是关闭的。
  6. rInflate和rInflateChildren方法都是通过递归解析xml中的布局,创建View,并添加到parent中。
LayoutInflater生成View的参数含义如下:
  • 参数root为默认父视图,如果为null,表示布局文件生成的View不会添加到默认视图中。attachToRoot属性将没有意义。
  • 参数root如果不为null,参数attachToRoot表示是否添加到父视图中
    • 当 attachToRoot为true时,则会把布局View添加到root中,作为root的子视图。
    • 当 attachToRoot为false时,不会把布局View添加到默认父视图root中,但是会把layout参数设置给布局View,当布局View被添加到父view当中时,这些layout属性会自动生效。
  • 当布局文件根节点是merge标签时,root必须不为null,并且attachToRoot必须为true,否则就会抛出InflateException异常。
  • attachToRoot默认值,当root != null时为true,root == null时为false。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章