您的位置 首页 java

从XML变成View,它经历了什么?

码个蛋(codeegg)第 626 次推文

作者:看我眼前007

原文:

友情提示:

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

内容基于 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 对象的方式

跟踪一下 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 是一个抽象类, 抽象类是不能实例化的

先想一下 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;
}

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

真相只有一个!最终都是通过服务获取 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<LayoutInflater> {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext);
}});
……
}

private static <T> void registerService(String serviceName, Class<T> serviceClass,
ServiceFetcher<T> 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("<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.Layout params 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("<merge /> 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("<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;
}
}

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

 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 . newInstanc e(args) ,我们已经明白这里使用了 反射创建 View 对象。

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

 /** 
* @hide
*/
public class PhoneLayoutInflater extends LayoutInflater {
private static final String sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};

@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != ) {
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);
}
}

再看下 LayoutInflater 中的代码

 protected View onCreateView(View parent, String name, AttributeSet attrs) 
throws ClassNotFoundException {
return onCreateView(name, attrs);
}

protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}

我们可以看到, 对于系统内置的 View,会依次在 View 的标签前面加上

“android.widget.”、”android.webkit.”、”android.app.” 、”android.view.”

然后通过 反射 的方法 创建 View

(文章略有删减,可点击原文查看~)

到此 ,Xml 到 View 对象的转换过程全部结束~~~

看到这里的童鞋很辛苦!这项技能get了吗?

近期文章:

  • 能实现爆炸效果的SpannableString

  • 大家快来看看404的兄弟姐妹

  • 我方卧底发自美团的真实Android资料

今日问题:

谁能来画个过程图?

画出来有信心的丢到码仔的学习群来,码仔给你发红包!

快来码仔社群解锁新姿势吧!社群升级:Max你的学习效率

文章来源:智云一二三科技

文章标题:从XML变成View,它经历了什么?

文章地址:https://www.zhihuclub.com/193091.shtml

关于作者: 智云科技

热门文章

发表回复

您的电子邮箱地址不会被公开。

网站地图