meta-data 解析
本文承接 Manifest 解析入口、四大组件解析 和 IntentFilter 解析。前文已经说明组件解析器如何分发 child tag;这里专门追踪 <meta-data> 和 <property> 的值是怎样从 XML 进入 Property,又怎样分别落到 Bundle、组件属性 Map 和应用对象。
Android 17 的实现有一个非常重要的分叉:两个标签共用 ParsingPackageUtils.parseMetaData() 读取名称和值,但 <meta-data> 通过 Property.toBundle() 写入 Bundle,<property> 则保留为强类型 Property 并写入 Map。因而不能用“meta-data 就是一个 Map”或“property 只是 meta-data 的别名”概括整个流程。
1. 数据边界
1.1 统一解析入口
parseMetaData() 的返回类型是 ParseResult<Property>,因此调用方仍然可以根据标签决定保存形式。名称和值的类型校验集中在一个地方,存储策略则属于调用方和 owner。
1.2 owner 的层级
| Manifest 位置 | 解析调用 | 保存 owner | 运行时出口 |
|---|---|---|---|
<application><meta-data> | parseMetaData(pkg, null, ...) | ParsingPackage.metaData | ApplicationInfo.metaData |
<activity>/<service>/<receiver>/<provider><meta-data> | ParsedComponentUtils.addMetaData | 组件 metaData Bundle | 对应 ActivityInfo/ServiceInfo/ProviderInfo |
<application><property> | parseMetaData(pkg, null, ...) | ParsingPackage.properties | 通过 Property API 使用 |
组件 <property> | ParsedComponentUtils.addProperty | 组件 properties Map | 组件属性消费者 |
组件参数为 null 表示应用级数据;非 null 时,Property 会记录组件类名。这一归属信息只存在于 Property,转成 Bundle 后不会再携带 class name。
2. Property 对象
2.1 字段与类型
源码文件:frameworks/base/core/java/android/content/pm/PackageManager.java
public static final class Property implements Parcelable {
private static final int TYPE_BOOLEAN = 1;
private static final int TYPE_FLOAT = 2;
private static final int TYPE_INTEGER = 3;
private static final int TYPE_RESOURCE = 4;
private static final int TYPE_STRING = 5;
private final String mName;
private final int mType;
private final String mClassName;
private final String mPackageName;
private boolean mBooleanValue;
private float mFloatValue;
private int mIntegerValue;
private String mStringValue;
}Property 的类型和值字段是一对一关系:布尔、浮点、整数、资源 ID、字符串分别使用不同字段。TYPE_INTEGER 和 TYPE_RESOURCE 都存入 mIntegerValue,但通过 getInteger() 与 getResourceId() 区分语义。
2.2 构造函数
public Property(@NonNull String name, boolean value,
String packageName, String className) {
this(name, TYPE_BOOLEAN, packageName, className);
mBooleanValue = value;
}
public Property(@NonNull String name, float value,
String packageName, String className) {
this(name, TYPE_FLOAT, packageName, className);
mFloatValue = value;
}
public Property(@NonNull String name, int value, boolean isResource,
String packageName, String className) {
this(name, isResource ? TYPE_RESOURCE : TYPE_INTEGER, packageName, className);
mIntegerValue = value;
}
public Property(@NonNull String name, String value,
String packageName, String className) {
this(name, TYPE_STRING, packageName, className);
mStringValue = value;
}解析器不会把资源引用先解析成资源内容;android:resource 保存的是资源 ID,直到消费者需要时才通过资源系统读取。直接整数和资源 ID 的区别在构造时由 isResource 固定下来。
2.3 Bundle 转换
public Bundle toBundle(Bundle outBundle) {
final Bundle b = outBundle == null || outBundle == Bundle.EMPTY
? new Bundle() : outBundle;
if (mType == TYPE_BOOLEAN) {
b.putBoolean(mName, mBooleanValue);
} else if (mType == TYPE_FLOAT) {
b.putFloat(mName, mFloatValue);
} else if (mType == TYPE_INTEGER) {
b.putInt(mName, mIntegerValue);
} else if (mType == TYPE_RESOURCE) {
b.putInt(mName, mIntegerValue);
} else if (mType == TYPE_STRING) {
b.putString(mName, mStringValue);
}
return b;
}toBundle() 有两个细节决定了存储行为:传入 null 或 Bundle.EMPTY 时创建可写 Bundle;资源引用转换后也是 int,Bundle 本身不记录“这是资源 ID”的类型标签。因此需要资源语义的消费者应保留 Property,不能只依赖转换后的 Bundle。
3. 统一解析
3.1 名称与归属
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
public static ParseResult<Property> parseMetaData(ParsingPackage pkg,
ParsedComponent component, Resources res, XmlResourceParser parser,
String tagName, ParseInput input) {
TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestMetaData);
try {
final Property property;
final String name = TextUtils.safeIntern(
nonConfigString(0, R.styleable.AndroidManifestMetaData_name, sa));
if (name == null) {
return input.error(tagName + " requires an android:name attribute");
}
final String packageName = pkg.getPackageName();
final String className = component != null ? component.getName() : null;名称通过 nonConfigString 读取,资源引用不会被接受为属性名;safeIntern 让重复名称共享字符串实例。component 为 null 时表示 application 级标签,否则把组件完整类名写入 Property。
3.2 resource 优先
TypedValue v = sa.peekValue(R.styleable.AndroidManifestMetaData_resource);
if (v != null && v.resourceId != 0) {
property = new Property(name, v.resourceId, true, packageName, className);
} else {
v = sa.peekValue(R.styleable.AndroidManifestMetaData_value);
if (v != null) {
if (v.type == TypedValue.TYPE_STRING) {
final CharSequence cs = v.coerceToString();
final String stringValue = cs != null ? cs.toString() : null;
property = new Property(name, stringValue, packageName, className);
} else if (v.type == TypedValue.TYPE_INT_BOOLEAN) {
property = new Property(name, v.data != 0, packageName, className);
} else if (v.type >= TypedValue.TYPE_FIRST_INT
&& v.type <= TypedValue.TYPE_LAST_INT) {
property = new Property(name, v.data, false, packageName, className);
} else if (v.type == TypedValue.TYPE_FLOAT) {
property = new Property(name, v.getFloat(), packageName, className);
} else {
return input.error(tagName + " only supports string, integer, float, "
+ "color, boolean, and resource reference types");
}
} else {
return input.error(tagName + " requires an android:value "
+ "or android:resource attribute");
}
}
return input.success(property);
} finally {
sa.recycle();
}
}当 android:resource 有有效资源 ID 时优先使用它,即使同时存在 android:value 也不会进入 value 分支。value 的类型由 TypedValue.type 决定:字符串先强制转文本,布尔看 data != 0,整数范围包含颜色等整型值,浮点使用 getFloat()。不支持的类型在严格解析模式下直接返回 error;缺少 value/resource 也是 error。
3.3 宽松模式的差异
Android 17 源码仍保留 RIGID_PARSER 分支:非严格模式下,不支持的类型会记录 warning 并返回 success(null);严格模式才返回错误。调用方必须检查 getResult() 是否为 null,不能仅依据 isSuccess() 判断已有数据。
4. meta-data 存储
4.1 组件级 Bundle
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedComponentUtils.java
static ParseResult<Bundle> addMetaData(ParsedComponentImpl component,
ParsingPackage pkg, Resources resources, XmlResourceParser parser,
ParseInput input) {
ParseResult<Property> result = ParsingPackageUtils.parseMetaData(pkg, component,
resources, parser, "<meta-data>", input);
if (result.isError()) {
return input.error(result);
}
final Property property = result.getResult();
if (property != null) {
component.setMetaData(property.toBundle(component.getMetaData()));
}
return input.success(component.getMetaData());
}每个 <meta-data> 标签产生一个 Property,然后合并进同一个组件 Bundle。第一次写入时,component.getMetaData() 返回 Bundle.EMPTY,toBundle() 会创建可写 Bundle;后续标签复用已有 Bundle。相同 name 再次写入时,Bundle.put* 按键覆盖旧值,解析器没有额外的重复键错误。
4.2 应用级延迟复制
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
case "meta-data":
// application meta-data is stored off to the side, so it can
// remain null in the primary copy (we like to avoid extra copies
// because it can be large)
ParseResult<Property> metaDataResult = parseMetaData(pkg, null /*component*/,
res, parser, "<meta-data>", input);
if (metaDataResult.isSuccess() && metaDataResult.getResult() != null) {
pkg.setMetaData(metaDataResult.getResult().toBundle(pkg.getMetaData()));
}
return metaDataResult;应用级 metadata 不经过组件 addMetaData(),而由 ParsingPackageUtils 直接写入 package 的 Bundle。注释解释了“延迟加载”的真实含义:metadata 放在主对象的旁侧字段中,避免在主副本中复制可能很大的 Bundle;并不是每次读取 ApplicationInfo 时重新解析 XML。
4.3 空 Bundle 语义
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
@Nullable
private Bundle metaData;
@NonNull
@Override
public Bundle getMetaData() {
return metaData == null ? Bundle.EMPTY : metaData;
}字段允许为 null 以节省空间,但 getter 对解析器和消费者提供非 null 视图。这个设计使 toBundle(component.getMetaData()) 可以安全地追加数据,同时保留“尚未产生 metadata”这一内部状态。
5. property 存储
5.1 组件属性 Map
static ParseResult<Property> addProperty(ParsedComponentImpl component,
ParsingPackage pkg, Resources resources, XmlResourceParser parser,
ParseInput input) {
ParseResult<Property> result = ParsingPackageUtils.parseMetaData(pkg, component,
resources, parser, "<property>", input);
if (result.isError()) {
return input.error(result);
}
final Property property = result.getResult();
if (property != null) {
component.addProperty(property);
}
return input.success(property);
}与 meta-data 的关键差异是没有 toBundle()。组件保留 Property 的类型、资源 ID、包名和 class name,后续可通过 getProperties() 按名称读取。
5.2 Map 的覆盖规则
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
@NonNull
private Map<String, Property> mProperties = emptyMap();
public void addProperty(@NonNull Property property) {
this.mProperties = CollectionUtils.add(this.mProperties,
property.getName(), property);
}
@NonNull
public Map<String, Property> getProperties() {
return mProperties;
}CollectionUtils.add(map, key, value) 以名称为 key,因此同名 property 的后一个值替换前一个值。与 Bundle 一样,源码没有把重复 name 作为解析错误;差别在于 Map 保留强类型对象。
5.3 应用级 property
应用级 <property> 使用同一个解析函数,但 owner 是 PackageImpl:
case "property":
ParseResult<Property> propertyResult = parseMetaData(pkg, null /*component*/,
res, parser, "<property>", input);
if (propertyResult.isSuccess()) {
pkg.addProperty(propertyResult.getResult());
}
return propertyResult;PackageImpl 使用 Map<String, PackageManager.Property> 保存应用属性,并在 hideAsParsed() 后冻结为不可变 Map。解析宽松模式返回 success(null) 时,addProperty(null) 会直接忽略,因此不会把空对象写入包。
6. 组件分发
6.1 组件分发
组件解析器把 metadata 和 property 当作普通 child tag 分发:
if (parser.getName().equals("meta-data")) {
result = ParsedComponentUtils.addMetaData(activity, pkg, resources, parser, input);
} else if (parser.getName().equals("property")) {
result = ParsedComponentUtils.addProperty(activity, pkg, resources, parser, input);
}Service 和 Provider 使用同一组 ParsedComponentUtils 方法。Receiver 复用 Activity 的解析函数,所以四类组件的 metadata owner 都是对应的 ParsedComponentImpl 实例;应用级标签才由 package parser 直接处理。
6.2 解析时序
parseMetaData() 只负责构造 Property;真正决定保存形态的是 C 和 O。这也是排查“解析成功但运行时没有值”时的分界点:先检查 owner 是否写入,再检查对外生成是否携带 GET_META_DATA。
7. 对外生成
7.1 ApplicationInfo
源码文件:frameworks/base/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
private static void updateApplicationInfo(ApplicationInfo ai, long flags,
PackageUserState state, AndroidPackage pkg, int userId) {
if ((flags & PackageManager.GET_META_DATA) == 0) {
ai.metaData = null;
}
}ApplicationInfo.metaData 是否保留由调用方 flags 决定。解析阶段即使已经构建了 package Bundle,未请求 GET_META_DATA 时对外对象仍清空该字段。
7.2 组件 Info
if ((flags & PackageManager.GET_META_DATA) != 0) {
var metaData = a.getMetaData();
// Backwards compatibility, coerce to null if empty
ai.metaData = metaData.isEmpty() ? null : metaData;
} else {
ai.metaData = null;
}Activity、Service、Provider 和其他 PackageItemInfo 子类都遵循同样规则:请求 flag 才复制 Bundle;空 Bundle 为兼容性转换成 null。这里是“延迟加载”在 API 层的具体表现:不是延迟解析,而是按查询 flags 延迟决定是否把已解析数据复制给返回对象。
7.3 Property 出口
PackageInfoUtils 的 GET_META_DATA 分支只处理 Bundle 字段,不会把 Property Map 自动转成 Bundle。使用 property 的代码必须从 ParsedComponent.getProperties() 或 AndroidPackage.getProperties() 取得强类型对象;这也是保留 resource/string/boolean 等类型信息的原因。
8. 消费者路径
8.1 解析后立即消费
部分解析后处理逻辑直接读取 metadata,并把它转换成组件字段:
if (!isAlias && activity.getLaunchMode() != LAUNCH_SINGLE_INSTANCE_PER_TASK
&& activity.getMetaData().containsKey(
ParsingPackageUtils.METADATA_ACTIVITY_LAUNCH_MODE)) {
final String launchMode = activity.getMetaData().getString(
ParsingPackageUtils.METADATA_ACTIVITY_LAUNCH_MODE);
if (launchMode != null && launchMode.equals("singleInstancePerTask")) {
activity.setLaunchMode(LAUNCH_SINGLE_INSTANCE_PER_TASK);
}
}这说明 metadata 不只是被动地返回给应用:解析完成后,PMS 自己会读取约定 key,并把 Bundle 值派生到 ParsedActivity 字段。类似逻辑还包括应用级 max aspect ratio、supports size changes、远程设备显示能力和窗口布局 affinity。
8.2 运行时查询
应用通过 PackageManager 获取 ApplicationInfo 或组件 Info 时,是否能看到 metadata 取决于查询 flags;PMS 内部派生逻辑则在 package freeze 前后直接读取解析对象。两条路径不要混为同一个消费者。
9. 错误与兼容
9.1 失败条件
| 条件 | 代码行为 | owner 状态 |
|---|---|---|
缺少 android:name | input.error | 不写入 |
同时无 android:value 和 android:resource | input.error | 不写入 |
| value 类型不支持且严格模式 | input.error | 不写入 |
| value 类型不支持且宽松模式 | warning + success(null) | 调用方忽略 |
| 合法 resource | 构造 TYPE_RESOURCE | 保存资源 ID |
| 合法 value | 按 TypedValue 类型构造 | 保存强类型值 |
9.2 失败传播
组件 addMetaData/addProperty 在 result.isError() 时返回 input.error(result),外层组件解析循环随后终止。application parser 则直接返回 metaDataResult 或 propertyResult,由上层 parseBaseApplication 统一处理。因此错误不会被悄悄转换成空 Bundle;只有宽松模式的不支持类型才可能出现 success(null)。
9.3 恢复与清理
单个标签失败时,调用方不会提交该 Property;已经成功写入前序标签的 Bundle/Map 不会在 parseMetaData 内回滚,因为解析结果最终会使整个组件或包失败。包替换/卸载时,旧的 ParsedPackage 和 resolver 引用整体被替换或移除,metadata 不存在独立的运行时清理队列。
10. 测试与诊断
10.1 输入与断言
| 输入 | 断言 | 覆盖范围 |
|---|---|---|
android:value="text" | Property.isString() 且 Bundle 为 String | 字符串路径 |
android:value="true" | isBoolean() 且值为 true | 布尔转换 |
| 整数/颜色 | isInteger() 且 getInteger() 正确 | 整型范围 |
| 浮点值 | isFloat() 且 getFloat() 正确 | 浮点转换 |
android:resource="@..." | isResourceId() 且保存资源 ID | resource 优先 |
| 缺少 name | ParseResult error | 必需属性 |
| 缺少 value/resource | ParseResult error | 值必需 |
| 两个同名 meta-data | Bundle 后者覆盖前者 | Bundle owner |
| 两个同名 property | Map 后者覆盖前者 | Property owner |
不带 GET_META_DATA 查询 | Info.metaData 为 null | 对外 flags |
测试不能只验证 ApplicationInfo.metaData,否则会遗漏 Property 类型、组件 class name、重复键覆盖和 flags 清空行为。
10.2 现场排查顺序
- 确认 XML 标签位于 application 还是组件 child 位置。
- 在
parseMetaData()检查 name、resource/value 和TypedValue.type。 - 判断调用方走
toBundle()还是addProperty()。 - 检查 owner 的 Bundle/Map 是否已写入,空 Bundle 是否只是
Bundle.EMPTY视图。 - 若是 PackageManager API 返回为空,确认查询 flags 是否包含
GET_META_DATA。 - 若是 PMS 内部行为异常,搜索对应
METADATA_*常量的派生逻辑,而不是只看最终 Info。
11. 源码路线
建议按以下顺序阅读:
ParsingPackageUtils.parseMetaData():名称、resource/value 和类型分支。ParsedComponentUtils.addMetaData()/addProperty():Bundle 与 Property Map 的分叉。ParsedComponentImpl:Bundle.EMPTY 视图、属性 Map 和 Parcelable 字段。ParsingPackageUtils.parseBaseAppChildTag():application 级旁侧存储。PackageManager.Property.toBundle():类型信息如何映射到 Bundle。PackageInfoUtils:GET_META_DATA如何决定对外复制。ParsingPackageUtils与ParsedActivityUtils中的METADATA_*消费点:解析结果如何派生组件字段。
12. 设计收束
完整状态链可以压缩成下面四步:
XML meta-data/property
-> Property(name, type, value, package/class owner)
-> meta-data: Bundle;property: Map<String, Property>
-> package/component 派生逻辑与 PackageInfoUtils 输出真正需要记住的不是标签名称,而是 owner 和生效时机:parseMetaData() 只造对象;addMetaData() 才把类型值压入 Bundle;addProperty() 保留强类型 Map;应用级 Bundle 为避免复制放在 package 旁侧;GET_META_DATA 决定对外对象是否携带 Bundle;PMS 自己的 METADATA_* 逻辑则可能在对外生成前把值转换成组件字段。掌握这条链,后续阅读 PackageCacher、Split APK 和应用级兼容策略时,才能判断 metadata 是被保存、被消费,还是只是在查询接口中被裁剪了。
