一直想写关于android解析xml文件构建View视图的学习笔记,到今天 才算静下心来大致过了一遍它的源码,特此简单的分析一下它的大致脉络。具体怎么解析xml的是由XmlPullParser这个类来完成的,至于解析的细节就不作说明。算是个简单的学习笔记。
在android里面是由LayoutInflater这个类来完成xml构建View的工作,主要工作交给该类的如下几个重载方法来完成:
前面上那个inflate方法最终会调用租后一个inflate方法来完成解析的工作,让我们看看这个方法都做了些什么:
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { //获取AttributeSet对象 final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context)mConstructorArgs[0]; //保存mContext对象,在最终用反射生成View对象的时候会用到该参数 mConstructorArgs[0] = mContext; //最终解析获取产生的返回的View对象 View result = root; // 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!"); } //节点名,即API中的控件或者自定义View完整限定名 final String name = parser.getName(); //处理<merge/>标签 if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, attrs, false); } else { // Temp is the root view that was found in the xml //Temp是xml文件里面的根View View temp; if (TAG_1995.equals(name)) {//处理<blink/>标签 temp = new BlinkLayout(mContext, attrs); } else {// //创建该xml布局文件所对应的根View。 temp = createViewFromTag(root, name, attrs); } //root 不为null的情况下就创建LayoutParams参数 ViewGroup.LayoutParams params = null; if (root != null) { // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); //如果xml的根View不跟root绑定 if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) //设置xml根View的params temp.setLayoutParams(params); } } // Inflate all children under temp //即系xml文件根View里面的 的所有子节点,也就是添加temp View的所有的子View rInflate(parser, temp, attrs, true); // We are supposed to attach all the views we found (int temp) // to root. Do that now. //如果root!=null并且绑定,就返回root if (root != null && attachToRoot) { //将params设置给temp, //并将temp添加到root里面 root.addView(temp, params); } // Decide whether to return the root that was passed in or the // top view found in xml. //如果root为null或者不绑定root就返回temp if (root == null || !attachToRoot) { result = temp; } } return result; } }
1)如果root为null的情况下,xml文件对应的根View(也就是上面对应的temp)的LayoutParams是为null的,其实这正是我们在ListView直接调用:
convertView = App.getLayoutInflater().inflate(R.layout.item, null);使得item.xml的根View的宽和高设置无效的原因,解决方法很简,详细解说 点击此处。
2)LayoutParam是由AttributeSet属性传递个generateLayoutParams方法来构建具体的LayoutParams对象,该AttributeSet对象在inflate的第一行产生。
在ViewGroup 类里面generateLayoutParams方法如下:
public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } public LayoutParams(Context c, AttributeSet attrs) { TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); setBaseAttributes(a, R.styleable.ViewGroup_Layout_layout_width, R.styleable.ViewGroup_Layout_layout_height); a.recycle(); } //设置基本的layout_width和layout_height属性 protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { width = a.getLayoutDimension(widthAttr, "layout_width"); height = a.getLayoutDimension(heightAttr, "layout_height"); } //可以看出,如果不设置layout_width或者layout_height的话,会抛出异常。 public int getLayoutDimension(int index, String name) { index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; if (type >= TypedValue.TYPE_FIRST_INT && type <= TypedValue.TYPE_LAST_INT) { return data[index+AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelSize( data[index+AssetManager.STYLE_DATA], mResources.mMetrics); } throw new RuntimeException(getPositionDescription() + ": You must supply a " + name + " attribute."); }也就是说在创建xml跟对象的时候会设置其LayoutParams对象并初始化其width和height属性,这两个属性在对View进行Measure的时候用到,这点需要注意,在ViewGroup中提供Measurechild方法:在该方法中会调用View的getLayoutParams方法来获取设置的LayoutParams对象,并取的其lp.width和lp.height来进行child的Measure操作。
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); //需要获取lp.width final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); //需要获取lp.height final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
其实通过上面的代码还可以得出一个关于View Measure的结论:childView的measureSpec是是由parentView的measureSpec和childView自身的LayoutParams来共同决定的
3)如果root不为null,那么就调用root.root.generateLayoutParams(attrs);方法产生一个LayoutParams对象,来设置xml根View的layoutParams.具体有两种方法
3.1)attachToRoot为false的情况下直接temp.setLayoutParams(params)
3.2)attachToRoot为true的情况下调用root.addView(temp.params)来讲params对象设置给temp.而ViewGroup的addView方法最终会调用如下方法:
private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) { if (!checkLayoutParams(params)) { params = generateLayoutParams(params); } //将params设置给child也就是上面所说的temp if (preventRequestLayout) { child.mLayoutParams = params; } else { child.setLayoutParams(params); } }
4)通过createViewFromTag来创建xml布局文件对应的根View,下面看看这个方法都做了些什么:
//此时第一次调用该方法,parent为上面所说的temp也就是xml的根View View createViewFromTag(View parent, String name, AttributeSet attrs) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } View view; //此处省略的工厂方法创建创建view的实例的代码,因为在平时的使用中并没有用到这些工厂,所以忽略此代码 if (view == null) { //android 自带view的实例 if (-1 == name.indexOf('.')) { //parent参数才此处没有用到 view = onCreateView(parent, name, attrs); } else {//创建自定义view的实例 view = createView(name, null, attrs);//注意第二个参数为null } } return view; }
public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { Constructor<? extends View> constructor = sConstructorMap.get(name); Class<? extends View> clazz = null; if (constructor == null) {//缓存中没有name对应View的constructor对象 // Class not found in the cache, see if it's real, and try to add it //获取name标签所代表view类的class对象 clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); 。。。。。 constructor = clazz.getConstructor(mConstructorSignature); //放入缓存中去 sConstructorMap.put(name, constructor); } else { // If we have a filter, apply it to cached constructor if (mFilter != null) { } } //传递参数,生成View实例对象 Object[] args = mConstructorArgs; args[1] = attrs; return constructor.newInstance(args); }
1)该方法根据第二个参数prefix来判断是否是自定义组件,如果null的话说明是程序员自定义的view组件,否则就是android api里面的view组件
2)为了提高解析xml创建View的效率,在LayoutInflate里面对tag及该tag对应View的Constructor做了缓存处理。这样在同一个xml文件里面有相同的Tag的时候就不用再次用ClassLoader加载clss文件了。
3)最后 三行代码完成了构建View的工作,其中args[0]对应的是mContext对象,而args[1]就AttributeSet这个对象了。最总通过构造器Constructor.newInstance来创建完成View的创建。这也是为什么View或者ViewGroup没有提供默认构造器的原因:防止自定义View的时候不重写View或者ViewGroup的构造器,这样解析的时候会出错。
到此位置xml的根标签解析成View对象的工作已经完成,那么xml文件中根View包含的子View又是怎么解析的呢?其实想想也应该知道,肯定也是根据子View的tag调用createViewFromTag来创建具体的子View,因为是一个View Tree也就是简单的递归调用而已。事实上查看inflate方法可知道在对xml子view的解析是在xml根View创建完成后调用rInlfate方法来完成的
//即系xml文件根View里面的 的所有子节点,也就是添加temp View的所有的子View rInflate(parser, temp, attrs, true);最后就让我们看看rInflate方法都做了些什么吧:
/ void rInflate(XmlPullParser parser, View parent, final 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)) {//解析<requestFocus />标签 parseRequestFocus(parser, parent); } else if (TAG_INCLUDE.equals(name)) {//解析<include />标签 if (parser.getDepth() == 0) { throw new InflateException("<include /> cannot be the root element"); } //解析<include />标签 parseInclude(parser, parent, attrs); } else if (TAG_MERGE.equals(name)) {//处理<merge />标签 throw new InflateException("<merge /> must be the root element"); } else if (TAG_1995.equals(name)) {//处理<blink/>标签 final View view = new BlinkLayout(mContext, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs, true); viewGroup.addView(view, params); } else { //根据当前节点名构建一个View实例对象 final View view = createViewFromTag(parent, name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; //创建一个layoutPrarams对象 final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); //继续递归调用 解析当前view 的子view rInflate(parser, view, attrs, true); //把生成的view添加到parent view中 //需要注意的是addView每次都调用了该子view的parentView所产生的Layoutparams对象 viewGroup.addView(view, params); } }//end while if (finishInflate) parent.onFinishInflate(); }
由此,android通过LayoutInflater解析xml创建View对象的简单脉络分析完毕,很清晰,通过追踪它的源码很容易就把此脉络理清;至于更深层次的解析原理,水平有限就不做分析了,如有不当之处欢迎批评指正