学习笔记-inflate详解

layoutInflater.inflate()是经常使用的一个系统方法,使用它将XML加载为一个View对象,来具体扒一扒它的源码。

1. inflate

inflate一共有四个重载方法:

  1. inflate(int resource, ViewGroup root)
  2. inflate(XmlPullParser parser, ViewGroup root)
  3. inflate(int resource, ViewGroup root, boolean attachToRoot)
  4. inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

前三个方法最后都调用到了第四个重载方法,不同的就是第三个方法中有一个tryInflatePrecompiled()方法,后续读取资源ID,转换为XmlResourceParser之后,还是调用了第四个重载方法,所有的逻辑都是在第四个重载方法之中。

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

    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();
    }
}
复制代码

进入tryInflatePrecompiled()查看这个方法会有一个if (!mUseCompiledView)的退出判断。查这个变量的赋值的地方,默认的初始化方法是设置为false,给它赋值为true的地方只有一个,在setPrecompiledLayoutsEnabledForTesting(),这是一个只针对单元测试开放的方法。

所以tryInflatePrecompiled()方法一般不会走到。

class LayoutInflater {
    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;
        ...
    }

    /**
     * @hide for use by CTS tests
     */
    @TestApi
    public void setPrecompiledLayoutsEnabledForTesting(boolean enablePrecompiledLayouts) {
        initPrecompiledViews(enablePrecompiledLayouts);
    }
    
    
    View tryInflatePrecompiled(int resource, Resources res, ViewGroup root, boolean attachToRoot) {
        if (!mUseCompiledView) {
            return null;
        }
        ...
    }
}
复制代码

1.1. XmlResourceParser

前面通过调用res.getLayout(resource),将LayoutRes转换为了XmlResourceParser对象。

XmlResourceParser是XML资源的解析接口,简单来说就是一个XML资源解析器,用于解析xml资源,在后面会大面积用到,这里对它的方法做一些简单介绍。

  1. int getEventType()

    返回当前的事件类型,主要类型类型包括:

    • int START_DOCUMENT = 0:XML文档开始标志

    • int END_DOCUMENT = 1:XML文档结束标志

    • int START_TAG = 2:XML标签开始标志,比如<LinearLayout

    • int END_TAG = 3:xml 标签结束标志,比如</LinearLayout>或者/>

  2. String getAttributeValue(String namespace,String name)

    返回指定的属性名对应的属性值,如果没有使用命名空间,则第一个参数传入null。

  3. String getName()

    返回当前 TAG 的名字,比如<LinearLayout返回LinearLayout

  4. int next()

光标移动到下一个标签。

  1. int getDepth()

    获取当前标签的深度,根布局深度为1,根布局的直接子控件深度为2 ,以此类推,每多一层深度加1。

1.2. 第四个重载方法

这个方法中是inflate的主要逻辑。

在这个方法中,首先是对merge做了特殊处理,直接调用了rInflate()继续遍历。

merge并不是一个ViewGroup或者View,它相当于声明了一些视图等待被添加到另一个ViewGroup中,可以减少很多不必要的布局。(使用merge的时候可以使用tools:parentTag属性进行预览添加到对应ViewGroup的效果)

对于不是merge的情况,则调用CreateViewFromTag()创建根视图之后,根视图将作为root调用rInflateChildren()继续遍历。这几个方法后文都有介绍。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        。。。
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser); //属性集合
        View result = root;
        try {
            //校验START_TAG标签
            advanceToRootNode(parser);
            final String name = parser.getName();

            if (TAG_MERGE.equals(name)) {
                //使用merge的时候,inflate必须有root且attachToRoot==true
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                //创建根视图
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                //inflate的时候如果没有传root,给根view设置的属性会全部失效
                if (root != null) {
                    // 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)
                        //如果attachToRoot==false,直接将param设置给根视图
                        //如果attachToRoot==true,通过addView设置给根视图
                        temp.setLayoutParams(params);
                    }
                }

                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);

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

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                //如果root != null && attachToRoot,会直接将root传回去,其他情况下将根视图传回去
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        }
        return result;
    }
}
复制代码

接着从这个方法还可以看到平时使用inflate的几个特性:

  • merge并不是视图,对它设置的标签都是无效的
  • 如果根布局是merge,使用inflate加载时,root不能为空且attchToRoot不能是false
  • 如果root为空,根视图的所有标签都将失效,原因应该是必须根据root的类型去创建不同的layoutParam
  • 如果root != null && attachToRoot,根视图会直接添加到root,并且root将作为返回值,否则根视图作为返回值。

1.3. rInflate

接着往下看,rInflateChildren()调用的还是rInflate()方法,两个的区别也就是context获取不太同。

rInflate()中首先是一个大循环,看这个循环条件((type = parser.next()) != XmlPullParser.END_TAG ||parser.getDepth() > depth),只有两者同时不满足(即走到了END标签,并且深度不大于parent的深度)才会退出循环,一般来说,也就是走到parentEND_TAG标签退出循环。

循环内部,对requestFocustaginclude标签做了特殊的处理,其他情况下调用createViewFromTag()创建当前View。并且调用rInflateChildren()进行递归往下调用。如果不是ViewGroup,会在下一次递归中读取到当前viewEND_TAG标签,也就是进不去循环之中。

所以可以确定的是,每一层rInflate()调用的时候,创建出来的都只有parent的直接子View。最终创建出来的子view都会被添加到对应的parent

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
                            boolean finishInflate) throws XmlPullParserException, IOException {
    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;
    boolean pendingRequestFocus = false;

    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:parent获得焦点
            pendingRequestFocus = true;
            consumeChildElements(parser); //跳过到parent层级
        } else if (TAG_TAG.equals(name)) {
            //tag: 调用parent.setTag(key, value)设置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只能是根标签
            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(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

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

    if (finishInflate) {
        parent.onFinishInflate();
    }
}
复制代码

1.4. createViewFromTag

前面的方法中,多次通过调用createViewFromTag()创建了控件。

这个方法中,首先特殊处理了view标签,将class路径作为name读取出来。之后读取主题。

分为两步进行了尝试创建,首先是调用tryCreateView()进行创建。如果返回NULL,接着判断路径名小数点的数量,分别走不同的方法,回想一下,没有小数点的标签,也就是系统自带的各种view,在XML中不需要输入全路径而只需要名称即可。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                       boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        //小写view,可以直接传入class路径,将name重置掉
        name = attrs.getAttributeValue(null, "class");
    }

    //读取主题
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }

    try {
        View view = tryCreateView(parent, name, context, attrs);
        if (view == null) {
            ...
            try {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(context, parent, name, attrs);
                } else {
                    view = createView(context, name, null, attrs);
                }
            }
        }
        return view;
    }
    ...
}
复制代码

1.4.1. tryCreateView

接着看下如何通过tryCreateView()尝试创建的。

方法首先判断blink标签,是留下来的一个小彩蛋,Let's party like it's 1995!

后续,调用mFactorymFactory2mPrivateFactory将创建方法代理出去。

public final View tryCreateView(@Nullable View parent, @NonNull String name,
                                @NonNull Context context,
                                @NonNull AttributeSet attrs) {
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }

    return view;
}
复制代码

为了快速找到这几个工厂实现方法在哪里,直接打个断点找一下。

Snipaste_2021-07-19_15-06-24.png

可以看到,主要调用的mFactory2的实现AppCompatDelegateImplmPrivateFactory的实现Activity

mPrivateFactory的实现Activity的逻辑集中在FragmentActivity中,其中对fragment标签做特殊处理,并且可以自己通过Activity去代理拦截创建。这里不向里面探索,主要来看下mFactory2的实现AppCompatDelegateImpl

AppCompatDelegateImpl中层层传递,最后在AppCompatDelegateImpl.createView()中代理给了AppCompatViewInflater实现。

AppCompatViewInflater.createView()中可以看到,对标签为TextView的直接创建返回了AppCompatTextView

这可不就是AppCompatXXX之类的控件转换的地方!意外收获。也就搞清楚了,为什么XML中就自动转换成对应的AppCompatXXX,而自定义View的时候需要直接继承AppCompatXXX

要是在这里没有拦截,一般就返回了NULL,交由LayoutInflater创建。

//AppCompatDelegateImpl
public View createView(View parent, final String name, @NonNull Context context,
                       @NonNull AttributeSet attrs) {
    ...
    if (mAppCompatViewInflater == null) {
        mAppCompatViewInflater = new AppCompatViewInflater();
        ...
    }

    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
            true, /* Read read app:theme as a fallback at all times for legacy reasons */
            VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
    );
}

//AppCompatViewInflater
final View createView(View parent, final String name, @NonNull Context context,
                      @NonNull AttributeSet attrs, boolean inheritContext,
                      boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    ...
    View view = null;

    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
        case "TextView":
            view = createTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageView":
            view = createImageView(context, attrs);
            verifyNotNull(view, name);
            break;
        ...
    }

    return view;
}

//AppCompatViewInflater
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
    return new AppCompatTextView(context, attrs);
}
复制代码

1.4.2. createView

tryCreateView()如果没有成功创建,会继续往下走,根据有没有小数点进入不同的onCreateView()或者createView(),首先来看下onCreateView()是如何处理系统控件的。

1.4.2.1. onCreateView

LayoutInflater中,onCreateView()也是层层传递,最后直接调用了createView,只是将包名前缀android.view.作为perfix属性带上进入了createView(),看到这也就知道,之所以不需要写完整包名,是因为自动进行了补充。

但走到这,有一个大疑惑,系统组件并不止android.view这一个包名,查了一下,发现LayoutInflater有一个实现类PhoneLayoutInflater重写了onCreateView()方法,在里面分别尝试了android.widgetandroid.webkitandroid.app的包名进行创建,也就是说,平时使用的LayoutInflater其实是PhoneLayoutInflater对象。

//LayoutInflater
protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

public class PhoneLayoutInflater extends android.view.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;
                }
            } 
        }

        return super.onCreateView(name, attrs);
    }
}
复制代码

1.4.2.2. createView

createView()终于看到了创建逻辑,首先在缓存中查找有没有创建器。

如果没有找到,就尝试通过包名去加载CLASS对象并拿到它的加载器缓存起来,这里使用的就是刚才传入的prefix + nameprefix就是oncreateView()中添加的完整包名前缀。

之后就是调用newInstance()创建对象,传入的参数Object[],共有两位,第一位是Context,第二位是AttributeSet。对应的就是View(Context context, AttributeSet attrs)构造方法。这里也就对应了如果自定义View只是实现了Context的构造方法,代码中动态创建不会出错,但是不能在XML使用。

另一个点,就是这里反射并没有更改访问权限的检查,所以JAVA中需要给CLASS设置publicKotlin中默认是public=_=)。

public final View createView(NonNull Context viewContext, NonNull String name,
                             @Nullable String prefix, @Nullable AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    ...
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        sConstructorMap.remove(name);
    }
    Class<? extends View> clazz = null;

    try {
        if (constructor == null) {
            // Class not found in the cache, see if it's real, and try to add it
            clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                    mContext.getClassLoader()).asSubclass(View.class);
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            sConstructorMap.put(name, constructor);
        }
      
        Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = viewContext;
        Object[] args = mConstructorArgs;
        args[1] = attrs;
      
        try {
            final View view = constructor.newInstance(args);

            return view;
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    }
    ...
}
复制代码

1.5. parseInclude

前面看到过,对于include标签做了特殊处理。

逻辑和inflate基本一致,读取到了layoutid之后,先调用tryInflatePrecompiled(),之前介绍过这个方法只是为了测试用。

然后根据是不是merge标签,走不同的分支进入到rInflate(),进入创建流程。

private void parseInclude(XmlPullParser parser, Context context, View parent,
                          AttributeSet attrs) throws XmlPullParserException, IOException {
    int type;
    
    int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
    
    ...

    final View precompiled = tryInflatePrecompiled(layout, context.getResources(),
            (ViewGroup) parent, /*attachToRoot=*/true);
    if (precompiled == null) {
        final XmlResourceParser childParser = context.getResources().getLayout(layout);

        try {
            ...
            if (TAG_MERGE.equals(childName)) {
                
                rInflate(childParser, parent, context, childAttrs, false);
            } else {
                final View view = createViewFromTag(parent, childName,
                        context, childAttrs, hasThemeOverride);
                final ViewGroup group = (ViewGroup) parent;
                // Inflate all children.
                rInflateChildren(childParser, view, childAttrs, true);
                ...
                group.addView(view);
            }
        } finally {
            childParser.close();
        }
    }
}
复制代码

2. Inflate获取

  1. 通过系统服务获取

    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    复制代码
  2. 通过from获取

    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;
    }
    复制代码
  3. 通过activity获取

    activityPhoneWindow中获取,PhoneWindow中也是通过from获取。

    public LayoutInflater getLayoutInflater() {
        return getWindow().getLayoutInflater();
    }
    
    public class PhoneWindow extends Window implements MenuBuilder.Callback {
        private LayoutInflater mLayoutInflater;
    
        public PhoneWindow(Context context) {
            super(context);
            mLayoutInflater = LayoutInflater.from(context);
        }
    
        public LayoutInflater getLayoutInflater() {
            return mLayoutInflater;
        }
    }
    复制代码
  4. 通过View获取

    View留有inflate方法,通过from获取LayoutInflater

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

如果觉得这篇文章不错,请点个赞支持一下。

猜你喜欢

转载自juejin.im/post/6987658855882702862