Android自定义View-xml解析角度理解
2017-12-25 11:29
459 查看
Android自定义View-xml解析角度理解
起因
在浏览工程的时候,看到如下的自定义Viewclass CustomView extends ViewGroup { ... CustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); LayoutInflater.inflate(R.layout.layout_custom_view,this,true); ... } }
遂进行改写
// layout_custom_view.xml 布局文件 <xx.xx.xx.CustomView ... > ... </xx.xx.xx.CustomView> class CustomView extends ViewGroup { CustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); LayoutInflater.inflate(R.layout.layout_custom_view,this,true); } @Override protected void onFinishInflate() { super.onFinishInflate(); ... } }
在运行以后,爆出NullPointException以致应用crash
![](https://github.com/jiaozidev/images/blob/master/%E8%87%AA%E5%AE%9A%E4%B9%89View%E7%A9%BA%E6%8C%87%E9%92%88_2017_12_21.png)
查看出错代码,发现布局文件为
<...> ... <xx.xx.xx.CustomView android:layout_width="wrap_content" android:layout_height="wrap_content" ... /> ... </...>
在一下午的时间里百思不得其解,在下班回家的路上突然想到是在报错文件的布局中,CustomView作为一个父容器没有包含任何的子view,导致在onFinishInflate()方法中获取的子元素都为空,正确使用的布局文件应该为:
... <include android:layout_width="wrap_content" android:layout_height="wrap_content" layout="@layout/layout_custom_view" /> ...
趁这个时机,了解下Android从xml文件中生成视图的过程。
XmlPullParser
next():获取下一个parsing EventSTART_DOCUMENT:文档开始,没有内容被读。只能在next(),nextToken(),nextTag()之前获取
START_TAG:TAG开始被读取,tag的名字可以使用getName()获取,namespace和prefix可以使用gatNameSpace()和getPrefix()获取
TEXT:字符出具被读取,可以使用getText()获取内容
END_TAG:TAG结尾被读取,tag的名字可以使用getName()获取,namespace和prefix可以使用gatNameSpace()和getPrefix()获取
END_DOCUMENT:逻辑上的文档结尾。在next(),nextToken()调用后,到达文档的结尾
getEventType():获取parser当前事件状态
流程
最终会调用到LayoutInflater.inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)// LayoutInflater /** * 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) + ")"); } final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
在这里将layout中的布局xml文件解析成XmlResourceParser,并调用LayoutInflater.inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
// LayoutInflater /** * 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]; mConstructorArgs[0] = inflaterContext; View result = root; try { // Look for the root node. int type; 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!"); } 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 xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; // XXX 在这里进行root和attachToRoot参数的处理 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"); } // 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); } // Decide whether to return the root that was passed in or the // top view found in xml. 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(parser.getPositionDescription() + ": " + 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文件,读取TAG,并进行处理,并对进行特殊判断,不能作为根布局。调用createViewFromTag创建在布局里的根View
/** * Creates a view from a tag name using the supplied attribute set. * <p> * <strong>Note:</strong> Default visibility so the BridgeInflater can * override it. * * @param parent the parent view, used to inflate layout params * @param name the name of the XML tag used to define the view * @param context the inflation context for the view, typically the * {@code parent} or base layout inflater context * @param attrs the attribute set for the XML tag used to define the view * @param ignoreThemeAttr {@code true} to ignore the {@code android:theme} * attribute (if set) for the view being inflated, * {@code false} otherwise */ 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(); } if (name.equals(TAG_1995)) { // Let's party like it's 1995! return new BlinkLayout(context, attrs); } try { 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); } if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) { throw e; } catch (ClassNotFoundException e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (Exception e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } }
对View的属性进行处理,并调用createView(String name, String prefix, AttributeSet attrs)生成真实的View
/** * Low-level function for instantiating a view by name. This attempts to * instantiate a view class of the given <var>name</var> found in this * LayoutInflater's ClassLoader. * * <p> * There are two things that can happen in an error case: either the * exception describing the error will be thrown, or a null will be * returned. You must deal with both possibilities -- the former will happen * the first time createView() is called for a class of a particular name, * the latter every time there-after for that class name. * * @param name The full name of the class to be instantiated. * @param attrs The XML attributes supplied for this instance. * * @return View The newly instantiated view, or null. */ public final View createView(String name, String prefix, 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 { 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 it clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } else { // If we have a filter, apply it to cached constructor if (mFilter != null) { // Have we seen this name before? Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { // New class -- remember whether it is allowed clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); if (!allowed) { failNotAllowed(name, prefix, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, attrs); } } } Object lastContext = mConstructorArgs[0]; if (mConstructorArgs[0] == null) { // Fill in the context if not already within inflation. mConstructorArgs[0] = mContext; } Object[] args = mConstructorArgs; args[1] = attrs; 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])); } mConstructorArgs[0] = lastContext; return view; } catch (NoSuchMethodException e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": 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 subclass final InflateException ie = new InflateException(attrs.getPositionDescription() + ": 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( attrs.getPositionDescription() + ": Error inflating class " + (clazz == null ? "<unknown>" : clazz.getName()), e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
通过反射获取对应View的构造函数,并进行初始化操作,放回rootView后,回到public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot),调用final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate),对包含的子View进行处理
/** * Recursive method used to descend down the xml hierarchy and instantiate * views, instantiate their children, and then call onFinishInflate(). * <p> * <strong>Note:</strong> Default visibility so the BridgeInflater can * override it. */ void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; 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)) { parseRequestFocus(parser, parent); } 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 { 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 (finishInflate) { parent.onFinishInflate(); } }
对子View进行递归createViewFromTag生成子View,并加入子View的容器,最后调用xml布局根View的onFinishInflate()
总结
出现空指针的原因就在于在布局中使用了自定义View,但是没有子View导致获取的参数都为null在onFinishInflate()调用时,还没有加入到显示的视图中
github地址
相关文章推荐
- Android解析自定义属性的XML实现底部导航栏TabSelectedView,实现灵活的配置扩展
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上,view)
- View 相关 Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下,ViewGroup篇)
- View相关 Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android自定义View的自定义属性atrrs.xml解析
- Android AsyncTask完全解析,带你从源码的角度彻底理解
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android自定义View过程解析
- Android事件分发机制完全解析,带你从源码的角度彻底理解(一)
- Android Volley完全解析(四),从源码的角度理解Volley
- Android自定义View制作动态炫酷按钮实例解析
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上) (出自郭霖老师)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)