在日常Android开发中,最经常使用的RecyclerView控件是大家都绕不开的,而编写其Adapter时更离不开LayoutInflater的调用。当然,如果你做这一行有些时日了,相信你对其使用一定是炉火纯青了。即使如此,我觉得LayoutInflater仍旧有值得分析的地方,相信你看完之后有更多的认识。Android系统中有许多包括ActivityManagerService在内的系统级服务,我们平时在使用时可通过Context调用,这些服务会在安卓系统初始化时以单例的形式注册,其中LayoutInflater服务就是其中之一。
我们常在RecyclerView.Adapter的onCreateViewHolder()中通过LayoutInflater将xml编写的布局文件转换成Android中的一个View对象:
@NonNull
@NotNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) {View inflate = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_recycler_item, parent,false);ViewHolder viewHolder = new ViewHolder(inflate);return viewHolder;
}
其实不仅仅是onCreateViewHolder(),在Activity中的setContentView()中内部也是通过LayoutInflater将xml布局转换成View。
@Override
public void setContentView(int layoutResID) {...if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());transitionTo(newScene);} else {mLayoutInflater.inflate(layoutResID, mContentParent);}...
} //SDK里PhoneWindow.java中
而我们最常使用的关于LayoutInflater的方法莫过于下面几个:
1、View.inflate(mContext,R.layout.layout_sence_recycler_item,null);
2、LayoutInflater.from(mContext).inflate(R.layout.layout_sence_recycler_item,parent);
3、LayoutInflater.from(mContext).inflate(R.layout.layout_sence_recycler_item,parent,false);
4、LayoutInflater inflater =(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);View view = inflater.inflate(R.layout.main, null);
.from(Context)方法原理暂时先不做详细讲解,其内部调用的是ContextImpl类中的getSystemService(),因此其实要说的是getSystemService(),这方法代码流程过多且与本文主旨无关不做赘述。
我们现在进去看看inflate()相关源码(在相关方法上按住Ctrl+鼠标左键点击),会发现其中总共四种重载方法:
第一种方法:
/*** Inflate a new view hierarchy from the specified xml resource. Throws* {@link InflateException} if there is an error.** @param resource ID for an XML layout resource to load (e.g.,* <code>R.layout.main_page</code>)* @param root Optional view to be the parent of the generated hierarchy.* @return The root View of the inflated hierarchy. If root was supplied,* this is the root View; otherwise it is the root of the inflated* XML file.*/
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {return inflate(resource, root, root != null);
}
第二种方法:
/*** Inflate a new view hierarchy from the specified xml node. Throws* {@link InflateException} if there is an error. ** <p>* <em><strong>Important</strong></em> For performance* reasons, view inflation relies heavily on pre-processing of XML files* that is done at build time. Therefore, it is not currently possible to* use LayoutInflater with an XmlPullParser over a plain XML file at runtime.** @param parser XML dom node containing the description of the view* hierarchy.* @param root Optional view to be the parent of the generated hierarchy.* @return The root View of the inflated hierarchy. If root was supplied,* this is the root View; otherwise it is the root of the inflated* XML file.*/
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {return inflate(parser, root, root != null);
}
第三种方法:
/*** Inflate a new view hierarchy from the specified xml resource. Throws* {@link InflateException} if there is an error.** @param resource ID for an XML layout resource to load (e.g.,* <code>R.layout.main_page</code>)* @param root Optional view to be the parent of the generated hierarchy (if* <em>attachToRoot</em> is true), or else simply an object that* provides a set of LayoutParams values for root of the returned* hierarchy (if <em>attachToRoot</em> is false.)* @param attachToRoot Whether the inflated hierarchy should be attached to* the root parameter? If false, root is only used to create the* correct subclass of LayoutParams for the root view in the XML.* @return The root View of the inflated hierarchy. If root was supplied and* attachToRoot is true, this is root; otherwise it is the root of* the inflated XML file.*/
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();}
}
第四种方法较长:
/*** Inflate a new view hierarchy from the specified XML node. Throws* {@link InflateException} if there is an error.* <p>* <em><strong>Important</strong></em> For performance* reasons, view inflation relies heavily on pre-processing of XML files* that is done at build time. Therefore, it is not currently possible to* use LayoutInflater with an XmlPullParser over a plain XML file at runtime.** @param parser XML dom node containing the description of the view* hierarchy.* @param root Optional view to be the parent of the generated hierarchy (if* <em>attachToRoot</em> is true), or else simply an object that* provides a set of LayoutParams values for root of the returned* hierarchy (if <em>attachToRoot</em> is false.)* @param attachToRoot Whether the inflated hierarchy should be attached to* the root parameter? If false, root is only used to create the* correct subclass of LayoutParams for the root view in the XML.* @return The root View of the inflated hierarchy. If root was supplied and* attachToRoot is true, this is root; otherwise it is the root of* the inflated XML file.*/
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]; //获取Context对象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("**************************");}if (TAG_MERGE.equals(name)) { //判断是否是merge标签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 xmlfinal View temp = createViewFromTag(root, name, inflaterContext, attrs); //解析单个元素,实例化xml布局的根view对象ViewGroup.LayoutParams params = null;if (root != null) {if (DEBUG) {System.out.println("Creating params from root: " +root);}// Create layout params that match root, if suppliedparams = root.generateLayoutParams(attrs); // 创建匹配root对象的布局参数if (!attachToRoot) {// Set the layout params for temp if we are not// attaching. (If we are, we use addView, below)temp.setLayoutParams(params); // attachToRoot:传进来的参数,如果该view不需要添加到父布局上,则直接将根据父布局生成的params参数来设置}}if (DEBUG) {System.out.println("-----> start inflating children");}// Inflate all children under temp against its context.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); //如果传入的父布局不为null,且attachToRoot为true,则给temp设置布局参数,将实例化的view对象加入到父布局root中}// Decide whether to return the root that was passed in or the// top view found in xml.if (root == null || !attachToRoot) { //如果传入的父布局为null,且attachToRoot为false,则返回tempresult = 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;}
}
继续跟随前三种重载方法后,你会发现最后都绕到了第四种重载方法里。
此方法可见总共三个参数:
参数一、xml解析器;
参数二、解析布局的父视图;
参数三、是否将要解析的视图添加进父视图中(是否要将当前加载的xml布局添加到第二个参数传入的父布局上面)。
关于XmlPullParser就不详解了,里面解析xml的过程较长,也与本文主旨无关。这里看一个重要方法advanceToRootNode():
/*** Advances the given parser to the first START_TAG. Throws InflateException if no start tag is* found.*/
private void advanceToRootNode(XmlPullParser parser)throws InflateException, IOException, XmlPullParserException {// Look for the root node.int type; //while循环解析查找xml标签,START_TAG是开始标签,END_DOCUMENT是结束标签while ((type = parser.next()) != XmlPullParser.START_TAG &&type != XmlPullParser.END_DOCUMENT) {// Empty}if (type != XmlPullParser.START_TAG) {throw new InflateException(parser.getPositionDescription()+ ": No start tag found!");}
}
根据以上代码,我们可以概括其inflate()流程如下:
1、解析xml布局文件中根标签(第一个元素);
2、如果根标签是merge标签,则调用rInflate()来将merge标签下所有子View添加到根标签中;
3、如果不是merge标签,则调用createViewFromTag();
4、调用rInflateChildren()解析temp下所有子View,并将其添加到temp下;
5、根据设置的参数判断返回对应视图。
可以说整段代码就是一个将xml解析成View结构的过程,其过程中细节实现靠rInflate、createViewFromTag和rInflateChildren方法,而rInflateChildren内部又是调用rInflate,因此我们看createViewFromTag()和rInflateChildren()即可。
createViewFromTag
其源码如下:
@UnsupportedAppUsage
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {if (name.equals("view")) {name = attrs.getAttributeValue(null, "class");}// Apply a theme wrapper, if allowed and one is specified.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); //通过tryCreateView返回view对象 if (view == null) {final Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = context;try {if (-1 == name.indexOf('.')) { //内置view控件的解析view = onCreateView(context, parent, name, attrs);} else {view = createView(context, name, null, attrs); //自定义View的解析}} finally {mConstructorArgs[0] = lastContext;}}return view;} catch (InflateException e) {throw e;} catch (ClassNotFoundException e) {final InflateException ie = new InflateException(getParserStateDescription(context, attrs)+ ": Error inflating class " + name, e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (Exception e) {final InflateException ie = new InflateException(getParserStateDescription(context, attrs)+ ": Error inflating class " + name, e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;}
}
继续看tryCreateView()的内部实现:
@UnsupportedAppUsage(trackingBug = 122360734)
@Nullable
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); //用户可以设置Factory来解析View,此对象默认为Null,可以忽略} else if (mFactory != null) {view = mFactory.onCreateView(name, context, attrs); //用户可以设置Factory来解析View,此对象默认为Null,可以忽略} else {view = null;}if (view == null && mPrivateFactory != null) {view = mPrivateFactory.onCreateView(parent, name, context, attrs); //用户可以设置Factory来解析View,此对象默认为Null,可以忽略}return view;
}
所以具体流程下来,你会发现,最终调用的还是:
if (view == null) {final Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = context;try {if (-1 == name.indexOf('.')) {view = onCreateView(context, parent, name, attrs);} else {view = createView(context, name, null, attrs);}} finally {mConstructorArgs[0] = lastContext;}
}
核心判断还是在这里,createViewFromTag的参数会将该元素的parent及名字传过来,我们修改下核心逻辑部分,即可为下面:
if (view == null) {final Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = context;try {if (-1 == name.indexOf('.')) {view = createView(name, "android.view.", attrs);} else {view = createView(name, null, attrs);}} finally {mConstructorArgs[0] = lastContext;}
}
相信这里你就理解了,如果这个名字在查找“.”返回-1即没有包含“.”时, 则认为是一个内置View,调用onCreate(),反之,调用createView()。
createView()
protected View onCreateView(String name, AttributeSet attrs)throws ClassNotFoundException {return createView(name, "android.view.", attrs);
}
其实onCreateView()最终还是调用的createView(),所以我们这里看createView()就行了。
@Nullable
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) { //判断缓存是否为空,为空则自行反射加载// Class not found in the cache, see if it's real, and try to add itclazz = Class.forName(prefix != null ? (prefix + name) : name, false,mContext.getClassLoader()).asSubclass(View.class);if (mFilter != null && clazz != null) {boolean allowed = mFilter.onLoadClass(clazz);if (!allowed) {failNotAllowed(name, prefix, viewContext, attrs);}}constructor = clazz.getConstructor(mConstructorSignature);constructor.setAccessible(true);sConstructorMap.put(name, constructor); //添加到缓存} else {// If we have a filter, apply it to cached constructorif (mFilter != null) {// Have we seen this name before?Boolean allowedState = mFilterMap.get(name);if (allowedState == null) {// New class -- remember whether it is allowedclazz = Class.forName(prefix != null ? (prefix + name) : name, false,mContext.getClassLoader()).asSubclass(View.class);boolean allowed = clazz != null && mFilter.onLoadClass(clazz);mFilterMap.put(name, allowed);if (!allowed) {failNotAllowed(name, prefix, viewContext, attrs);}} else if (allowedState.equals(Boolean.FALSE)) {failNotAllowed(name, prefix, viewContext, attrs);}}}Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = viewContext;Object[] args = mConstructorArgs;args[1] = attrs;try {final View view = constructor.newInstance(args); //创建新View实例,args是自定义主题相关的变量if (view instanceof ViewStub) { // 如果是ViewStub,则用同一个Context加载这个ViewStub的LayoutInflater// 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 (NoSuchMethodException e) {final InflateException ie = new InflateException(getParserStateDescription(viewContext, attrs)+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (ClassCastException e) {// If loaded class is not a View subclassfinal InflateException ie = new InflateException(getParserStateDescription(viewContext, attrs)+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (ClassNotFoundException e) {// If loadClass fails, we should propagate the exception.throw e;} catch (Exception e) {final InflateException ie = new InflateException(getParserStateDescription(viewContext, attrs) + ": Error inflating class "+ (clazz == null ? "<unknown>" : clazz.getName()), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} finally {Trace.traceEnd(Trace.TRACE_TAG_VIEW);}
}
方法很长,这里只看其核心部分:
if (constructor == null) {// Class not found in the cache, see if it's real, and try to add itclazz = Class.forName(prefix != null ? (prefix + name) : name, false,mContext.getClassLoader()).asSubclass(View.class); //如果prefix不为空则构造完整类的路径,并通过反射形式加载if (mFilter != null && clazz != null) {boolean allowed = mFilter.onLoadClass(clazz);if (!allowed) {failNotAllowed(name, prefix, viewContext, attrs);}}constructor = clazz.getConstructor(mConstructorSignature); //获取构造函数constructor.setAccessible(true); //将构造函数存入缓存中sConstructorMap.put(name, constructor);
} else {...
}Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = viewContext;Object[] args = mConstructorArgs;args[1] = attrs;try {final View view = constructor.newInstance(args); //通过反射构造Viewif (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;}
代码里的思路还是很清晰的,通过反射找到对应类的字节码文件,然后找到其对应构造方法,实例化,拿到View。
可能这段代码会让你疑惑:
constructor = clazz.getConstructor(mConstructorSignature);
我们点进去查看内部调用:
private Constructor<T> getConstructor0(Class<?>[] parameterTypes,int which) throws NoSuchMethodException
{if (parameterTypes == null) {parameterTypes = EmptyArray.CLASS;}for (Class<?> c : parameterTypes) {if (c == null) {throw new NoSuchMethodException("parameter type is null");}}Constructor<T> result = getDeclaredConstructorInternal(parameterTypes);if (result == null || which == Member.PUBLIC && !Modifier.isPublic(result.getAccessFlags())) {throw new NoSuchMethodException(getName() + ".<init> "+ Arrays.toString(parameterTypes));}return result;
}
但其实这就是反射获取的有两个参数的构造方法,这也就是为什么在做自定义控件的时候要重载两个函数的构造函数的原因。可以说这就是解析单个View的全部过程。但LayoutInflater要解析的是整个窗口中的视图树,这个靠rInflate()实现:
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 || //while循环解析各viewparser.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) { //如果根布局是include标签,抛异常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 {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); //将解析的View添加到父控件(ViewGroup)中}}if (pendingRequestFocus) {parent.restoreDefaultFocus();}if (finishInflate) {parent.onFinishInflate();}
}
可见,添加全部View的过程就是在while循环中调用一个createViewFromTag生成根view,然后 rInflateChildren → rInflate → rInflateChildren → rInflate → … … 就这样递归调用一直到遍历完全,然后通过addView添加到父控件。
相信看到这里就都明白了,整个inflate过程可以分为两大步骤:
- 通过解析器来将xml文件中的内容解析出来。
- 使用反射将解析出来的元素创建成View对象。
root为null的情况
而根据LayoutInflater.inflate()中的内部方法和返回对象又可以总结出:
调用的inflate方法 | 对应最终调用inflater()方法参数 | 返回情况 |
---|---|---|
inflate(id, null) | inflater(parser, null, false) | 生成对应View对象并返回 |
inflate(id, root) | inflater(parser, root, true) | 生成View对象添加到root上并返回root |
inflate(id, null, false) | inflate(parser, root, false) | 生成View对象并返回 |
inflate(id, null, true) | inflate(parser, null, true) | 生成View对象并返回 |
inflate(id, root, false) | inflate(parser, root, false) | 生成View对象并返回 |
inflate(id, root, true) | inflater(parser, root, true) | 生成View对象添加到root上并返回root |
只要传递的root不为空,则会根据root来创建生成View的LayoutParams。
如果LayoutInflater调用inflate(id, null),不传入root即父布局,则填充的View的layout_width和layout_height的值无论修改成多少,都不会有效果。确切点来讲,所有以layout_开头的属性都会失去作用,原因很简单,一个View的测量结果并不只是由它自己的layout_width和layout_height(即LayoutParams)所决定的,而是由父容器给它的约束(MeasureSpec)和它自身的LayoutParams共同决定的。有兴趣的朋友可以尝试验证下。
常见报错
LayoutInflate.inflate()方法报错大体有两个原因:
1、在传入父布局的情况下重复addView,对应非法状态异常;
java.lang.IllegalStateException The specified child already has a parent. You must call removeView() on the child's parent first.
最常见的非法状态异常,出现场景原因五花八门,举一个最常见的例子,我们在调用Fragment时,为什么后面的参数一定要是false?
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {return inflater.inflate(R.layout.fragment_layout, container, false);
}
其实在Fragement源码中,onCreateView()里返回的View其实已经添加到container中了。说白了就是Fragment有自己的AddView操作,如果第三个参数传入true,那么就会直接将inflate出来的布局添加到父布局当中。然后再次addView的时候就会发现它已经有一个父布局了,从而抛出IllegalStateException。(对应逻辑源码在上述解析中可看到)
2、传入相关参数有问题导致报空指针。
java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object referenceat android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:715)...
这种没什么好说的,要么是layoutInflate对象为空、或layoutInflate所依赖的Context对象为空,要么是inflate()参数中的布局文件有错,导致XmlPullParser对象无法正确的解析布局文件原因。
总结
inflate就是先把最外层的root解析完,然后用rInflate去递归把子view解析完,子view用createViewFromTag方法去解析单个View,用createView反射出view,全都递归完再返回。当然,解析肯定是耗时操作,很明显耗时操作有两处:
解析xml布局文件 和 反射获取实例
而谷歌官方对此做的优化是:
预编译 和 缓存
关于LayoutInflater源码解析先到这里了,如果有其它问题或者疑惑可以留言。