从LayoutInflater源码解答CustomView的两个问题

1,627 阅读7分钟

问题1:为什么CustomView在xml中使用时,不可以像系统View一样类名引用,而必须是以完整包名引用,否则会出现ClassNotFoundException

AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #9 in com.example.myapplication:layout/activity_main: Error inflating class CustomView
AndroidRuntime: Caused by: java.lang.ClassNotFoundException: android.view.CustomView
       at java.lang.Class.classForName(Native Method)
       at java.lang.Class.forName(Class.java:454)
       at android.view.LayoutInflater.createView(LayoutInflater.java:825)
       at android.view.LayoutInflater.createView(LayoutInflater.java:786)
       ...

问题2: 为什么CustomView在xml中使用时,不可缺少constructor(Context, AttributeSet)的构造函数,否则会出现NoSuchMethodException

AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #9 in com.example.myapplication:layout/activity_main: Error inflating class com.example.myapplication.CustomView
AndroidRuntime: Caused by: java.lang.NoSuchMethodException: com.example.myapplication.CustomView.<init> [class android.content.Context, interface android.util.AttributeSet]
      at java.lang.Class.getConstructor0(Class.java:2332)
      at java.lang.Class.getConstructor(Class.java:1728)
      at android.view.LayoutInflater.createView(LayoutInflater.java:834)
      ...

一、LayoutInflater解析xml的过程

一切还要从LayoutInflater解析布局说起,当Activity中通过setContentView传入xml时,最终会进入AppCompatDelegateImpl中,就是在这里,LayoutInflater正式登场了。

@Override
 public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

LayoutInflater登场后,做了两场表演,一是构造自己的实例,二是通过inflate()创建View树。

1)LayoutInflater.from(mContext)构建实例。

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自身的构造函数创建实例,

protected LayoutInflater(LayoutInflater original, Context newContext) {
        mContext = newContext;
        mFactory = original.mFactory;
        mFactory2 = original.mFactory2;
        mPrivateFactory = original.mPrivateFactory;
        setFilter(original.mFilter);
        initPrecompiledViews();
    }

并且没有在外部设置过mFactorymFactory2mPrivateFactory

public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

这样可以得出一个结论,LayouterInflater实例中的mFactorymFactory2mPrivateFactory三个变量至始至终为null,这个结论先放这,后面会用到。

2)View树的解析工作通过inflater.inflate()发起,分为两个大的步骤,一是通过createViewFromTag()方法创建View,二是通过rInflateChildren()方法创建ViewGroup的子View。

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
           ...
            View result = root;
            try {
                ...
                // 第一步,根据节点标签创建View对象
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    
                 ...
                // 第二步,创建View对象的子View
                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.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (Exception e) {
                ...
            }
            return result;
        }
    }
  • 第一步:通过createViewFromTag()创建View对象。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {
        ...
        try {
            View view = tryCreateView(parent, name, context, attrs);
            if (view == null) {
               ...if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                ...
            }
            return view;
        } catch (Exception e) {
           ...
        } 
    }

这个方法里会先执行tryCreateView()方法,这里就会依赖mFactorymFactory2mPrivateFactory,根据前文的结论,这三个变量都为null,所以这个方法最终返回的View是null。

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

因此creatViewFromTag()方法里也没有真正创建View,而是进入到onCreateView()或者createView()中进行创建,不论哪个,最终都会执行到下面的方法中。

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

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
            if (constructor == null) {
                // 通过标签名反射获取View Class对象
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);
                ...
                // 获取View的构造器
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } 
            ...
            try {
                // 通过构造器反射创建View对象             
                final View view = constructor.newInstance(args);
                if (view instanceof ViewStub) {
                    // Use the same context when inflating ViewStub later.
                    final ViewStub viewStub = (ViewStub) view;
                    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                }
                return view;
            } finally {              
                mConstructorArgs[0] = lastContext;
            }
        } catch (Exception e) {
            ...
        } 
    }

这个方法就是真正创建View的实现,根据传入的标签名,反射创建出View对象【tip: 创建过程采用了缓存机制实现,缓存对象是要创建View的构造器,只有当View没有被创建过时,才会执行反射创建,并将View的构造器存到缓存表中,当下一次再需要创建相同的View对象时,就直接从缓存表中取出对应的构造器,创建对象,避免重复反射】。

  • 第二步:通过rInflateChildren()方法创建ViewGroup的子View。相关源码如下,
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)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } 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 {  
                //和创建父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();
        }
    }

会对创建出来的View进行遍历,如果存在子View,那么就通过递归调用,一层一层地将所有子View通过createViewFromTag()创建出来,创建的子View会被添加到对应的ViewGroup中,就是这样把一个完整的xml布局对应的View树给创建了出来。

二、为什么自定义View在xml中使用时,必须引用完整包名,而系统View只需要引入类名?

为了说明问题,这里自定义了一个CustomView,并设置了三个默认构造函数,

public class CustomView extends View {
    public CustomView(Context context) {
        super(context);
        init(context);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    private void init(Context context) {
        //...
    }
}

将CustomView和系统TextView放到同一个布局中,其中TextView只以类名引入,而CustomView以完整包名引入。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="44dp"
        android:background="#000000" />

    <com.example.myapplication.CustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ff0000" />

</LinearLayout>

这时运行发现页面正常显示无异常。

[问题来了] 现在将布局中的CustomView也改成类名引入,再运行,就遇到了文章开头的ClassNotFoundException

<CustomView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ff0000" />

再回头看下createViewFromTag()方法源码,

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
       ...
        try {
            View view = tryCreateView(parent, name, context, attrs);
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {//name字串中没有.
                        view = onCreateView(context, parent, name, attrs);
                    } else {//name字串中含有.
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
            return view;
        } catch (Exception e) {
            ...
        } 
    }

也就是看view标签name中是否含有“.”字符,如果有就执行createView()方法,没有则执行onCreateView()方法。

i) name中不含“.”onCreateView()执行时,调用了createView(name, attrs),该方法里又调用了createView(context, name, prefix, attrs),传入prefix=“android.view”

protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }
public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Context context = (Context) mConstructorArgs[0];
        if (context == null) {
            context = mContext;
        }
        return createView(context, name, prefix, attrs);
    }

ii) name中含“.”,直接调用了createView(context, name, prefix, attrs)方法,传入的prefix=null

[答案在此] 因此不管name中是否含有“.”,最终都会执行createView(context, name, prefix, attrs),只是传入的prefix不同而已。方法内部与prefix产生关联的代码是这句,

clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
            mContext.getClassLoader()).asSubclass(View.class);

这句话是根据View标签name反射创建View对象,反射创建时,需要完整的类名,否则会找不到类。

由于系统View都是在android.view包下的,createView()时name中不含“.”,因而会添加android.view的prefix,得到View的完整名称。因此在xml布局放置系统View标签时,不写完整包名也能正常解析。然而对于自定义View,如果xml不写完整的包名,自然也是会被加上android.view的prefix,而自定义View根本不在android.view包下,反射时自然就找不到类了。

三、为什么自定义View在xml中使用时,不可缺少constructor(Context, AttributeSet)?

[问题来了] 在xml布局中使用自定义View时,有时会出现NoSuchMethodException异常。由于解析过程实际是根据标签名反射创建View的过程,可以推断是反射过程抛了异常。于是查看LayouterInflater#onCreateView()反射创建的源码,

public final View createView(@NonNull Context viewContext, @NonNull String name,
       @Nullable String prefix, @Nullable AttributeSet attrs){
          ...
          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);
          ...
         final View view = constructor.newInstance(args);         
         return view;
  }

其中,mConstructorSignature为,

static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class};

[答案在此] 也就是反射调用类中参数表为(Context, AttributeSet)的构造函数,创建出对象。显然易见,导致反射报错的原因,是自定义View中缺少了参数表为(Context, AttributeSet)的构造函数。因此,自定义View时,如果要在xml布局中使用,一定要加上参数表为(Context, AttributeSet)的构造函数。