Skip to content

packages.xml 恢复

追踪 Android 17 packages.xml 的原子恢复、版本状态、包设置反序列化、用户状态读取和扫描后磁盘一致性边界。

基于android-17.0.0_r1
AndroidPackageManagerServiceSettingspackages.xml持久化恢复源码阅读

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.xmlSettings全局包、VersionInfo、签名、KeySet、shared user、disabled system packagereadSettingsLPw()
packages-backup.xml原子写入恢复写入期间的旧主文件ResilientAtomicFile.openRead()
packages.xml.reservecopy长期保留副本成功写入后的保留副本ResilientAtomicFile.openRead()
package-restrictions.xmlSettings/用户状态per-user enabled、stopped、installed、suspend 等readPackageRestrictionsLPr()
runtime permissions 文件RuntimePermissionPersistence每用户 runtime grant 状态readStateForUserSync()
packages.listSettings供其他组件使用的轻量包列表写回阶段生成
packages.kmap/configfsSettingspackage→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

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

java
// PackageManagerService
t.traceBegin("read user settings");
mFirstBoot = !mSettings.readLPw(computer,
        mInjector.getUserManagerInternal().getUsers(/* excludeDying= */ false));
t.traceEnd();

PMS 只负责把读取结果转换成 mFirstBoot;真正的主文件选择、XML 解析和恢复由 Settings 持有。下面进入 Settings 的全局读取实现。

java
// 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

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

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

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

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

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

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

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

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

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 运行时诊断 ​

text
adb shell dumpsys package version
adb shell dumpsys package packages
adb shell dumpsys package <package-name>
adb shell logcat -s PackageManager Settings PackageManagerService

诊断顺序建议是:

  1. dumpsys package version 中 internal/external VersionInfo 的 SDK、database version、build fingerprint 和 partition fingerprint;
  2. packages.xml 主文件、reserve copy、backup 文件的存在性和修改时间;
  3. package 的 codePath、version、appId、flags/privateFlags、signing details 和 disabled system setting;
  4. user restrictions 中 installed/stopped/enabled/suspended 状态;
  5. PMS 启动日志中的 No existing packages settings、Upgrading from、failRead、Expected better 和 cleanup 信息;
  6. 磁盘实际 APK 路径是否与 XML codePath 一致,是否经过 SCAN_REQUIRE_KNOWN 和 system/data 冲突处理。

6. 源码路线 ​

定位 packages.xml 或启动恢复问题时,可沿以下顺序阅读:

  1. PackageManagerService 构造函数:确认 mFirstBoot 读取结果和 mIsUpgrade fingerprint 比较。
  2. Settings 构造函数/getSettingsFile():确认三个全局文件和旧 stopped 文件路径。
  3. readSettingsLPw():确认 XML 顶层 tag、主文件缺失和损坏重试。
  4. readLPw():确认 VersionInfo 补齐、pending package、restrictions、runtime permissions 和 disabled system user 关联。
  5. PackageCacher/preparePackageParserCache():确认 parser cache 根目录与 entry 失效。
  6. InitAppsHelper:确认 system/data 扫描 flags、SCAN_REQUIRE_KNOWN 和扫描结束清理。
  7. InstallPackageHelper:确认 code path、版本、签名、system/data 冲突和孤立包处理。
  8. 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 数据定位真正的启动瓶颈。