安卓开发中,最常见的就是在 Activity 的 onCreate 方法中调用 setContentView 后,开始一系列的 findViewById、 setOnClickListener、setText 等等视图相关的操作,在调用 setContentView 方法后整个 Activity 的视图树就形成了,也就是说我们在 xml 布局文件中写的布局都 “转化” 成了我们在代码中可以操作的一个个 View、ViewGroup 对象。似乎一切都是那么的顺理成章。但是仔细想一下下边这些问题就会发现原来掌握的知识还远远不够:

1、视图树是怎么形成的?

2、视图树是靠什么结构存起来的(ViewGroup 和 View 的存储关系)?

3、addView、removeView、detachView 是怎样影响视图树的?

4、setContentView后视图树形成了,这个时候所有的视图经过 measure、layout、draw三大过程了吗?

一、视图树形成过程

在 Activity 中调用 setContentView 后,最终会走到这里:

public void setContentView(int resId) {
        this.ensureSubDecor();
        //contentParent是一个系统的继承自 FrameLayout 的ViewGroup,我们自己的xml布局文件就是放在这个contentParent中
        ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
        contentParent.removeAllViews();
        //关键代码
        LayoutInflater.from(this.mContext).inflate(resId, contentParent);
        this.mOriginalWindowCallback.onContentChanged();
    }

最终会通过 LayoutInflater 的 inflate 方法,将系统的 contentParent 以及我们的布局id传入,由此开始生成视图树。inflate有多个不同参数的重载:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    	 // 省略无关代码
        final Resources res = getContext().getResources();
        final XmlResourceParser parser = res.getLayout(resource);
        return inflate(parser, root, attachToRoot);
    }

这里会根据我们的 xml 布局文件生成一个 XmlResourceParser 对象,这个对象的作用就类似于 ArrayList 的 Iterator 迭代器,通过迭代器去遍历访问整个 xml 文件的中各个节点信息。更详细的安卓Pull解析这里就不说了。最终方法走到了另一个重载的 inflate 方法。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            //省略了很多无关的代码。。。
            final Context inflaterContext = mContext;
            // 注释1
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            View result = root;
            try {
                // 注释2
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                final String name = parser.getName();
                
                // 注释3
                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, inflaterContext, attrs, false);
                } else {
                    // 注释4
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        // 注释5
                        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);
                        }
                    }

                    // 注释6
                    rInflateChildren(parser, temp, attrs, true);

                    // 注释7
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // 注释8
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
               
            }
            //省略无关代码
            return result;
        }
    }

我对 inflate 的代码做了精简只保留了主线上的代码。先看 注释1,调用了 Xml.asAttributeSet(parser), 源码很简单:

public static AttributeSet asAttributeSet(XmlPullParser parser) {
        return (parser instanceof AttributeSet)
                ? (AttributeSet) parser
                : new XmlPullAttributes(parser);
    }

这个 parser 对象是我们在上一个inflate重载方法中生成的,他是一个 XmlResourceParser 对象,而 XmlResourceParser 就是直接实现的 AttributeSet 接口, 所以上边的方法就是直接将 parser 强转为父类并返回。所以回到 注释1, attrs 和 parser 实际上是指向同一个内存地址的两个引用。这句话很重要,因为当 parser 调用 next 方法向下查找节点时, attrs 实际上也指向下一个节点了,因为这两个引用指向的是同一个对象。再看 注释2,调用了parser.next() 方法此时布局解析器指向第一个节点,然后调用 parser.getName() 就可以获得当前节点的名称,例如:LinearLayout。注释3判断当前的节点是否为 merge 标签,暂时先考虑简单的情况,直接看 注释4,获取到当前节点的名字以及attr(布局文件的属性)信息后,开始创建View:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
     
        try {
            View view;
            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;
        }
    }

省略了很多代码,直接看关键代码处,通过 判断条件: -1 == name.indexOf(’.’) ,这句代码是什么意思呢?name 是我们在布局文件中写的标签,想一想如果是系统的控件例如 TextView、Button等都是直接使用的,如果是我们自定义的View,那在布局文件中就需要完整的包名:

<TextView />
<Button />
<com.study.CustomButton />

看到没,这个判断条件就是判断这个标签是系统的View 还是自定义的 View。如果是自定义的View那么就执行 createView 方法,如果是系统View,就执行 onCreateView,先来看 onCreateView:

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

哦!原来也是调用的 createView,只不过第二个参数加上了 android.view 这个包名,而如果是自定义View的时候,第二个参数传的是 null。这可以得出什么结论呢?如果是系统空间,包名 + name 就可以得到这个系统控件的全类名。如果是自定义控件,因为name中已经是全类名了,所以第二个参数直接传null。为什么需要全类名呢?继续往下看:

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;
        //关键代码1
        clazz = mContext.getClassLoader().loadClass(
                prefix != null ? (prefix + name) : name).asSubclass(View.class);
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        //关键代码2
        final View view = constructor.newInstance(args);
        return view;
    }

删减了很多代码,看 关键代码1,通过反射拿到了我们要创建的View的 class 对象,这下就知道为什么要全类名了吧?原来我们的View 树种中所有的View对象都是通过反射创建的。然后关键代码2通过调用构造方法创建实例对象并返回。

绕了一大圈,我们终于知道了布局文件中的标签是如何 “翻译” 成我们代码中的 View对象的了。现在重新回到 inflate 方法中,注释4通过 createViewFromTag 方法反射创建了View实例对象,在注释5处调用root.generateLayoutParams生成当前View的 LayoutParams 对象。root是ViewGroup的子类,所以直接看ViewGroup:

public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

方法直接创建了一个LayoutParams对象,继续看LayoutParams的构造方法:

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();
        }
        
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
            width = a.getLayoutDimension(widthAttr, "layout_width");
            height = a.getLayoutDimension(heightAttr, "layout_height");
        }

原来我们在xml布局文件中写的 layout_width、layout_height 属性是在这里被解析的!!这样我们在布局文件中写的宽高属性就被解析保存在LayoutParams对象中了。继续回到 inflate 方法,当 attachToRoot 为 false 的时候,我们就把LayoutParams设置给当前生成的View对象。

先来一个简单的总结,到 注释5 为止,我们先根据布局文件生成了一个解析器 parser, 遍历解析器拿到第一个布局标签,根据这个布局标签的信息,通过反射的形式创建了一个View实例对象。然后继续解析当前标签的layout_width、layout_height属性并生成一个LayoutParams对象,当 attachToRoot 为 false 的时候,我们就把LayoutParams设置给当前生成的View对象。

好了,继续来看注释6,调用了 rInflateChildren 方法,从名字就可以看出,这个方法的作用就是以上一步中生成的 View 实例对象 temp 作为 parent,然后递归的去创建 temp 的所有子 View:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

继续调用了 rInflate :

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) {

            final String name = parser.getName();

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

删掉了很多无关的代码,可以看到每一次 while 循环,都会创建一个 parent 的一级子View,并把这个一级的子View作为parent继续递归…每一次循环的最后都把创建的 view 添加到 parent 中。这样当这个方法执行完成后,以parent作为root,他所有的子View,以及子View的子View(孙子、重孙子。。。)等等所有的View就都创建并add到相应的父view中去了。

好了,再来做一个简单的总结。到注释6为止,我们先根据布局文件生成了一个解析器 parser, 遍历解析器拿到第一个布局标签,根据这个布局标签的信息,通过反射的形式创建了一个View实例对象 temp。然后继续解析当前标签的layout_width、layout_height属性并生成一个LayoutParams对象,当 attachToRoot 为 false 的时候,我们就把LayoutParams设置给当前生成的View对象。并且以 temp 作为根节点,再来一次递归把temp的所有子View也都实例化并add到相应的父View中,至此,我们的整棵View树已经创建完毕了!!布局文件中所有的 xml 标签都被翻译成了 View 对象,并且都被 add 到了相应的父View中。

再回到 inflate 方法,还剩下 注释7 和 注释8,回到Activity的setContentView方法中,在调用inflate方法的时候,会传入一个 contentParent 对象,还有印象吗?这个对象就是解析我们布局文件的时候传入的最根部的 root。所以注释7的判断是成立的,我们的xml布局文件中的跟标签所生成的View对象,最终会被add到contentParent中!!至此,我们的布局文件也就完全被 “翻译创建”,并被添加到contentParent中了。这样,一个页面中以 DectorView 作为最根部的视图,一直延伸到我们自己创建的布局文件,整棵树就完全创建成功了。到这里,第一个问题应该也解释清楚了。

二、视图树是靠什么结构存起来的(ViewGroup 和 View 的存储关系)

一棵完整的View树,拿到任意一个ViewGroup,都可以获取到他有哪些子View。相反拿到任意一个View,也都可以知道他的parent是谁。那么View树是如何保存这种链条关系的呢?上边分析源码的时候,多次提到 viewGroup.addView(view, params); 通过这个方法将子View 添加到他的父View中,直接看ViewGroup源码:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    // 所有子View
    private View[] mChildren;
    //子View的数量
    private int mChildrenCount;
}

首先看ViewGroup的类结构,内部通过一个 View数组保存所有的子View,所以这里可以猜想,addView 方法实际上就是将 View 添加到mChildren数组中,并且将mChildrenCount自增1。下面来看ViewGroup的add方法:

//第一个add重载方法
    public void addView(View child) {
        addView(child, -1);
    }
    
    //第二个add重载方法
    public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }
    
    //第三个add重载方法
    public void addView(View child, int index, LayoutParams params) {
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }
    
    //最终调用了addViewInner
    private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {
        //省略无关代码
        //index 默认是 -1 ,所以此处被赋值为mChildrenCount
        if (index < 0) {
            index = mChildrenCount;
        }

        addInArray(child, index);

        if (preventRequestLayout) {
            child.assignParent(this);
        } else {
        	  //关键代码
            child.mParent = this;
        }
    }

经过了多个重载方法,最终调用了addViewInner。addInArray方法很容易就可以想到,就是将child 添加到 mChildren 数组中。我们先来看下面的判断,preventRequestLayout为false,所以走下边的代码,直接将 this 也就是ViewGroup 赋值给 child 对象的 mParent 属性,那么 mParent 属性又是什么呢?

public class View {
	protected ViewParent mParent;
}

ViewParent 是一个接口,而ViewGroup正是实现了这个接口!在 addViewInner 方法中,对 View 的 mParent 进行了赋值。这样,任意一个View,都可以通过 mParent 找到他的直接父类!再回过头来看 addInArray 方法:

private void addInArray(View child, int index) {
        View[] children = mChildren;
        final int count = mChildrenCount;
        final int size = children.length;
        if (index == count) {
            if (size == count) {
                mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
                System.arraycopy(children, 0, mChildren, 0, size);
                children = mChildren;
            }
            //关键代码
            children[mChildrenCount++] = child;
        }
        //省略代码。。。
    }

在addViewInner方法中,index 被赋值为mChildrenCount,所以这个方法就是判断当前mChildren数组是否需要扩容,并将child放到数组指定的位置上。

再来总结一波,通过 ViewGroup的addView方法,一共做了两件事:1.将view添加到ViewGroup的数组中。2.给View 的 mParent属性赋值为当前的ViewGroup对象。通过这两步,整个view树就像一个 “双向链表” 一样,从根部的root,可以找到最顶部的View,也可以从任意一个View,一直找到根部的root。 到这里,第二个问题也解释清楚了。

三、addView、removeView、detachView 是怎样影响视图的?

addView在上边已经说过了,下边主要来看一下detachView 和 removeView 有什么不同。先看detachView做了什么:

protected void detachViewFromParent(View child) {
        removeFromArray(indexOfChild(child));
    }
    
    private void removeFromArray(int index) {
        final View[] children = mChildren;
        //省略代码。。。
        children[index].mParent = null;

        final int count = mChildrenCount;
        if (index == count - 1) {
            children[--mChildrenCount] = null;
        } else if (index >= 0 && index < count) {
            System.arraycopy(children, index + 1, children, index, count - index - 1);
            children[--mChildrenCount] = null;
        } else {
            throw new IndexOutOfBoundsException();
        }
    }

在 detachViewFromParent 中,直接回调了removeFromArray。这个方法做了两件事:1.将child的 mParent属性置为null。2.将child从mChildren数组中移除。这样就切断了View和ViewGroup的联系。但是这个方法会引起UI上的变化吗?答案是不会的,因为这个方法并没有触发界面重新绘制,所以UI是没有变化的。只是在调用getChildCount的时候返回的数量出现了变化。

再看removeView的时候都干了什么:

public void removeView(View view) {
        if (removeViewInternal(view)) {
            requestLayout();
            invalidate(true);
        }
    }

看到了 requestLayout 和 invalidate 两个方法,也就是说如果 removeViewInternal 方法返回 true,那么就会引起视图的刷新,UI也就改变了。所以看下removeViewInternal:

private boolean removeViewInternal(View view) {
        final int index = indexOfChild(view);
        if (index >= 0) {
            removeViewInternal(index, view);
            return true;
        }
        return false;
    }

只要当前要移除的child确实是ViewGroup的子View,那么就一定返回true,继续看重载方法:

private void removeViewInternal(int index, View view) {
	// 省略很多代码
	removeFromArray(index);
	// 省略很多代码
}

再来总结一波,当我们调用 detachViewFromParent 的时候,只是把要想要 detach 的 view 从数组中移除,这时候因为没有处罚界面的刷新,所以在UI上并没有变化。这个方法是 protected 访问权限的所以我们在外部并不能直接访问,那这个 detachViewFromParent 方法有什么用呢?如果你看过 RecyclerView 的源码,在 LayoutManager中有这样一个方法:detachAndScrapAttachedViews 。在RecyclerView 布局子View的时候,会执行这个方法先将 RecyclerView 中所有的子View 全部 detach 下来,再重新 add 进去。更详细的可以看 启舰的RecyclerView系列的文章。再来说 removeView 相比于 detachView 他又额外的调用了 requestLayout、invalidate 触发了界面的刷新。到这里第三个问题应该也解释清楚了。

四、setContentView后视图树形成,这个时候所有的视图经过 measure、layout、draw三大过程了吗?

如上面的分析,当我们在 Activity 的 onCreate 方法中调用了 setContentView 后,整个视图树就已经形成了。这个时候我们可以通过 findViewById 找到 View 对象,也可以 setOnClickListener 设置点击事件,但是实际上此时的 View 树处于一个万事俱备只欠东风的状态,这个 “东风” 也就是 ViewRootImpl 调用 performTraversals 方法开始界面的绘制。这里就又扯到Activity的启动流程了,直接给出结论了:在Activit的创建过程中,View 的真正测量是在 Activity 的 onResume 方法之后,也就是说我们在 onCreate、onResume方法之前,View还没有经过测量布局绘制,我们在onCreate、onResume方法中想获取 View 的宽、高是获取不到的!因为这时还没有经过测量。所以在setContentView后视图树形成,这个时候视图并没有经过 measure、layout、draw三大过程!