java xml转对象_从XML变成View,它经历了什么?

235e13ba14c7f162277f9cdd18996b7d.gif

码个蛋(codeegg)第 626 次推文

作者:看我眼前007

原文:https://www.jianshu.com/p/eccd8ba87e8b

友情提示:

本文篇幅较长,建议在电脑上慢慢阅读~

内容基于 Android API 26 Platform 源码。

写作背景

Android 开发框架中,使用 Xml 文件描述 Ui 页面,通过setContentView(resId)或者LayoutInflater.inflate(resId,……)的方式把 Xml 文件描述的页面转换成 Java 对象。Xml 文件加上 AndroidStudio 提供的预览功能,使得 Android 开发过程中页面和业务逻辑可以并行开发,极大地提高了开发效率。

但是大部分 Android 工程师对 xml 文件如何转换成 Java 不是十分了解,本文将带大家一起探究 View 从 xml 文件到 Java 对象的转换过程

xml 转成成 Java 对象有几种方式?

我们先罗列一下 xml 转换成 Java 对象的方式

  1. 在 Activity中调用 setContentView(resId)

  2. LayoutInflater.from(context).inflate(resId,……)

跟踪一下 Activity.setContentView(resId)

我们一般在项目使用的 Activity 可能是

1. android.support.v7.app.AppCompatActivity
2. android.support.v4.app.FragmentActivity
3. android.app.Activity
4. 其他 Activity

所有的 Activity 都是 android.app.Activity 的子类。

但是!每个继承 android.app.Activity 的子类 setContentView(resId) 实现方式都被重载了。我们这里先看最基础的 android.app.Activity

public void setContentView(@LayoutRes int layoutResID) {
getWindow.setContentView(layoutResID);
initWindowDecorActionBar;
}

查看一下 getWindow源码

public Window getWindow {
return mWindow;
}

全局搜索 mWindow 对象赋值的地方找到以下代码

mWindow = new PhoneWindow(this, window, activityConfigCallback);

这里 PhoneWindow 的源码在 sdk 里面是隐藏的,我们去 androidxref ->PhoneWindow.java 查看 PhoneWindow.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 == ) {
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 != && !isDestroyed) {
cb.onContentChanged;
}
}

当我们没有设置转场动画的时候会执行

mLayoutInflater.inflate(layoutResID, mContentParent);

在 PhoneWindow 的构造函数中我们找到了 mLayoutInflater 对象赋值语句

public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}

所以我们得出一个结论

Activity.setContentView(resId) 最终还是使用

LayoutInflater.from(context).inflate(resId, ……)

再回头看下

android.support.v7.app.AppCompatActivity

android.support.v4.app.FragmentActivity

我们发现

android.support.v4.app.FragmentActivity没有重载

android.app.Activity.setContentView(resId),

但是 android.support.v7.app.AppCompatActivity重载了

@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate.setContentView(layoutResID);
}

再跟踪一下源代码我们发现最终会调用到

android.support.v7.app.AppCompatDelegateImplV9.setContentView(resId)

@Override
public void setContentView(int resId) {
ensureSubDecor;
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews;
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged;
}

这里我们又发现了 LayoutInflater的身影。

这里我们可以总结一下:

xml 转成成 Java对象是通过 LayoutInflater的inflate 方法来完成的。

LayoutInflater对象实例化

看一下 LayoutInflater的源码第一行

public abstract class LayoutInflater {……}

LayoutInflater是一个抽象类, 抽象类是不能实例化的

a29d68fe685bb2be697e262dcabd87b2.png 18e8592983666da11ce7c65882d12839.png

先想一下 LayoutInflater 对象获取的方式

  1. 在 Activity 中通过 getLayoutInflater 获取

  2. 通过 LayoutInflater.from(context) 获取

  3. context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) 获取

看 Activity 的 getLayoutInflater

public LayoutInflater getLayoutInflater {
return getWindow.getLayoutInflater;
}

这里我们就可以看出 Activity 通过 getLayoutInflater 获取的是 PhoneWindow 的 mLayoutInflater (如果忘记了可以往上翻一下,或者去参考资料的链接里找找源码)

再看一下 LayoutInflater.from(context)

public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == ) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}

此时,我们必须请出柯南君帮我们宣布

1d065edfda52ef46194960b5e073a8a6.png

真相只有一个!最终都是通过服务获取 LayoutInflater实例对象。

下一步, 源码追踪
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
这里先说明一个前提,context 的实现类是 ContextImpl

所以我们直接查看:

ContextImpl.getSystemService(Context.LAYOUT_INFLATER_SERVICE)

@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}

继续跟踪 SystemServiceRegistry

public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != ? fetcher.getService(ctx) : ;
}

这时候我们在 SystemServiceRegistry 类停留一下,发现这里似乎只注册各种系统服务的地方。

我们找到了

Context.LAYOUT_INFLATER_SERVICE注册代码。

static {
……
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext);
}});
……
}
private static void registerService(String serviceName, Class serviceClass,
ServiceFetcher serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}

然后我们终于找到 LayoutInflater的实现类是 PhoneLayoutInflater

此时我们可以休息一下,喝口水,上个卫生间,进入下个阶段~

读取xml文件并创建View对象

LayoutInflater.inflate
public View inflate(@LayoutRes int resource, @able ViewGroup root)
public View inflate(@LayoutRes int resource, @able ViewGroup root, boolean attachToRoot)

再去源码查看一下,发现两个方法其实只有一个方法是核心,另一个只是做了一下封装,让我们少传入一个参数。

public View inflate(@LayoutRes int resource, @able ViewGroup root) {
return inflate(resource, root, root != );

}

所以我们重点看一下 inflate(@LayoutRes int resource, @able ViewGroup root, boolean attachToRoot) 的源码

public View inflate(@LayoutRes int resource, @able ViewGroup root, boolean attachToRoot) {
final Resources res = getContext.getResources;
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
+ Integer.toHexString(resource) + ")");
}

final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close;
}
}

我们看到首先通过 res 对象把 resId 指向的 xml 文件转换为 XmlResourceParser 然后执行 inflate(parser, root, attachToRoot) 方法,该方法比较长,这里只贴出核心步骤。

public View inflate(XmlPullParser parser, @able ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {

final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;

try {
……

if (TAG_MERGE.equals(name)) {
if (root == || !attachToRoot) {
throw new InflateException(" 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 = ;

if (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);
}
}
……

// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);

// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != && attachToRoot) {
root.addView(temp, params);
}

// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == || !attachToRoot) {
result = temp;
}
}

} ……
省略异常处理部分
……

return result;
}
}

以上步骤还是很长,我们将拆分几部分分析。

第一部分
 if (TAG_MERGE.equals(name)) {
if (root == || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}

rInflate(parser, root, inflaterContext, attrs, false);

}

如果 xml 根标签是 merge,则 root 不能为空, attachToRoot 必须是 true。

然后执行 rInflate(parser, root, inflaterContext, attrs, false)

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(" cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException(" 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;
}
}

上面这个方式我们需要重点记一下

1. 遍历该节点的子节点
2. 子节点有 "requestFocus"、"tag"、""、"include"
3. 子节点不能是 "merge"
4. 子节点的其他情况,则是各种 View 的标签
5. View 标签和 "include" 标签会创建 View 对象
6. 遍历结束以后执行 parent.onFinishInflate

如果子节点是 include,则执行 parseInclude。

parseInclude的源码和 inflate(parser, root, attachToRoot)类似,都是读取xml对应的文件,转换成 XmlResourceParser 然后遍历里的标签。

经过层层调用,我们可以找到最终创建 View 的代码

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);

第一部分代码,我们的到的结论是,

createViewFromTag(parent, name, context, attrs)负责创建 View 对象。

第二部分
 // Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = ;

if (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);
}
}
……

// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);

// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != && attachToRoot) {
root.addView(temp, params);
}

// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == || !attachToRoot) {
result = temp;
}

因为这里排除了merge标签,这里的根标签肯定是一个 View,所以调用了 createViewFromTag(root, name, inflaterContext, attrs)方法创建 View 。

再次印证了第一部分得出的结论 createViewFromTag(parent, name, context, attrs)负责创建 View 对象。

然后看下后面的代码我们就明白

inflate(@LayoutRes int resource, @able ViewGroup root, boolean attachToRoot)三个参数的关系了

  1. root 不为 的时候,才会读取 xml 跟布局的 params 属性。 (这里可以解释为啥我们有时候用 LayoutInflater 加载的 xml 根标签的属性总是无效 )

  2. attachToRoot 为 True ,返回的是 root 对象。否则返回的是 xml 创建的根标签指定的 View

创建 View 对象

通过上面的判断我们终于找到了最最核心的方法 createViewFromTag

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}

有包裹了一层,并且把 ignoreThemeAttr 设置为 false,表示这里会收到 Theme 的影响。

我们在 createViewFromTag(parent, name, context, attrs, false) 中找到了创建 View 的代码

 View view;
if (mFactory2 != ) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != ) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = ;
}

if (view == && mPrivateFactory != ) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

if (view == ) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, , attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}

return view;

这里又出现了 mFactory2、mFactory、mPrivateFactory 三个对象,似乎都是可以创建 View 。 对于android.app.Activity来说,这三个对象为 或者空实现(下一节会讲这个) 所以我们直接看

 final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, , attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}

这里需要说明一下,如果 name属性里面含有 . 表示这是一个自定义 View,系统自带 View 我们可以省略类的路径,而自定义 View 则不能省略。

对于自定义 View 的创建,这里省略了大部分代码

public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor extends View> constructor = sConstructorMap.get(name);
……
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]));
}
mConstructorArgs[0] = lastContext;
return view;

} ……
}

仅仅看到 constructor.newInstance(args),我们已经明白这里使用了 反射创建 View 对象。

而对于 Android 内置的各种 View, 我们在 LayoutInflater 的实现类 PhoneLayoutInflater中找到了重载

/**
* @hide
*/
public class PhoneLayoutInflater extends LayoutInflater {
private static final String sClassPrefixList = {
"android.widget.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值