这是我参与8月更文挑战的第6天,活动详情查看: 8月更文挑战
借 8 月更文挑战督促自己,感谢掘金!
一、背景
昨天,在谈论 RecyclerView 的复用机制的时候,说到了事件的分发以及 View 的大致渲染流程,今天看了一下源码,做一下记录。
二、流程分析
1、入口
如果创建一个Activity ,系统为我们默认继承 AppCompatAvtovity ,而不是 Activity。在这里,我们把自定义的 Activity 改成继承 Activity,然后进入 setContentView,代码如下:
第一行代码,获取了一个 Window 对象,然后调用这个 Window 的 setContentView ,点进源码一看,这个 Window 是一个抽象类:
通过源码知道,这个 Window 有一个唯一的实现类 PhoneWindow,接下来看 PhoneWindow 的 setContentView:
在 PhoneWindow 的setContentView 主要做了两件事情:
- 创建一个 DecorView。
- 通过 LayoutInflater.inflate 去加载布局。
什么是 DecorView 呢?在 PhoneWindow 中,通过查看源码可以知道,这是一个顶层的 View,承载我们整个视图树,源码如下:
接下来是真正的 View 的渲染流过 LayoutInflater.inflate。
2、渲染流程
LayoutInflater 有四个不同的构造方法,但是对视图的渲染很明显最终会走到下面的这个方法中:
这里有三个参数,parser 、root 以及 attachToRoot ,我们在使用 inflate 方法时通常传入的是 布局文件,布局文件的会被映射成一个 int 类型的值,那么它是在什么时候进行转化的呢?
看看我们调用时的构造方法
解析的重点已经标注了,获得 parser 之后把它作为参数传递到另一个构造函数,来看一下获取parser 最终调用的函数:
具体过程已经折叠了,需要注意的是在转换的过程中有一个 加锁 的行为。接下来看最终调用的函数(已删除不重要的代码):
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
// 1、在 inflate 的过程中使用了 synchronized,这就意味着不可能同时存在多个线程对同一个 View 进行渲染
synchronized (mConstructorArgs) {
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
// 2、如果最外层的节点是 merge 标签,就要求传入的 root 不为 null,并且传入 attachToRoot 为 true,否则抛出异常
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 {
// 重点二
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
// 3、第一种情况 root != null
if (root != null) {
// 获取布局参数
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
// 重点三
rInflateChildren(parser, temp, attrs, true);
// 4、第二种情况 root != null && attachToRoot
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 5、第三种情况,root == null || !attachToRoot
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
} finally {
}
return result;
}
}
分析都在代码里了,现在来对上面的分析做一下总结。
- 首先会对最外层的标签进行检测,如果是 merger 的话,需要做特殊处理,如果这里不抛出异常的话,就会执行
rInflate。 - createViewFromTag 主要是通过节点名创建 View 对象。
- 会对传入的
root和attachToRoot进行判断,总共有三种情况-
root !=null && attachToRoot 为 false 的情况,会对最外层的 layout 属性进行设置。
-
root !=null && attachToRoot 为 true 的情况,会给加载的布局指定一个父布局。
-
root == null 的话,attachToRoot 不起任何作用。
-
我们似乎还有细节没有弄清楚,rInflate() 的执行流程是怎么样的?createViewFromTag()又是怎么通过 tag 创建 View 的?我们先来看createViewFromTag() 的流程(删减后的代码):
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);
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;
}
}
return view;
} catch (InflateException e) {
} catch (ClassNotFoundException e) {
} catch (Exception e) {
}
}
在 createViewFromTag() 中其实需要重点关注 tryCreateView():
通过源码可以发现,这里通过 Factory 的 onCreateView() 方法创建 View,那这个 Factory 又是什么呢?
可以看到,Factory 提供了一种 hook,方便我们来拦截使用 LayoutInflate 创建 View 的过程。因此,我们可以利用这种方式来把系统的的 View 替换成 自定义的 View。
到此为止,我们看完了 createViewFromTag() 的流程,现在来看 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 ||
parser.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) {
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 (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
代码其实很好理解,在这里获取 View 的层级,从 父 View 一级一级进行递归调用。
三、总结
总结一下,我们需要继承 Activity ,而不是 AppCompatActivity,这两者有什么区别呢?通过源码可以看到,AppCompatActivity 是 Activity 的子类,与 Activity 相比,它会在界面中设置标题栏。当然这是可以隐藏的,只需要在代码中进行控制就可以了。
总结一下大致流程:
- Activity 中,
setContentView()其实是获取 PhoneWindow 的实例,最后实际上是调用了PhoneWindow的setContentView()。 - 在 PhoneWindow 中,如果没有初始化 decorView 就先进行初始化,然后开始执行
LayoutInflate的inflate()方法。 - 在 inflate() 中使用 tag name 反射创建创建 View,然后再通递归调用解析 View,展示出来。
另外,由于水平有限,有错误的地方在所难免,未免误导他人,欢迎大佬指正!码字不易,感谢大家的点赞关注!🙏