从XML变成View,它经历了什么? xml格式如何转换
bigegpt 2024-10-12 05:14 6 浏览
码个蛋(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 对象的方式
在 Activity中调用 setContentView(resId)
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
是一个抽象类, 抽象类是不能实例化的
先想一下 LayoutInflater 对象获取的方式
在 Activity 中通过 getLayoutInflater 获取
通过 LayoutInflater.from(context) 获取
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.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("<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)三个参数的关系了
root 不为 的时候,才会读取 xml 跟布局的 params 属性。 (这里可以解释为啥我们有时候用 LayoutInflater 加载的 xml 根标签的属性总是无效 )
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.",
"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你的学习效率
相关推荐
- 得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
-
一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...
- warm-flow新春版:网关直连和流程图重构
-
本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...
- 扣子空间体验报告
-
在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...
- spider-flow:开源的可视化方式定义爬虫方案
-
spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...
- solon-flow 你好世界!
-
solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...
- 新一代开源爬虫平台:SpiderFlow
-
SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...
- 通过 SQL 训练机器学习模型的引擎
-
关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...
- 鼠须管输入法rime for Mac
-
鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...
- Go语言 1.20 版本正式发布:新版详细介绍
-
Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...
- iOS 10平台SpriteKit新特性之Tile Maps(上)
-
简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...
- 程序员简历例句—范例Java、Python、C++模板
-
个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...
- Telerik UI for iOS Q3 2015正式发布
-
近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...
- ios使用ijkplayer+nginx进行视频直播
-
上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...
- IOS技术分享|iOS快速生成开发文档(一)
-
前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...
- macOS下配置VS Code C++开发环境
-
本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...
- 一周热门
- 最近发表
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- resize函数 (64)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- mybatis大于等于 (64)
- xcode-select (66)
- httperror403.14-forbidden (63)
- logstashinput (65)
- hadoop端口 (65)
- dockernetworkconnect (63)
- esxi7 (63)
- vue阻止冒泡 (67)
- c#for循环 (63)
- oracle时间戳转换日期 (64)
- jquery跨域 (68)
- php写入文件 (73)
- java大写转小写 (63)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)