Android LayoutInflater 布局加载原理
LayoutInflater 介绍
LayoutInflater 是一个用于将 xml 布局文件加载为 View 或者 ViewGroup 对象的工具,称之为布局加载器
LayoutInflater 获取
LayoutInflater 本身是一个抽象类,我们不可以直接通过 new 的方式去获得它的实例,通常有下面三种方式:
第一种
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
第二种
LayoutInflater inflater = LayoutInflater.from(context);
第二种
在 Activity 内部调用 getLayoutInflater() 方法
后面两种方法的实现:
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
在 Activity 内部调用 getLayoutInflater 方法其实调用的是 PhoneWindow 的mLayoutInflater:
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
inflate有多个不同的重载方法:
inflate(int resource, ViewGroup root)
inflate(int resource, ViewGroup root, boolean attachToRoot)
inflate(XmlPullParser parser, ViewGroup root)
inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
比对下这几个重载方法:
public View inflate(int resource, ViewGroup root) {
// root不为空时,attachToRoot默认为true
return inflate(resource, root, root != null);
}
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, ViewGroup root) {
// root不为空时,attachToRoot默认为true
return inflate(parser, root, root != null);
}
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
...
}
前三个方法最终调用的都是:
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
...
}
而且,root 不为空时,attachToRoot 默认为 true。布局 id 会被通过调用 getLayout 方法生成一个 XmlResourceParser 对象。
Android 中布局文件都是使用 xml 编写的,所以解析过程自然涉及 xml 的解析。常用的 xml 解析方式有 DOM,SAX 和 PULL 三种方式。DOM 不适合 xml 文档较大,内存较小的场景,所以不适用于手机这样内存有限的移动设备上。SAX 和 PULL 类似,都具有解析速度快,占用内存少的优点,而相对之下,PULL 的操作方式更为简单易用,所以,Android 系统内部在解析各种 xml 时都用的是 PULL 解析器。
这里解析布局 xml 文件时使用的就是 Android 系统提供的 PULL 方式。
继续分析inflate方法。
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;
// 首先注意 result 初始值为 root
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!");
}
// 获取当前节点名称,如merge,RelativeLayout等
final String name = parser.getName();
// 处理merge节点
if (TAG_MERGE.equals(name)) {
// merge必须依附在一个根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
// 根据当前信息生成一个View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
// 如果指定了root参数的话,根据节点的布局参数生成合适的LayoutParams
params = root.generateLayoutParams(attrs);
// 若指定了attachToRoot为false,会将生成的布局参数应用于上一步生成的View
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
// 由上至下,递归加载xml内View,并添加到temp里
rInflateChildren(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
// 如果root不为空且指定了attachToRoot为true时,会将temp作为子View添加到root中
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.
// 如果指定的root为空,或者attachToRoot为false的时候,返回的是加载出来的View,
// 否则返回root
if (root == null || !attachToRoot) {
result = temp;
}
}
} ... // 异常处理
return result;
}
}
首先定义布局根 View 这一个概念,注意与 root 并不是同一个东西:
- root 是我们传进来的第二个参数
- 布局根 View 是传递进来的布局文件的根节点所对应的 View
inflate 方法主要有下面几个步骤:
- 首先查找根节点,如果整个 XML 文件解析完毕也没看到根节点,会抛出异常
- 如果查找到的根节点名称是 merge 标签,会调用 rInflate 方法继续解析布局,最终返回 root
- 如果是其他标签(View、TextView等),会调用 createViewFromTag 生成布局根 View,并调用 rInflate 递归解析余下的子 View,添加至布局根 View 中,最后视 root 和 attachToRoot 参数的情况最终返回 view 或者 root。
root 和 attachToRoot 参数的关系:
-
root != null, attachToRoot == true:
传进来的布局会被加载成为一个 View 并作为子 View 添加到 root 中,最终返回 root;而且这个布局根节点的 android:layout_ 参数会被解析用来设置View的大小。 -
root == null, attachToRoot无用:
当 root 为空时,attachToRoot 是什么都没有意义,此时传进来的布局会被加载成为一个 View 并直接返回;布局根 View 的 android:layout_xxx 属性会被忽略。 -
root != null, attachToRoot == false:
传进来的布局会被加载成为一个 View 并直接返回。布局根 View 的 android:layout_xxx 属性会被解析成 LayoutParams 并保留。(root 只用来参与生成布局根 View 的 LayoutParams)
接下来看rInflate 和 createViewFromTag 方法。
rInflate 方法
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*/
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();
// 解析 “requestFocus” 标签,让父 View 调用 requestFocus() 获取焦点
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 {
// 调用createViewFromTag生成一个View
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 逐层递归调用rInflate,解析view嵌套的子View
rInflateChildren(parser, view, attrs, true);
// 将解析生成子View添加到上一层View中
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
// 内层子View被解析出来后,将调用其父View的“onFinishInflate()”回调
if (finishInflate) {
parent.onFinishInflate();
}
}
首先是几个特殊标签的处理,如 requestFocus、include 等,最后一个else的内容如下。
rInflate 主要是调用了 createViewFromTag 生成当前解析到的 View 节点,并递归调用 rInflate 逐层生成子 View,添加到各自的上层 View 节点中。
当某个节点下面的所有子节点 View 解析生成完成后,才会调起 onFinishInflate 回调。
所以 createViewFromTag 才是真正生成 View 的地方。
createViewFromTag 方法
/**
* Creates a view from a tag name using the supplied attribute set.
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*
* @param parent the parent view, used to inflate layout params
* @param name the name of the XML tag used to define the view
* @param context the inflation context for the view, typically the
* {@code parent} or base layout inflater context
* @param attrs the attribute set for the XML tag used to define the view
* @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
* attribute (if set) for the view being inflated,
* {@code false} otherwise
*/
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
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);
}
// 三个Factory都不存在调用LayoutInflater自己的onCreateView或者createView
// 如果View标签中没有".",则代表是系统的widget,则调用onCreateView,
// 这个方法会通过"createView"方法创建View
// 不过前缀字段会自动补"android.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;
} ...
首先会依次调用 mFactory2、mFactory 和 mPrivateFactory 三者之一的 onCreateView 方法去创建一个 View。
如果这几个 Factory 都为 null,会调用 LayoutInflater 自己的 onCreateView 或者 createView 来实例化 View。
通常情况下,自定义工厂 mFactory2、mFactory 和私有工厂 mPrivateFactory 是空的,当 Activity 继承自 AppCompatActivity 时,才会存在自定义 Factory。
所以,生成 View 的重任就落在了onCreateView和createView身上。
onCreateView 调用的其实是 createView,即 View 的节点名称没有.
时,将自动补上 android.view.
前缀(即完整类名):
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
createView 方法
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
// 缓存中不存在某View的构造方法,先new出来放缓存中
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
...
}
Object[] args = mConstructorArgs;
args[1] = attrs;
...
return view;
} catch (NoSuchMethodException e) {
...
}
通过传进来的全类名,调用 newInstance 来创建一个这个类的实例并返回,返回的这个实例就是我们需要的View了。
结合上面的递归解析过程,每个层级的节点都会被生成一个个的 View,并根据 View 的层级关系 add 到对应的直接父 View(上层节点)中,最终返回一个包含了所有解析好的子 View 的布局根 View。
加载xml布局的原理
其实就是从根节点开始,递归解析 xml 的每个节点,每一步递归的过程是:通过节点名称(全类名),使用 ClassLoader 创建对应类的实例,也就是 View,然后,将这个 View 添加到它的上层节点(父View)。并同时会解析对应 xml 节点的属性作为 View 的属性。每个层级的节点都会被生成一个个的 View,并根据 View 的层级关系 add 到对应的直接父 View(上层节点)中,最终返回一个包含了所有解析好的子 View 的布局根 View。