安卓开发中,最常见的就是在 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三大过程!