显示图形系统分析之LayoutInflater的inflate函数流程分析

236 阅读5分钟

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。

我们在分析Activity的setContentView流程的时候,我们知道,解析layout是通过如下的代码来解析的

LayoutInflater.from(context).inflate(layoutResID, mContentParent);

今天,我们就来分析一下LayoutInflater对象的inflate函数流程

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

可以看到,此处会调用Context对象的getSystemService函数,参数为Context.LAYOUT_INFLATER_SERVICE,getSystemService函数流程分析中已经对这个函数进行分析过,会调用SystemServiceRegsitry的getService函数,而这个参数的注册如下

registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
        new CachedServiceFetcher<LayoutInflater>() {
    @Override
    public LayoutInflater createService(ContextImpl ctx) {
        return new PhoneLayoutInflater(ctx.getOuterContext());
    }});

亦即,最终会返回一个PhoneLayoutInflater对象

classDiagram
LayoutInflater <|-- PhoneLayoutInflater
class LayoutInflater {
    <<abstract>>
    +from(Context)
    +cloneInContext(Context)*
    +inflate(@LayoutRes int, @Nullable ViewGroup)
    +inflate(XmlPullParser, @Nullable ViewGroup)
    +inflate(@LayoutRes int, @Nullable ViewGroup, boolean)
    -tryInflatePrecompiled(@LayoutRes int, Resources, @Nullable ViewGroup, boolean)
    +inflate(XmlPullParser, @Nullable ViewGroup, boolean)
    #rInflate(XmlPullParser, View, Context, AttributeSet, boolean)
}
class PhoneLayoutInflater {
    #onCreateView(String, AttributeSet)
    +cloneInContext(Context)
}

由上述类图可知,PhoneLayoutInflater是继承自LayoutInflater的,且LayoutInflater是一个抽象类

PhoneLayoutInflater类的inflate函数调用

从上述的类图可以看到,PhoneLayoutInflater类没有重写inflate函数,因此最终调用的是父类LayoutInflater的inflate函数

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

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    // ...... 日志打印代码省略
    // 1. 尝试预加载layout,若成功,直接返回对应的View
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    // 2. 获取layout的xml资源文件解析器
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

如上备注可知,此处先尝试预加载layout配置文件,若不成功则加载layout的资源文件解析器来解析layout,因此

尝试预加载layout

先看其代码

private @Nullable
View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
    boolean attachToRoot) {
    // 此处的mUseCompiledView的值是什么?
    // 追踪这个参数可知,在LayoutInflater对象的构造函数中,调用的initPrecompiledViews的无参函数,
    // 会将这个参数的值设置为false, 
    // 也就是说,当我们使用正常的流程LayoutInflater.from(Context)来初始化PhoneLayoutInflater的时候,
    // 这个参数的值一直为false,从而直接返回一个null的值
    // 那么这个参数是在什么情况下为true呢?
    // 查看代码可知,只有当setPrecompiledLayoutsEnabledForTesting函数被调用且参数设置为true的情况下,该参数才会为true
    // 而setPrecompiledLayoutsEnabledForTesting函数只有CTS测试验证代码才能调用
    if (!mUseCompiledView) {
        return null;
    }

    // ...... trace

    // ...... CTS测试使用代码,此处不做分析,暂时省略
}

如上代码所知,上述函数只有在CTS测试的情况下,才可能会返回一个非null的View对象,因此此处我们不做分析

资源文件解析器解析layout

final Resources res = getContext().getResources();
// ......
// 获取layout资源解析器
XmlResourceParser parser = res.getLayout(resource);
try {
    // 解析对应的layout
    return inflate(parser, root, attachToRoot);
} finally {
    parser.close();
}

可以看到,此处先获取一个layout资源解析器,然后在调用重载inflate函数来解析对应的View对象

获取layout资源解析器

Resources.java
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
    // 调用loadXmlResourceParser函数加载一个资源解析器
    return loadXmlResourceParser(id, "layout");
}

XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
        throws NotFoundException {
    final TypedValue value = obtainTempTypedValue();
    try {
        final ResourcesImpl impl = mResourcesImpl;
        // 确认是否能够找到对应的layout,若未找到,则抛出NotFoundException异常
        impl.getValue(id, value, true);
        if (value.type == TypedValue.TYPE_STRING) {
            // 最终调用loadXmlResourceParser函数
            return loadXmlResourceParser(value.string.toString(), id, value.assetCookie, type);
        }
        throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                + " type #0x" + Integer.toHexString(value.type) + " is not valid");
    } finally {
        releaseTempTypedValue(value);
    }
}

XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws NotFoundException {
    // 分析这个函数,会返回一个XmlBlock.Parser对象
    return mResourcesImpl.loadXmlResourceParser(file, id, assetCookie, type);
}

如此,最终会返回一个XmlBlock.Parser对象

inflate重载函数调用

inflate(parser, root, attachToRoot);

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        // ...... trace

        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;

        try {
            // 前往xml文件的起始节点
            advanceToRootNode(parser);
            final String name = parser.getName();

            // ...... debug code
            // 开始解析xml文件
            
            // 如果是merge节点,则进入if语句
            if (TAG_MERGE.equals(name)) {
                // 此处的if判断也即表明,在LayoutInflater的inflate函数中传入的layout中只包含merge节点对应的节点
                // 则不允许传入的第二个参数root为null
                // 否则会抛出异常
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                // 当为merge节点的时候,此时直接进入解析,并最终会调用到createViewFromTag来初始化View对象
                // 通过rInflateChildren函数解析上述View对象的子View,最后递归解析子View的子View,
                // 并最后添加到上述的View对象中
                // 最终会添加到root对象中
                // 这边使用了递归的思想解析完成后添加到最终的root中
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                // 根据xml中的节点信息创建一个View对象
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    // ...... debug code
                    // 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);
                    }
                }

                // ...... debug code

                // Inflate all children under temp against its context.
                // 以刚刚创建的temp对象为父Container,继续解析对应的子View
                // 此函数最终会调用到
                // 此处会递归调用,有兴趣可以分析一下流程
                rInflateChildren(parser, temp, attrs, true);

                // ...... debug code

                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                // 如果root不为null,则表明root是当前解析的layout的父框架,则将解析出来的View添加到该父框架中
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                // 若root为null,表明当前解析的layout即为当前的最外层Container,因此直接返回创建的View对象即可
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        }
        // ...... catch and finally code, delete
        return result;
    }
}

也就是说,在xml资源解析器解析layout对应的xml的时候,有两种情况

  1. 当根节点为merge时,则LayoutInflater的inflate函数传入的第二个参数root不能为null,否则会抛出异常,然后资源解析器解析layout文件的时候,根据其对应的节点解析对应的View对象,并添加到对应的父View中
  2. 当根节点为非merge的时候,直接根据对应的节点名生成对应的View,并递归添加到对应的父View中

如此,LayoutInflater类的inflate函数简要流程分析结束