这篇文章主要是为了解决一个疑惑“layout中的XML文件是如何渲染成View的”。
一、XML2View§
下面是一种加载view的实现,几乎所有的Android开发都有写过。
LayoutInflater layoutInflater = LayoutInflater.from(context);
View layout = layoutInflater.inflate(R.layout.main_fragment, parent, false);
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
}
@Override
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 {
/// 最后通过这里将xml转换成view
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
在 PhoneWindow#setContentView 中,通过 LayoutInflater#inflate 方法将layout布局生成View对象
二、LayoutInflater§
读懂一个庞大的类的最好方法往往是通过阅读注释 Instantiates a layout XML file into its corresponding {@link android.view.View} objects.
To create a new LayoutInflater with an additional {@link Factory} for your own views, you can use {@link #cloneInContext} to clone an existing ViewFactory, and then call {@link #setFactory} on it to include your Factory.
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
LayoutInflater 一共有四个 inflate 方法,第一处 inflate 方法会调用到第三处的 inflate 方法,先获取 Resources 对象,最终调用第二处方法,最后调用到如下方法
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 {
advanceToRootNode(parser);
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)) {
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
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(
getParserStateDescription(inflaterContext, attrs)
+ ": " + 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;
}
}
梳理入上逻辑,可以得到以下大致流程:
-
先解析XML文件中Attribute属性,保存在属性集中;
-
遍历查询到第一个跟节点,如果是
,则把父容器作为父布局参数,反之,获取到根view,则把根view作为父布局参数,走rInflateChildren逻辑; -
根据父容器不为是否为空和是否需要附着在父容器来返回不同结果。
基于上述流程,解析第一个根节点的逻辑基础上可以猜测:rInflateChildren的处理逻辑应该是递归处理根节点下的节点树并解析成对应的View树,放在父布局中。
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
此处表明,rInflateChildren函数本质上是调用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();
}
}
在上段代码中就是具体实现了,主要流程:
- 对特殊标签做处理;
- rInflate函数主要逻辑,通过递归方法来解析传入的数据;
这里的递归什么时候结束?§
<?xml version="1.0" encoding="utf-8"?> <!-- depth=0 -->
<LinearLayout> <!-- depth=1 -->
<TextView> <!-- depth=2 -->
</TextView> <!-- depth=2 -->
<LinearLayout> <!-- depth=2 -->
<TextView/> <!-- depth=3 -->
</LinearLayout> <!-- depth=2 -->
</LinearLayout> <!-- depth=1 -->
这里有个例子,在递归子节点树的时候实际上外层已经解析过了,此处只是演示XmlPullParser的大致解析逻辑。
-
第一次调用rInflate函数,depth=0,解析第一个
,为START_TAG,实例化LinearLayou对象L1,触发第一次rInflate递归。 -
depth=1,解析到第一个
,为START_TAG,实例化TextView对象,并把自己添加到L1,触发第二次rInflate递归 。 -
depth=2,解析到第一个,为END_TAG不满足while条件,结束第二次递归 。
-
回到第一次递归while循环,depth=2,解析第一个
,为START_TAG,实例化LinearLayout对象L2,触发第三次递归。 -
depth=2,解析到
,实际上这种写法等同于第一个TextView,对于解析器而言,都是按照START_TAG -> TEXT -> END_TAG的解析逻辑。解析到第二个TextView,并把自己添加到L2,触发第四次递归 。 -
depth=3,不满足while条件,结束第四次递归 。
-
depth=2,解析到第一个,为END_TAG不满足while条件,结束第三次递归 。
-
depth=1,解析到第二个,为END_TAG不满足while条件,结束第一次递归 。
三、递归中怎么创建View?§
上述逻辑解释了如何解析XML,同时诞生了一个问题:递归中是如何创建View的? 可以看到代码中通过createViewFromTag将Tag转化为View。
// name即获取当前节点的名称,比如<TextView>获取的是TextView
final String name = parser.getName();
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
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) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
通过上述代码:
-
走了name的转化流程,如果name是View,则需要转化为对应的class属性值;
-
name转换成View,如果name是blink,则直接构建BlinkLayout返回,否则先让工厂生成,如果工厂无法生成,就使用默认生成。既然mFactory、mFactory2、mPrivateFactory是优先构建View对象,所以有必要了解这些是什么。
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
@Nullable
View onCreateView(@NonNull String name, @NonNull Context context,
@NonNull AttributeSet attrs);
}
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent The parent that the created view will be placed
* in; <em>note that this may be null</em>.
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
@Nullable
View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs);
}
实质上,只是一个暴露给构建View的接口方法,而接口对象赋值是在构造器内和set方法中。LayoutInflate是一个抽象类,所以先检查其实现类,通过源码检索,可以看到实现继承LayoutInflater的类一共有两个
-
AsyncLayoutInflater.java的私有静态内部类BasicInflate
-
PhoneLayoutInflater:Android系统默认实现的LayoutInflater对象,也就是我们经常获取的LayoutInflater对象。
public class PhoneLayoutInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
public PhoneLayoutInflater(Context context) {
super(context);
}
protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
}
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
public LayoutInflater cloneInContext(Context newContext) {
return new PhoneLayoutInflater(this, newContext);
}
}
整个实现只有一处被外部调用,外部引用比较有趣的是Activiy中的处理。
@Override
public LayoutInflater onGetLayoutInflater() {
final LayoutInflater result = Activity.this.getLayoutInflater();
if (onUseFragmentManagerInflaterFactory()) {
return result.cloneInContext(Activity.this);
}
return result;
}
既然实现类没有处理Factory,只能看setFactory和setFactory2方法
public void setFactory(Factory factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = factory;
} else {
mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
}
}
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
@UnsupportedAppUsage
public void setPrivateFactory(Factory2 factory) {
if (mPrivateFactory == null) {
mPrivateFactory = factory;
} else {
mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
}
}
private static class FactoryMerger implements Factory2 {
private final Factory mF1, mF2;
private final Factory2 mF12, mF22;
FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
mF1 = f1;
mF2 = f2;
mF12 = f12;
mF22 = f22;
}
@Nullable
public View onCreateView(@NonNull String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
View v = mF1.onCreateView(name, context, attrs);
if (v != null) return v;
return mF2.onCreateView(name, context, attrs);
}
@Nullable
public View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs) {
View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
: mF1.onCreateView(name, context, attrs);
if (v != null) return v;
return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
: mF2.onCreateView(name, context, attrs);
}
}
Factory工厂不能为空且只能设置一次,并且两个set方法的调用是互斥的,如果开发者希望设置自定义的工厂,则需要从原来的LayoutInflater中复制一个对象,然后调用setFactory或是setFactory2方法设置解析工厂,另外setPrivateFactory是系统底层调用的,所以不开放对外设置。
那么setFactory或者setFactory2在android中如何设置的呢?这些类可在v4包中查阅到。
setFactory的调用来自于BaseFragmentActivityGingerbread.java和LayoutInflaterCompatBase.java。
-
BaseFragmentActivityGingerbread.java处理低于3.0版本的activity版本都需要设置解析fragmen标签
-
LayoutInflaterCompatBase.java,则暴露FactoryWrapper分装类,根据不同的版本实现不同的LayoutInflaterFactory。常规的业务比如换肤,即可在这里实现自己的Factory达到View换肤效果。
setFactory2的调用来自于LayoutInflaterCompatHC.java和LayoutInflaterCompatLollipop!因此我们有理由相信,HC版本(3.0)和Loolopop版本(5.0)必定需要兼容解析一些布局,哪些呢?2333,自己想想。其中LayoutInflaterCompatHC.java实际上内部实现了一个静态的FactoryWrapperHC类,该类继承1-2中的FactoryWrapper类,用来提供HC版本的Factory而已。对于LayoutInflaterCompatLollipop,基本逻辑也是如此。
@Nullable
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
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
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs)
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs)
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs) + ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
上述代码可知,LayoutInflater内存缓存了构造器,过滤部分tag后通过类名反射View对象。 其中4处给我们的启示是,实际上系统PhoneLayoutInflater只是扩展了类名的路径,让LayoutInflater识别更多的类而已。如果正真想要处理View定制样式,还是需要自定义LayoutInflater.Factory2。 最后递归XML文件得到的标签名来转化成View的优先级为:mFactory2 > mFactory > mPrivateFactory > onCreateView。拼接类名并经过剔除筛选后反射得到View对象。