序章:一场View的「身份窃取」迷案
想象一下,你经营着一家高级定制西装店。每个客人走进来,你都按标准流程量体裁衣------这是LayoutInflater的日常工作。
但有一天,来了一位特殊客人------AppCompatActivity。他悄悄在你的裁缝铺里安插了一位「内应」------Factory2。从此以后,每当有人下单TextView,这位内应就会偷梁换柱,把普通西装换成贴了AppCompat标签的高级定制款。
更神奇的是,客人明明在订单上写了「红色领带」(XML属性),最后做出来的却是「蓝色领带」(主题默认)。这到底是怎么回事?
今天,我们就潜入Android源码深处,揭开这场View「身份窃取」与「属性覆盖」的全部秘密。
第一章:LayoutInflater------那位勤劳的「裁缝」
1.1 他是谁?
LayoutInflater是Android系统中负责将XML布局文件「翻译」成View对象树的灵魂人物。每次你调用setContentView(R.layout.activity_main),背后都是它在默默工作。
java
// 我们平时这样用
View view = LayoutInflater.from(context).inflate(R.layout.my_layout, parent, false);
1.2 他的工作流程(简化版)
txt
XML文件 → XmlResourceParser解析 → 遍历每个标签 → 创建对应的View → 组装成View树
关键方法叫inflate(),它会把XML中的每个标签(如<TextView>、<Button>)逐个处理,最终变成内存中的Java对象。
第二章:Factory2------那个「偷梁换柱」的内应
2.1 什么是Factory2?
Factory2是LayoutInflater内部的一个接口,它允许你在View被创建之前「截胡」------自己决定如何创建这个View。
java
// LayoutInflater.java 中的两个接口
public interface Factory {
View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
// 比Factory多了一个parent参数(API 11引入)
View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
Factory2继承自Factory,只是多了一个parent参数,可以知道当前View的父容器是谁。
2.2 裁缝铺里的「内应」是怎么安插的?
AppCompatActivity在初始化时,会偷偷给LayoutInflater设置一个Factory2:
java
// AppCompatActivity内部大致逻辑
LayoutInflater layoutInflater = LayoutInflater.from(context);
LayoutInflaterCompat.setFactory2(layoutInflater, this);
这个Factory2就是AppCompatDelegate,它内部又委托给AppCompatViewInflater来干活。
2.3 内应如何「偷梁换柱」?
当XML里遇到<TextView>时,AppCompatViewInflater会检查这个标签名,然后「狸猫换太子」------返回一个AppCompatTextView代替普通的TextView:
java
// AppCompatViewInflater 的核心逻辑(简化)
if (name.equals("TextView")) {
return new AppCompatTextView(context, attrs);
}
这就是为什么你在AppCompatActivity中写<TextView>,运行时拿到的却是AppCompatTextView。
2.4 只能安插一次!------设计上的「一夫一妻制」
LayoutInflater的Factory/Factory2只能设置一次 ,再次设置会抛出异常:
java
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException(
"A factory has already been set on this LayoutInflater");
}
// ...
mFactorySet = true;
}
那如果多个地方都想设置怎么办?Android用了一个巧妙的设计------装饰器模式 ,把多个Factory合并成一个:
java
// 如果已经有Factory了,就用FactoryMerger包装起来
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
FactoryMerger像一个「责任链」,逐个尝试每个Factory,直到有一个成功创建了View。
小白理解:就像裁缝铺只能有一个「内应」,但如果来了两个都想当内应的人,老板就让他们排成一条队------第一个人先试,不行再换第二个。
第三章:View创建的「审判日」------createViewFromTag源码解析
现在来到整个流程最核心的方法:createViewFromTag。它就像裁缝铺里的「审判官」,决定每个标签最终由谁来处理。
3.1 完整源码流程
java
// LayoutInflater.java
View createViewFromTag(View parent, String name, Context context,
AttributeSet attrs, boolean ignoreThemeAttr) {
// 第一步:处理特殊标签 "view"
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// 第二步:处理主题相关(后面细说)
if (!ignoreThemeAttr) {
TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
// 第三步:彩蛋!1995年的BlinkLayout
if (name.equals(TAG_1995)) {
return new BlinkLayout(context, attrs);
}
// 第四步:【核心】Factory2/Factory/PrivateFactory 责任链
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都返回null,走默认创建流程
if (view == null) {
if (-1 == name.indexOf('.')) {
// 系统View(如"TextView"),加前缀"android.widget."
view = onCreateView(parent, name, attrs);
} else {
// 自定义View(如"com.example.MyView"),直接创建
view = createView(name, null, attrs);
}
}
return view;
}
3.2 优先级解析
| 优先级 | Factory类型 | 说明 |
|---|---|---|
| 1(最高) | mFactory2 |
最优先,能拿到parent参数 |
| 2 | mFactory |
旧版接口,无parent参数 |
| 3 | mPrivateFactory |
内部使用(如FragmentManager解析<fragment>标签) |
| 4(最低) | 默认反射创建 | 所有Factory都返回null时才走这条路 |
小白理解:审判官先问「内应1号」(Factory2)能不能做,不行就问「内应2号」(Factory),还不行就问「内部御用裁缝」(PrivateFactory),最后才自己动手(反射创建)。
第四章:android:textViewStyle------主题的「默认配方」
4.1 什么是textViewStyle?
在Android的主题(Theme)中,有一个叫textViewStyle的属性:
xml
<!-- themes.xml -->
<style name="Theme.AppCompat">
<item name="android:textViewStyle">@style/Widget.AppCompat.TextView</item>
</style>
这就像裁缝铺的「默认配方手册」------如果没有特别说明,所有TextView都按这个样式来裁制。
4.2 TextView构造函数如何读取这个「默认配方」?
java
// TextView.java
public TextView(Context context, AttributeSet attrs) {
// 注意第三个参数:com.android.internal.R.attr.textViewStyle
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
// 在这里通过 defStyleAttr 读取主题中的默认样式
// ...
}
关键点 :defStyleAttr参数告诉View:「去当前主题里找textViewStyle这个属性对应的样式,作为我的默认样式」。
4.3 属性的「四层优先级」
一个View的最终样式,由四个层级的属性共同决定:
txt
优先级从低到高:
1. 主题(Theme)中的默认值(如 textViewStyle)
2. 样式(Style)中的值
3. XML布局中的属性值
4. 代码中动态设置的值(最高)
小白理解:就像做一件衣服------先按「默认配方」(主题)裁出基本款,再按「风格手册」(样式)做调整,然后按「订单备注」(XML属性)修改,最后客人还可以现场要求改(代码设置)。后面的覆盖前面的。
第五章:为什么XML中的设置可以覆盖textViewStyle?
5.1 源码层面的真相
在View的构造函数中,通常会调用obtainStyledAttributes来获取所有属性:
java
TypedArray a = context.obtainStyledAttributes(
attrs, // XML中的属性
R.styleable.TextView,
defStyleAttr, // 主题中的textViewStyle
defStyleRes // 默认样式资源
);
obtainStyledAttributes的属性合并规则:
txt
最终值 = 按优先级合并:
1. 先从 defStyleAttr(主题中的 textViewStyle)读取默认值
2. 再从 attrs(XML中的属性)读取,如果有就覆盖
3. 最后从 defStyleRes 读取兜底值
关键 :attrs(XML属性)的优先级高于defStyleAttr(主题属性)。所以当你在XML中写了android:textColor="#FF0000",它就会覆盖主题中textViewStyle里定义的文字颜色。
5.2 验证:TextView的源码
java
// TextView.java 的构造函数中
public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 获取所有样式属性,XML中的属性会覆盖主题中的默认值
TypedArray a = context.obtainStyledAttributes(
attrs,
com.android.internal.R.styleable.TextView,
defStyleAttr, // ← 这里传入 textViewStyle
0
);
// 从 a 中读取各个属性值(XML优先于主题)
int textColor = a.getColor(R.styleable.TextView_textColor, defaultColor);
// ...
a.recycle();
}
obtainStyledAttributes内部会按照「XML属性 > 样式 > 主题默认」的顺序合并属性值,确保XML中的设置拥有最高优先级。
第六章:完整调用时序图
txt
┌─────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Activity │ │LayoutInflater│ │ Factory2 │ │ TextView │
└────┬────┘ └──────┬───────┘ └──────┬──────┘ └──────┬───────┘
│ │ │ │
│ setContentView()│ │ │
│────────────────>│ │ │
│ │ │ │
│ │ inflate() │ │
│ │ (遍历XML标签) │ │
│ │ │ │
│ │ createViewFromTag() │ │
│ │ (遇到<TextView>) │ │
│ │────────────────────>│ │
│ │ │ │
│ │ onCreateView() │ │
│ │ (返回AppCompatTV) │ │
│ │<────────────────────│ │
│ │ │ │
│ │ new AppCompatTextView(context, attrs) │
│ │─────────────────────────────────────────>│
│ │ │ │
│ │ │ 构造函数调用 │
│ │ │ this(context, │
│ │ │ attrs, textViewStyle)
│ │ │ │
│ │ │ obtainStyledAttrs()
│ │ │ (XML属性覆盖主题) │
│ │ │ │
│ │ 返回View树 │ │
│<────────────────│ │ │
│ │ │ │
流程说明:
- Activity 调用
setContentView(),触发布局加载 - LayoutInflater 开始
inflate(),逐个解析XML标签 - 遇到
<TextView>标签,调用createViewFromTag() - 检查
mFactory2不为空(AppCompatActivity已设置),调用Factory2.onCreateView() - Factory2 (实际是AppCompatViewInflater)返回
AppCompatTextView实例 AppCompatTextView构造函数中传入textViewStyle作为默认样式obtainStyledAttributes按优先级合并属性(XML覆盖主题)- 最终View树返回给Activity并显示
第七章:总结------一张图看懂全部
txt
┌─────────────────────────────────────────────────────────────────────────────┐
│ View 创建与属性覆盖全景图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ XML布局 LayoutInflater View对象 │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │<TextView │ │ inflate() │ │ TextView │ │
│ │ android: │──────────────>│ ↓ │───────────>│ 或 │ │
│ │ textColor│ │createViewFrom│ │AppCompatTV │ │
│ │ ="#FF0000"│ │ Tag() │ │ │ │
│ │/> │ └──────┬───────┘ └──────────────┘ │
│ └──────────┘ │ ▲ │
│ ↓ │ │
│ ┌─────────────────┐ │ │
│ │ Factory2 拦截 │ │ │
│ │ (AppCompatVI) │ │ │
│ └─────────────────┘ │ │
│ │ │ │
│ ↓ │ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ TextView 构造函数 │ │
│ │ this(context, attrs, R.attr.textViewStyle) ←─主题默认样式 │ │
│ │ ↓ │ │
│ │ obtainStyledAttributes(attrs, styleable, textViewStyle) │ │
│ │ ↓ │ │
│ │ 属性合并优先级:XML属性 > 样式 > 主题默认(textViewStyle) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 最终View: │ │
│ │ AppCompatTextView │ │
│ │ 文字颜色=#FF0000 │ ← XML覆盖了主题默认 │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
核心要点回顾
-
LayoutInflater是Android的布局加载引擎,负责将XML转为View对象
-
**
android:textViewStyle**是主题中的默认样式属性,作为defStyleAttr传给View构造函数 -
XML属性覆盖主题 的根源在于
obtainStyledAttributes的属性合并机制------XML中的属性优先级高于主题默认值 -
完整优先级:代码动态设置 > XML属性 > 样式(Style) > 主题默认(Theme)
一句话总结 :
LayoutInflater是裁缝,Factory2是内应负责偷梁换柱(把TextView换成AppCompatTextView),textViewStyle是主题给的默认配方,而XML中的属性就像客人的现场要求------永远比默认配方优先级高!
希望这篇文章帮你彻底搞懂了LayoutInflater、Factory2和textViewStyle的来龙去脉。如果觉得有帮助,欢迎点赞、收藏、转发!