Android View绘制- LayoutInflater.inflate() 流程简要解析
布局,作为 Android 中展示 UI 的最主要的元素,其实它是怎么通过布局文件转化为实际的 UI 的?这篇文章,就是对这一过程进行简要的解析,并且提出一些实际开发过程需要注意的问题。
阅读该文章你可以了解到
- LayoutInfalter 的工作过程
- 布局优化的一些建议
LayoutInflater 原理讲解
我们需要在 Activity 上显示一个布局文件,通常在 Activity 的 onCreate() 使用以下代码:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_inner_class_memory_leack);
}
实际上, setContentView() 的调用链如下
// Activity.setContentView(),这里的getWindow() 拿到的是一个 PhoneWindow 对象
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
// PhoneWinwod.setContent()
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
最终会进入到 PhoneWindow 这里,然后最终调用的代码是第二十二行的 mLayoutInflater.inflate(layoutResID, mContentParent); 这一行代码,这里将 mContentParent 传入作为父容器。然后最终都会调用到 LayoutInflater 的 inflate() 方法,如下:
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 标签,则不会创建父布局元素
// 直接通过 rInflate() 方法,遍历生成子 View
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
// 创建该 Layout 布局中的父布局
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
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;
}
}
这里看一些关键代码
- 第三十三行,如果标签是 Merge 标签,则会跳过为当前标签创建 View 的流程,这也是布局文件里面 Merge 标签优化的原理所在。
- 第四十四行,通过 createViewFromTag() 方法,创建该 布局文件中的最高父节点 View
- 第五十五行到第七十二行,通过 判断形参 attachToRoot 和 root 的值,决定,是否将 Inflate 出来的父节点 View,加入到 root 父容器里面,同时影响该方法的返回值,规则如下:
- 如果 root,为null,则不会添加到 root 中,并且没有设置 LayoutParams,该方法最后返回父节点 View
- 如果 root 不为空,attachRoot 没有指定或者为 ture,则默认添加到 root 中,会将 LayoutParams 传递给 root.addView() 方法,同时返回值为 root View。
- 如果 root 不为空,attachRoot 为 false,则等同第一种情况,只不过会根据 root 计算 LayoutParmas,并且给当前 Inflate 的 View,也就是 temp 设置 LayoutParmas,当然只有宽和高属性(其实等于调用了 new LayoutParams() 构造方法)。
- inflate() 也是个遍历调用的过程,会从父容器调用遍历到子 View,调用的代码是第四十行和第六十七行的代码,也就是最终还是会调用到 LayoutInflater.rInflate() 方法。
然后简单看一下 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();
}
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;
}
}
这里的代码比较简单,就是调用各种 LayoutFactory 的 onCreateView() 方法去创建 View,这里的 LayoutFactory 有其它用途,也就是可以通过自定义 Factory 实现换肤功能。
这里简单提一下吧,不过具体的换肤功能还是比较复杂的:
private void testChangeSkin(){
getLayoutInflater().setFactory(new LayoutInflater.Factory() {
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
//通过获取你所需要的换肤属性 或者换肤控件,对一些属性进行调整
return null;
}
});
}
就是通过为 Window 设置自定义的 Factory,然后控制每个 View 的创建,然后在创建的时候, 读取自定义属性,控制 View 的一些颜色,字体属性,达到自定义换肤的目的,类似实现的 github 上有开源的框架,感兴趣的可以去了解一下。
https://github.com/fengjundev/Android-Skin-Loader
再次回到代码,看一下 rInflate() 方法,这个方法也是比较重要的:
private static final String TAG_MERGE = "merge";// merge 标签,减少布局嵌套
private static final String TAG_INCLUDE = "include";//include 标签,增加布局文件复用
private static final String TAG_1995 = "blink";// 类似 FrameLayout
private static final String TAG_REQUEST_FOCUS = "requestFocus";//requestFocus 请求焦点
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();//depth ViewTree 的深度
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)) {//所以 merge 标签只能作为一个布局文件的父标签
throw new InflateException("<merge /> must be the root element");
} else {
//最后调用 createViewFromTag() 方法创建这个节点的 View
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
// 根据父布局,计算 LayoutParmas,得出当前 View 宽高测量数值
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//循环调用 rInflate()方法,直到 xml 文件末尾
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) {
parent.onFinishInflate();//会回调这个方法,表示该父 View 的所有子 View inflate() 完成
}
}
关键的解释,已经在代码中添加了注释,比较有趣的是,这里 Inflate 过程也是递归调用的,并且每一个 父 View 的 子 View inflate() 完成,还有回调 onFinishInflate() 方法,那么我们是不是可以在父 View 的这个方法里面,拿到 子View 的宽高呢?
需要知道这个答案的,可以看后续的文章,不过答案自然是,不能的。
原理优化以及进阶使用
讲解了 Inflate 的流程,自然还需要做做一番拓展,看下这个知识点,在我们日常开发有哪些高逼格的使用技巧
- 实现自定义换肤,上面已经简单提到过了
- 优化布局,了解那些布局标签的真正原理
- inflate() 是一个递归的过程,所以我们需要合理设计自己的布局文件,避免使用过于复杂的布局。
- 关于 inflate() 耗时的计算,可以在父布局的 onFinishInflate() 添加耗时完成的计算,查看哪些布局文件是耗时的, 然后做对应的优化。
最终 inflate() 完成之后,只是粗略的设置了把父布局和子 View 融合在一起,并且把 LayoutParams 保存在 子 View 中,并还没有完成子 View 真正的绘制和布局,需要了解的,看后序文章分析。