packages.xml 恢复
本文面向已经读过 增量扫描、扫描冲突处理 和 ScanRequest 与 Result 的读者。前文使用 VersionInfo、disabled system package、mExpectingBetter 和 PackageSetting 解释启动状态与冲突选择;本文回到这些状态的持久化来源,追踪 packages.xml 如何读取、主文件损坏时如何切换副本、包与 shared user 如何恢复,以及目录扫描完成后哪些磁盘状态仍需要重新对照。
Android 17 的 packages.xml 不是“读一个 XML 就结束”。Settings.readSettingsLPw() 负责全局包设置、版本信息、签名、KeySet、shared user、disabled system package 等状态;每用户的 package-restrictions.xml 和 runtime permissions 文件由后续逻辑单独读取;PMS 扫描磁盘 APK 后还会用 code path、版本、签名和 flags 对持久化状态进行校正。主文件可恢复,不等于 APK 一定存在;APK 扫描成功,也不等于所有用户状态都已恢复。
读完后,读者应能从 PackageManagerService 找到 mFirstBoot 的真实来源;从 Settings.getSettingsFile() 看懂主文件、backup 和 reserve copy 的角色;从 readSettingsLPw() 追到 pending packages、KeySet、disabled system package 和 runtime permissions;还能根据错误日志判断问题发生在 XML 读取、每用户限制、磁盘扫描、设置校正还是最终写回。
1. 持久化边界
1.1 文件分工
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
| 文件/状态 | owner | 内容 | 读取入口 |
|---|---|---|---|
packages.xml | Settings | 全局包、VersionInfo、签名、KeySet、shared user、disabled system package | readSettingsLPw() |
packages-backup.xml | 原子写入恢复 | 写入期间的旧主文件 | ResilientAtomicFile.openRead() |
packages.xml.reservecopy | 长期保留副本 | 成功写入后的保留副本 | ResilientAtomicFile.openRead() |
package-restrictions.xml | Settings/用户状态 | per-user enabled、stopped、installed、suspend 等 | readPackageRestrictionsLPr() |
| runtime permissions 文件 | RuntimePermissionPersistence | 每用户 runtime grant 状态 | readStateForUserSync() |
packages.list | Settings | 供其他组件使用的轻量包列表 | 写回阶段生成 |
packages.kmap/configfs | Settings | package→uid/user 映射 | writeKernelMappingLPr() |
packages.xml 恢复的是“包管理数据库”的全局部分;用户安装状态和 runtime permission 不是同一个 XML。排查“包存在但用户看不到”时,必须同时检查全局 package setting 和 user restrictions。
1.2 Settings 路径
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
Settings(File dataDir, RuntimePermissionsPersistence runtimePermissionsPersistence,
LegacyPermissionDataProvider permissionDataProvider,
DomainVerificationManagerInternal domainVerificationManager,
Handler handler, PackageManagerTracedLock lock) {
mSystemDir = new File(dataDir, "system");
mSystemDir.mkdirs();
FileUtils.setPermissions(mSystemDir.toString(),
FileUtils.S_IRWXU | FileUtils.S_IRWXG
| FileUtils.S_IROTH | FileUtils.S_IXOTH,
-1, -1);
mSettingsFilename = new File(mSystemDir, "packages.xml");
mSettingsReserveCopyFilename = new File(mSystemDir, "packages.xml.reservecopy");
mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml");
mPackageListFilename = new File(mSystemDir, "packages.list");
FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID);
mStoppedPackagesFilename = new File(mSystemDir, "packages-stopped.xml");
mBackupStoppedPackagesFilename = new File(mSystemDir, "packages-stopped-backup.xml");
}Android 17 的 Settings 不在构造函数中创建 ResilientAtomicFile 成员,而是在 getSettingsFile() 中用三个文件名构造:主文件、previous/backup、reserve copy。packages-stopped.xml 是旧格式迁移输入;正常用户状态使用每用户目录下的 package-restrictions.xml。
2. 原子恢复
2.1 readLPw 入口
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java、frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
// PackageManagerService
t.traceBegin("read user settings");
mFirstBoot = !mSettings.readLPw(computer,
mInjector.getUserManagerInternal().getUsers(/* excludeDying= */ false));
t.traceEnd();PMS 只负责把读取结果转换成 mFirstBoot;真正的主文件选择、XML 解析和恢复由 Settings 持有。下面进入 Settings 的全局读取实现。
// Settings
boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) {
final ArrayMap<String, Long> originalFirstInstallTimes = new ArrayMap<>();
try {
if (!readSettingsLPw(computer, users, originalFirstInstallTimes)) {
return false;
}
} finally {
if (!mVersion.containsKey(StorageManager.UUID_PRIVATE_INTERNAL)) {
Slog.wtf(PackageManagerService.TAG,
"No internal VersionInfo found in settings, using current.");
findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
}
if (!mVersion.containsKey(StorageManager.UUID_PRIMARY_PHYSICAL)) {
Slog.wtf(PackageManagerService.TAG,
"No external VersionInfo found in settings, using current.");
findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
}
}
// pending packages, restrictions, permissions and disabled users follow
return true;
}mFirstBoot 只由 readLPw() 的返回值决定;mIsUpgrade 是稍后基于 VersionInfo fingerprint 计算的另一个状态。readLPw() finally 中补齐 internal/external VersionInfo,保证即使 XML 缺少 version 节点,后续扫描也有可用的当前基线。
2.2 主文件与副本
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
private ResilientAtomicFile getSettingsFile() {
return new ResilientAtomicFile(mSettingsFilename, mPreviousSettingsFilename,
mSettingsReserveCopyFilename,
FileUtils.S_IRUSR | FileUtils.S_IWUSR
| FileUtils.S_IRGRP | FileUtils.S_IWGRP,
"package manager settings", this);
}ResilientAtomicFile 的具体切换策略由该类实现,但 Settings 明确提供三条候选路径。写入成功后 reserve copy 保留上一份可用状态;主文件读取失败时,failRead() 允许恢复逻辑尝试其他候选。不要把 packages-backup.xml 当成永久备份,它是原子写入过程中的 previous 文件,成功写入后通常不存在。
2.3 readSettingsLPw
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
boolean readSettingsLPw(@NonNull Computer computer, @NonNull List<UserInfo> users,
ArrayMap<String, Long> originalFirstInstallTimes) {
mPendingPackages.clear();
mInstallerPackages.clear();
originalFirstInstallTimes.clear();
ArrayMap<Long, Integer> keySetRefs = new ArrayMap<>();
ArrayList<Signature> readSignatures = new ArrayList<>();
try (ResilientAtomicFile atomicFile = getSettingsFile()) {
FileInputStream str = null;
try {
str = atomicFile.openRead();
if (str == null) {
findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
return false;
}
final TypedXmlPullParser parser = Xml.resolvePullParser(str);
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
// seek the first element
}
if (type != XmlPullParser.START_TAG) {
mReadMessages.append("No start tag found in settings file\n");
PackageManagerService.reportSettingsProblem(Log.WARN,
"No start tag found in package manager settings");
return false;
}文件不存在或没有 start tag 时,方法返回 false;这会让 PMS 把本次启动标记为 first boot。正常 XML 读取则继续按顶层 tag 分发。mPendingPackages 和 keySetRefs 在每次尝试前清空,避免从失败的候选文件把半解析状态带到下一次恢复。
2.4 损坏重试
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
} catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException
| IllegalArgumentException e) {
// Remove corrupted file and retry.
atomicFile.failRead(str, e);
// Ignore the result to not mark this as a "first boot".
readSettingsLPw(computer, users, originalFirstInstallTimes);
}
}
return true;
}XML/IO/数组越界/非法参数会触发 failRead() 并递归重试。注释明确要求忽略递归调用的返回值,当前外层仍返回 true;因此“主文件损坏”与“所有候选都不存在”在 mFirstBoot 语义上不同。损坏恢复成功后,PMS 不会把本次启动当成首次启动。
3. XML 状态
3.1 VersionInfo
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
public static class VersionInfo {
int sdkVersionFull;
int sdkVersion;
int databaseVersion;
String buildFingerprint;
String fingerprint;
public void forceCurrent() {
sdkVersion = Build.VERSION.SDK_INT;
sdkVersionFull = Build.VERSION.SDK_INT_FULL;
if (Build.getMajorSdkVersion(sdkVersionFull) != sdkVersion) {
throw new RuntimeException("SDK versions don't match");
}
databaseVersion = CURRENT_DATABASE_VERSION;
buildFingerprint = Build.FINGERPRINT;
fingerprint = PackagePartitions.FINGERPRINT;
}
}VersionInfo 记录的是 volume 级别的 SDK、数据库版本和 fingerprint。它不是某个 APK 的版本;APK 版本存在 PackageSetting.versionCode。PMS 用 internal volume 的 fingerprint 判断系统分区升级,再决定权限重授和 code cache 清理。
3.2 包与 disabled
readSettingsLPw() 解析 <package> 放入 mPackages,解析 <updated-package> 放入 mDisabledSysPackages。前者代表当前可参与后续扫描的设置,后者保存被 data 更新覆盖的 system 原始版本。两者的 code path、flags、signing details 和 appId 都可能不同,不能在恢复时合并为一个对象。
3.3 pending shared
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
for (int i = 0; i < mPendingPackages.size(); i++) {
final PackageSetting p = mPendingPackages.get(i);
final int sharedUserAppId = p.getSharedUserAppId();
if (sharedUserAppId <= 0) {
continue;
}
final Object idObj = getSettingLPr(sharedUserAppId);
if (idObj instanceof SharedUserSetting) {
final SharedUserSetting sharedUser = (SharedUserSetting) idObj;
addPackageSettingLPw(p, sharedUser);
} else if (idObj != null) {
String msg = "Bad package setting: package " + p.getPackageName()
+ " has shared uid " + sharedUserAppId + " that is not a shared uid\n";
mReadMessages.append(msg);
PackageManagerService.reportSettingsProblem(Log.ERROR, msg);
} else {
String msg = "Bad package setting: package " + p.getPackageName()
+ " has shared uid " + sharedUserAppId + " that is not defined\n";
mReadMessages.append(msg);
PackageManagerService.reportSettingsProblem(Log.ERROR, msg);
}
}
mPendingPackages.clear();包可能先被读入 pending list,等对应的 shared user setting 出现后再关联。找不到 shared UID 时记录 settings problem 并丢弃关联,不会凭空创建一个新的 shared user。读取成功后的 mPendingPackages.clear() 是恢复阶段的清理点。
3.4 用户限制
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
if (mBackupStoppedPackagesFilename.exists() || mStoppedPackagesFilename.exists()) {
readStoppedLPw();
mBackupStoppedPackagesFilename.delete();
mStoppedPackagesFilename.delete();
writePackageRestrictionsLPr(UserHandle.USER_SYSTEM, /*sync=*/true);
} else {
for (UserInfo user : users) {
readPackageRestrictionsLPr(user.id, originalFirstInstallTimes);
}
}
for (UserInfo user : users) {
mRuntimePermissionsPersistence.readStateForUserSync(user.id, getInternalVersion(),
mPackages, mSharedUsers, getUserRuntimePermissionsFile(user.id));
}旧 stopped 文件存在时先迁移到新的 per-user restrictions 格式,并删除旧文件;否则逐用户读取 restrictions。runtime permissions 紧随其后读取,输入包括当前 package/shared user 集合和 internal VersionInfo。这个顺序说明用户权限状态依赖全局包设置已先恢复。
4. 扫描后对照
4.1 code path
packages.xml 中的 codePath 是历史状态,不是磁盘事实。system/data/APEX 扫描会根据实际文件重新建立 ParsedPackage,再通过 ScanRequest、PackageSetting 和冲突逻辑决定是否沿用、更新、隐藏或删除历史对象。SCAN_REQUIRE_KNOWN 会在 data 扫描时要求新发现路径与已知 path 一致,OTA 的 mExpectingBetter 是例外。
4.2 版本与 mtime
包设置同时保存 version code、last modified time、first/last install time。扫描阶段会把 APK 实际 mtime 与设置对照,用于签名缓存、system/data 选择和 parser cache 失效;版本号决定更新优先级,但不能替代 path 和 signature 检查。
4.3 flags 与 seInfo
PackageSetting.flags/privateFlags 由 XML 恢复后会在扫描 policy 中重新派生 system、privileged、vendor/product/system_ext、APEX、updated system 等身份;seInfo、ABI、native library path 和 component resolver 状态都可能因此改变。看到 XML 中 flags 正确,不代表当前 APK 的分区路径和 policy 已经被接受。
4.4 孤立设置
如果 packages.xml 中存在 package,但扫描目录没有对应 code path,PMS 会在 system package cleanup、data scan 和 fixSystemPackages() 等阶段判断是已卸载、更新残留、期待更好版本还是孤立设置。清理策略取决于 system/data/APEX、用户数据和 disabled setting,不能用“磁盘不存在就立刻删除 XML”概括。
5. 测试与诊断
5.1 主文件副本
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@Test
public void testReadKeySetSettings() throws Exception {
writeOldFiles();
Settings settings = makeSettings();
assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
verifyKeySetMetaData(settings);
}
@Test
public void testReadReserveCopyKeySetSettings() throws Exception {
writeReserveCopyOldFiles();
Settings settings = makeSettings();
assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
verifyKeySetMetaData(settings);
}
@Test
public void testReadMalformedPackagesXmlKeySetSettings() throws Exception {
writeReserveCopyOldFiles();
writeCorruptedPackagesXml();
Settings settings = makeSettings();
assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
verifyKeySetMetaData(settings);
}三个测试的 arrange 分别是主文件、reserve copy、损坏主文件 + 可用 reserve copy;action 都是 readLPw();assert 是读取成功且 KeySet metadata 完整。它们证明原子恢复和 KeySet 反序列化能协同工作,不证明磁盘 APK 与恢复设置已经一致。
5.2 写入后的副本
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@Test
public void testWriteCorruptReadKeySetSettings() throws Exception {
writeOldFiles();
Settings settings = makeSettings();
assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
settings.writeLPr(computer, /*sync=*/true);
File filesDir = InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
File packageXml = new File(filesDir, "system/packages.xml");
File packagesReserveCopyXml = new File(filesDir, "system/packages.xml.reservecopy");
assertTrue(packageXml.exists());
assertTrue(packagesReserveCopyXml.exists());
assertFalse(new File(filesDir, "packages-backup.xml").exists());
assertTrue(Arrays.equals(Files.readAllBytes(Path.of(packageXml.getAbsolutePath())),
Files.readAllBytes(Path.of(packagesReserveCopyXml.getAbsolutePath()))));
writeCorruptedPackagesXml();
assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
verifyKeySetMetaData(settings);
}测试同时断言成功写入后主文件和 reserve copy 存在且内容一致、backup 文件不存在;再损坏主文件并重新读取,验证 reserve copy 可恢复。它没有覆盖设备断电时的所有文件系统故障,也没有证明 packages.list/kernel mapping 已同步。
5.3 用户状态
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@Test
public void testWriteCorruptReadPackageRestrictions() {
final Settings settingsUnderTest = makeSettings();
populateDistractionFlags(settingsUnderTest);
settingsUnderTest.writePackageRestrictionsLPr(0, /*sync=*/true);
writeCorruptedPackageRestrictions(0);
populateDefaultSettings(settingsUnderTest);
settingsUnderTest.readPackageRestrictionsLPr(0, mOrigFirstInstallTimes);
verifyDistractionFlags(settingsUnderTest);
}这个测试只覆盖 user 级 restrictions 文件损坏恢复,输入是 user 0 的 distraction flags,断言读取后状态仍可恢复。它与主 packages.xml 测试分离,说明全局设置和 per-user 状态各自拥有原子文件与重试边界。
5.4 运行时诊断
adb shell dumpsys package version
adb shell dumpsys package packages
adb shell dumpsys package <package-name>
adb shell logcat -s PackageManager Settings PackageManagerService诊断顺序建议是:
dumpsys package version中 internal/external VersionInfo 的 SDK、database version、build fingerprint 和 partition fingerprint;- packages.xml 主文件、reserve copy、backup 文件的存在性和修改时间;
- package 的 codePath、version、appId、flags/privateFlags、signing details 和 disabled system setting;
- user restrictions 中 installed/stopped/enabled/suspended 状态;
- PMS 启动日志中的
No existing packages settings、Upgrading from、failRead、Expected better和 cleanup 信息; - 磁盘实际 APK 路径是否与 XML codePath 一致,是否经过
SCAN_REQUIRE_KNOWN和 system/data 冲突处理。
6. 源码路线
定位 packages.xml 或启动恢复问题时,可沿以下顺序阅读:
PackageManagerService构造函数:确认mFirstBoot读取结果和mIsUpgradefingerprint 比较。Settings构造函数/getSettingsFile():确认三个全局文件和旧 stopped 文件路径。readSettingsLPw():确认 XML 顶层 tag、主文件缺失和损坏重试。readLPw():确认 VersionInfo 补齐、pending package、restrictions、runtime permissions 和 disabled system user 关联。PackageCacher/preparePackageParserCache():确认 parser cache 根目录与 entry 失效。InitAppsHelper:确认 system/data 扫描 flags、SCAN_REQUIRE_KNOWN和扫描结束清理。InstallPackageHelper:确认 code path、版本、签名、system/data 冲突和孤立包处理。- Settings 写回方法:确认 packages.xml、package restrictions、packages.list 和 kernel mapping 是否同步。
一个实用练习是:主 packages.xml 损坏但 reserve copy 可读,reserve copy 中 foo 的 codePath 是 /data/app/old,磁盘上实际发现 /data/app/new,当前 partitions fingerprint 发生变化。请分别判断 readLPw() 的返回值、mFirstBoot、mIsUpgrade、parser cache 目录、SCAN_REQUIRE_KNOWN、foo 的 path/version 校正和最终是否可能保留旧 data 状态。这个练习要求把 XML 恢复、fingerprint、磁盘扫描和冲突裁决分开。
7. 设计收束
Android 17 的 packages.xml 恢复与一致性检查是一条分层链路:
Settings用主文件、backup、reserve copy 恢复全局包数据库;文件损坏重试成功时不把启动误判为 first boot。VersionInfo记录 volume 级 SDK、database version 和 package-partition fingerprint;它决定 OTA/升级路径,但不是 APK 版本记录。- pending package、shared user、disabled system package、KeySet、user restrictions 和 runtime permissions 按不同阶段恢复,不能合并成一个 XML 状态。
- PMS 仍会扫描真实 system/APEX/data code path,用 path、mtime、version、signature 和 flags 对照并校正历史 PackageSetting。
- parser cache、packages.xml、package restrictions 和 runtime permissions 各自有失效与清理边界;一个文件恢复成功不代表其他状态同步。
- 测试证明主文件/reserve copy 原子恢复、KeySet metadata、user restrictions 损坏恢复和 VersionInfo 读写;完整磁盘一致性仍需结合启动扫描、dumpsys 和日志验证。
后续专题会进入扫描性能分析,利用本文的启动时间、cache 计数和 system/data 阶段边界,解释如何从 EventLog、Trace 和 PackageCacher 数据定位真正的启动瓶颈。
