Skip to content

Settings持久化

追踪 Android 17 PMS Settings 的全局、用户和权限文件,解析原子写入、损坏恢复、异步合并与辅助输出的一致性边界。

基于android-17.0.0_r1
AndroidPackageManagerServiceSettingsResilientAtomicFilepackages.xml源码阅读

Settings持久化 ​

本文面向已经读过 PMS构造函数 的读者。构造函数专题解释了 PMS 为什么在启动扫描后调用 writeSettingsLPrTEMP(),以及下次启动为什么先调用 Settings.readLPw();本文继续向下追踪这些方法实际读写哪些文件、一次写入如何提交、主文件损坏后如何选择 backup/reserve copy,以及全局包状态、per-user 状态和运行时权限为什么不能当成同一个 packages.xml。

Android 17 的 Settings 不是“一个 XML 数据库”。它至少包含三组具有不同 owner、目录、锁、调度器和恢复实现的持久化状态:/data/system/packages.xml 保存全局包身份和版本;/data/system/users/<userId>/package-restrictions.xml 保存每个用户的安装、启停、挂起和组件状态;Permission APEX 在 /data/misc_de/<userId>/apexdata/com.android.permission/ 保存当前 runtime-permissions 状态。此外,Settings 还派生 packages.list 和可选的 sdcardfs kernel mapping,供不应解析完整包数据库的 native 消费者使用。

本文的主线是“一次状态变化如何落盘并在重启后恢复”。PackageSetting 的所有字段、SharedUser 的签名约束和权限授予算法分别属于后续专题;这里会展示足以解释序列化契约的真实字段,但不把数据结构文章重复塞进持久化文章。

1. 文件版图 ​

1.1 全局文件 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
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);

final File kernelDir = new File("/config/sdcardfs");
mKernelMappingFilename = kernelDir.exists() ? kernelDir : null;

// Deprecated: Needed for migration
mStoppedPackagesFilename = new File(mSystemDir, "packages-stopped.xml");
mBackupStoppedPackagesFilename = new File(mSystemDir, "packages-stopped-backup.xml");

生产环境传入的 dataDir 是 Environment.getDataDirectory(),因此 mSystemDir 对应 /data/system。这些文件的角色不同:

路径owner内容写入协议
/data/system/packages.xmlSettings全局包、版本、签名、shared UID、domain verification、keysetResilientAtomicFile
/data/system/packages-backup.xmlResilientAtomicFile写入前的旧主文件,或中断写入留下的恢复源rename + 保留
/data/system/packages.xml.reservecopyResilientAtomicFile最近一次成功主文件的复制品copy + sync + fs-verity
/data/system/packages.listSettingsnative 消费者所需的扁平包/UID/GID/seInfo 列表JournaledFile
/config/sdcardfs/<package>Settings可选的 legacy sdcardfs 映射configfs 节点写入
packages-stopped*.xmllegacy migration旧单用户 stopped 状态只读迁移后删除

packages-backup.xml 不是长期轮换的“上一版本备份”。注释明确说明:当前设置成功存储后会删除它。正常完成一次写入后,磁盘应保留主文件与 reserve copy,而没有 temporary backup。

1.2 用户文件 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
private File getUserSystemDirectory(int userId) {
    // This instead of Environment.getUserSystemDirectory(userId) to support testing.
    return new File(new File(mSystemDir, "users"), Integer.toString(userId));
}

private ResilientAtomicFile getUserPackagesStateFile(int userId) {
    File mainFile = new File(getUserSystemDirectory(userId), "package-restrictions.xml");
    File temporaryBackup = new File(getUserSystemDirectory(userId),
            "package-restrictions-backup.xml");
    File reserveCopy = new File(getUserSystemDirectory(userId),
            "package-restrictions.xml.reservecopy");
    return new ResilientAtomicFile(mainFile, temporaryBackup, reserveCopy,
            FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
            "package restrictions", this);
}

每个 userId 有独立的 restrictions 三件套。它保存的是 PackageUserStateInternal:installed、stopped、enabled、hidden、suspended、instant app、enabled/disabled components、first install time、archive state 等。包名与 appId 的全局身份仍来自 packages.xml,用户文件只给这些已知包叠加用户维度状态。

这也是多用户状态不能嵌在全局 <package> 中统一写完的原因:单用户的启停变化只需更新该用户文件,避免每次 toggle 组件都重写整个全局包数据库;删除用户时也可以直接删除该用户的 restrictions 与权限状态。

1.3 权限文件 ​

源码文件:packages/modules/Permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java

java
private static final String APEX_MODULE_NAME = "com.android.permission";

private static final String RUNTIME_PERMISSIONS_FILE_NAME = "runtime-permissions.xml";
private static final String RUNTIME_PERMISSIONS_RESERVE_COPY_FILE_NAME =
        RUNTIME_PERMISSIONS_FILE_NAME + ".reservecopy";

@NonNull
static File getFile(@NonNull UserHandle user) {
    ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME);
    File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user);
    return new File(dataDirectory, RUNTIME_PERMISSIONS_FILE_NAME);
}

源码文件:frameworks/base/core/java/android/content/ApexEnvironment.java

java
public File getDeviceProtectedDataDirForUser(@NonNull UserHandle user) {
    return Environment.buildPath(
            Environment.getDataMiscDeDirectory(user.getIdentifier()), APEX_DATA,
            mApexModuleName);
}

当前 runtime-permissions 文件不在 /data/system/users/<id>,而在 Permission APEX 的 device-encrypted 用户目录:

text
/data/misc_de/<userId>/apexdata/com.android.permission/runtime-permissions.xml
/data/misc_de/<userId>/apexdata/com.android.permission/runtime-permissions.xml.reservecopy

Settings 仍构造 /data/system/users/<id>/runtime-permissions.xml 路径,但它只作为旧格式迁移输入:若 APEX persistence 返回 null,Settings 才读取 legacy 文件并异步写入新位置。把 legacy 路径描述成 Android 17 的当前 owner,会误判备份、回滚和 SELinux 边界。

下面的图只展示 owner 与文件关系,不把同名 XML 误画成一次统一事务。

2. 写入入口 ​

2.1 延迟合并 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java
static final int WRITE_SETTINGS_DELAY = 10*1000;  // 10 seconds

void scheduleWriteSettings() {
    // We normally invalidate when we write settings, but in cases where we delay and
    // coalesce settings writes, this strategy would have us invalidate the cache too late.
    // Invalidating on schedule addresses this problem.
    invalidatePackageInfoCache(
            PackageMetrics.INVALIDATION_REASON_SCHEDULE_WRITE_SETTINGS);
    ApplicationPackageManager.invalidateQueryIntentActivitiesCache();
    if (!mHandler.hasMessages(WRITE_SETTINGS)) {
        mHandler.sendEmptyMessageDelayed(WRITE_SETTINGS, WRITE_SETTINGS_DELAY);
    }
}

状态改变后常见入口不是立即调用 writeLPr(),而是调度一个 10 秒后的 WRITE_SETTINGS。同一窗口内只保留一条 message,多个包状态变化被合并为一次全量序列化。缓存却在“调度时”立即失效,因为内存状态已经变化;若等文件真正写入才 invalidation,查询可能在十秒内继续返回旧缓存。

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageHandler.java

java
case WRITE_SETTINGS: {
    mPm.writeSettings(/*sync=*/false);
} break;

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java
void writeSettings(boolean sync) {
    synchronized (mLock) {
        mHandler.removeMessages(WRITE_SETTINGS);
        mBackgroundHandler.removeMessages(WRITE_DIRTY_PACKAGE_RESTRICTIONS);
        writeSettingsLPrTEMP(sync);
        synchronized (mDirtyUsers) {
            mDirtyUsers.clear();
        }
    }
}

PackageHandler 消费 message 后,在 PMS mLock 内执行写入,并清掉全局 Settings 与 dirty-user restrictions 的待处理消息。这里的 owner 是 PMS handler,状态快照由 mLock 保护;磁盘 I/O 发生在同一调用链,scheduleWriteSettings() 本身才是异步边界。

2.2 sync语义 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java
void writeSettingsLPrTEMP(boolean sync) {
    snapshotComputer(false);
    mPermissionManager.writeLegacyPermissionsTEMP(mSettings.mPermissions);
    mSettings.writeLPr(mLiveComputer, sync);
}

sync=false 不表示 packages.xml 会在后台写。Settings.writeLPr() 始终在当前线程执行 startWrite()、XML 序列化和 finishWrite()。参数只继续传给 writeAllUsersPackageRestrictionsLPr(sync),决定 per-user restrictions 是当前线程写,还是投递给 Settings handler;runtime permissions 则无论该参数为何值,都会走自己的异步调度。

因此要区分两个维度:

维度false 的含义真实同步部分
scheduleWriteSettings()10 秒后由 PackageHandler 执行调度时只改 message/cache
writeLPr(..., sync)per-user restrictions 可延迟packages.xml 主提交仍同步
runtime permission write独立的 700~1300ms 合并窗口先在 PMS 锁内抽取状态,文件稍后写

把三者都称为“异步写 Settings”,会让故障分析无法判断调用线程究竟卡在 XML 序列化、restrictions handler 还是 Permission APEX I/O。

3. 全局序列化 ​

3.1 根级内容 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
void writeLPr(@NonNull Computer computer, boolean sync) {
    final long startTime = SystemClock.uptimeMillis();

    invalidatePackageCache(
            PackageMetrics.INVALIDATION_REASON_WRITE_SETTINGS);

    ArrayList<Signature> writtenSignatures = new ArrayList<>();

    try (ResilientAtomicFile atomicFile = getSettingsFile()) {
        FileOutputStream str = null;
        try {
            str = atomicFile.startWrite();

            final TypedXmlSerializer serializer = Xml.resolveSerializer(str);
            serializer.startDocument(null, true);
            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output",
                    true);

            serializer.startTag(null, "packages");

writeLPr() 的前置契约由方法后缀表达:LP 表示调用方持有 packages lock,r 表示读取/写入持久化状态。方法先使 package cache 失效,再从 ResilientAtomicFile 获取新的主文件输出流。

Xml.resolveSerializer() 可能使用 Android binary XML,也能与文本 XML 兼容;读取端使用对应的 Xml.resolvePullParser()。因此文件名虽然是 .xml,调试时不能假设 cat 一定得到可读文本。可在可 root 的 debuggable 设备上用 abx2xml 转换后检查。

根 <packages> 下的写入顺序为:

  1. 每个 volume 的 version;
  2. verifier device identity;
  3. permission trees 与 legacy permission definitions;
  4. 当前 mPackages 中的 package;
  5. disabled system package;
  6. shared user;
  7. renamed package;
  8. domain verification state;
  9. keyset manager state。

解析器按 tag 名分派,不依赖上述顺序才能识别,所以它仍能读取旧 schema 中的 last-platform-version、database-version、单用户 preferred activities 等迁移标签。

3.2 Version状态 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
for (int i = 0; i < mVersion.size(); i++) {
    final String volumeUuid = mVersion.keyAt(i);
    final VersionInfo ver = mVersion.valueAt(i);

    serializer.startTag(null, TAG_VERSION);
    XmlUtils.writeStringAttribute(serializer, ATTR_VOLUME_UUID, volumeUuid);
    serializer.attributeInt(null, ATTR_SDK_VERSION, ver.sdkVersion);
    serializer.attributeInt(null, ATTR_SDK_VERSION_FULL, ver.sdkVersionFull);
    serializer.attributeInt(null, ATTR_DATABASE_VERSION, ver.databaseVersion);
    XmlUtils.writeStringAttribute(serializer, ATTR_BUILD_FINGERPRINT,
            ver.buildFingerprint);
    XmlUtils.writeStringAttribute(serializer, ATTR_FINGERPRINT, ver.fingerprint);
    serializer.endTag(null, TAG_VERSION);
}

VersionInfo 是 PMS 判断升级的持久化输入。sdkVersion/sdkVersionFull 驱动权限与 schema 迁移;databaseVersion 独立于 SDK;buildFingerprint 用于诊断;PackagePartitions.FINGERPRINT 对应的 fingerprint 决定分区内容是否变化、是否进入真实 OTA 路径。

一个旧文件没有 sdkVersionFull 时,读取端不是填 0,而是从 sdkVersion 调用 Build.parseFullVersion() 生成默认值。这使引入 major/minor version scheme 后仍能读取旧 Settings。

3.3 Package记录 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
for (final PackageSetting pkg : mPackages.values()) {
    if (pkg.getPkg() != null && pkg.getPkg().isApex()) {
        // Don't persist APEX which doesn't have a valid app id and will fail to
        // load
        continue;
    }
    writePackageLPr(serializer, writtenSignatures, pkg);
}

for (final PackageSetting pkg : mDisabledSysPackages.values()) {
    if (pkg.getPkg() != null && pkg.getPkg().isApex()) {
        continue;
    }
    writeDisabledSysPackageLPr(serializer, pkg);
}

APEX package state由 ApexManager/APEX metadata 管理,Settings 跳过会导致 native parser 无有效 appId 的 APEX 条目。普通 package 和 disabled updated-system package 使用不同 tag:package 表示当前有效版本,updated-package 保留 system image 原版本的 setting,供 data 更新卸载或 OTA reconcile 使用。

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
void writePackageLPr(TypedXmlSerializer serializer, ArrayList<Signature> writtenSignatures,
        PackageSetting pkg) throws java.io.IOException {
    serializer.startTag(null, "package");
    serializer.attribute(null, ATTR_NAME, pkg.getPackageName());
    if (pkg.getRealName() != null) {
        serializer.attribute(null, "realName", pkg.getRealName());
    }
    serializer.attribute(null, "codePath", pkg.getPathString());

    serializer.attributeInt(null, "publicFlags", pkg.getFlags());
    serializer.attributeInt(null, "privateFlags", pkg.getPrivateFlags());
    serializer.attributeLongHex(null, "ft", pkg.getLastModifiedTime());
    serializer.attributeLongHex(null, "ut", pkg.getLastUpdateTime());
    serializer.attributeLong(null, "version", pkg.getVersionCode());
    serializer.attributeInt(null, "targetSdkVersion", pkg.getTargetSdkVersion());

    if (!pkg.hasSharedUser()) {
        serializer.attributeInt(null, "userId", pkg.getAppId());
        if (pkg.getPccId() > 0) {
            serializer.attributeInt(null, "pccId", pkg.getPccId());
        }
    } else {
        serializer.attributeInt(null, "sharedUserId", pkg.getAppId());
    }

全局 record 保存 package identity、code path、version、flags、appId/sharedUserId、ABI、install source、volume、签名/keyset、SDK/static library、domain set 和 metadata 等。installed/stopped/enabled 等 user state 不在这里作为当前格式的主 owner,它们写入 per-user restrictions。

签名通过 writtenSignatures 共享索引,避免在每个 package/shared-user 下重复完整证书。KeySetManager 的根级表与 package 内的 keyset identifier 必须一起恢复,因此测试常用 keyset metadata 检查整个 packages.xml 是否从 reserve copy 正确恢复,而不是只断言 readLPw() 返回 true。

3.4 提交后输出 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
serializer.endTag(null, "packages");
serializer.endDocument();

atomicFile.finishWrite(str);

writeKernelMappingLPr();
writePackageListLPr();
writeAllUsersPackageRestrictionsLPr(sync);
writeAllRuntimePermissionsLPr();
com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
        "package", SystemClock.uptimeMillis() - startTime);
return;

主 packages.xml 成功 finishWrite() 后,Settings 才更新辅助输出。它们不是同一个原子事务:packages.xml 已提交后,packages.list、某个 user restrictions 或 runtime permissions 仍可能失败或延迟。消费者必须按各自文件的恢复协议处理,不能假设看到新 packages.xml 就意味着所有派生文件已经同步到同一时刻。

4. 写入协议 ​

4.1 三个文件 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/ResilientAtomicFile.java

java
final class ResilientAtomicFile implements Closeable {
    private final File mFile;
    private final File mTemporaryBackup;
    private final File mReserveCopy;

    private final int mFileMode;
    private final String mDebugName;
    private final ReadEventLogger mReadEventLogger;

ResilientAtomicFile 是一个独立的 package-private final class,不继承 AtomicFile。它管理三个明确路径,并在一次实例生命周期中保存已打开的 main/reserve 输入输出流。设计目标不仅是“rename 临时文件”,而是同时应对:写入中断、主文件内容损坏、reserve copy 损坏和多线程路径覆盖。

4.2 startWrite ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/ResilientAtomicFile.java

java
public FileOutputStream startWrite() throws IOException {
    if (mMainOutStream != null) {
        throw new IllegalStateException("Duplicate startWrite call?");
    }

    new File(mFile.getParent()).mkdirs();

    if (mFile.exists()) {
        // Presence of backup settings file indicates that we failed
        // to persist packages earlier. So preserve the older
        // backup for future reference since the current packages
        // might have been corrupted.
        if (!mTemporaryBackup.exists()) {
            if (!mFile.renameTo(mTemporaryBackup)) {
                throw new IOException("Unable to backup " + mDebugName
                        + " file, current changes will be lost at reboot");
            }
        } else {
            mFile.delete();
            Slog.w(LOG_TAG, "Preserving older " + mDebugName + " backup");
        }
    }
    // Reserve copy is not valid anymore.
    mReserveCopy.delete();

若当前主文件存在且没有 backup,先把主文件 rename 为 temporary backup;若 backup 已存在,说明之前已有未完成写入,代码保留更老的 backup,并删除当前 main。这样不会用一次新的可疑 main 覆盖最后已知的旧恢复点。

reserve copy 在新写入开始时删除,因为它对应旧提交,不能与即将生成的新 main 被误认为同一代。此时崩溃,下一次读取会优先发现 temporary backup。

源码文件:frameworks/base/services/core/java/com/android/server/pm/ResilientAtomicFile.java

java
// In case of MT access, it's possible the files get overwritten during write.
// Let's open all FDs we need now.
try {
    mMainOutStream = new FileOutputStream(mFile);
    mMainInStream = new FileInputStream(mFile);
    mReserveOutStream = new FileOutputStream(mReserveCopy);
    mReserveInStream = new FileInputStream(mReserveCopy);
} catch (IOException e) {
    close();
    throw e;
}

return mMainOutStream;

新内容直接写到 mFile,不是写 packages.xml.new。安全性来自旧 main 已被 rename 到 backup。代码同时打开 main/reserve 的输入和输出 FD,防止多线程路径替换后 finish 阶段打开到另一代文件。返回给 serializer 的只有 main output stream。

4.3 finishWrite ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/ResilientAtomicFile.java

java
private void finalizeOutStream(FileOutputStream str) throws IOException {
    // Flash/sync + set permissions.
    str.flush();
    FileUtils.sync(str);
    FileUtils.setPermissions(str.getFD(), mFileMode, -1, -1);
}

public void finishWrite(FileOutputStream str, final boolean doFsVerity) throws IOException {
    if (mMainOutStream != str) {
        throw new IllegalStateException("Invalid incoming stream.");
    }

    // Flush and set permissions.
    try (FileOutputStream mainOutStream = mMainOutStream) {
        mMainOutStream = null;
        finalizeOutStream(mainOutStream);
    }
    // New file successfully written, old one are no longer needed.
    mTemporaryBackup.delete();

main 先 flush、fsync、chmod,成功后才删除 temporary backup。这一顺序保证 main 尚未稳定时仍保留旧文件。backup 删除后,main 成为权威提交;后续 reserve 或 fs-verity 失败不会回滚 main。

java
try (FileInputStream mainInStream = mMainInStream;
     FileInputStream reserveInStream = mReserveInStream) {
    mMainInStream = null;
    mReserveInStream = null;

    // Copy main file to reserve.
    try (FileOutputStream reserveOutStream = mReserveOutStream) {
        mReserveOutStream = null;
        FileUtils.copy(mainInStream, reserveOutStream);
        finalizeOutStream(reserveOutStream);
    }

    if (doFsVerity) {
        // Protect both main and reserve using fs-verity.
        try (ParcelFileDescriptor mainPfd = ParcelFileDescriptor.dup(mainInStream.getFD());
             ParcelFileDescriptor copyPfd = ParcelFileDescriptor.dup(reserveInStream.getFD())) {
            FileIntegrity.setUpFsVerity(mainPfd);
            FileIntegrity.setUpFsVerity(copyPfd);
        } catch (IOException e) {
            Slog.e(LOG_TAG, "Failed to verity-protect " + mDebugName, e);
        }
    }
} catch (IOException e) {
    Slog.e(LOG_TAG, "Failed to write reserve copy " + mDebugName + ": " + mReserveCopy, e);
}

reserve 是 main 的字节复制,复制后也 flush、sync、chmod。fs-verity 同时尝试保护 main 与 reserve,但失败只记录 error,不让 finishWrite() 失败;reserve copy I/O 的外层异常也只记录。持久化的最低成功条件是 main 完成 finalize,reserve 和 verity 是额外韧性层。

4.4 failWrite ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/ResilientAtomicFile.java

java
public void failWrite(FileOutputStream str) {
    if (mMainOutStream != str) {
        throw new IllegalStateException("Invalid incoming stream.");
    }

    // Close all FDs.
    close();

    // Clean up partially written files
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Slog.i(LOG_TAG, "Failed to clean up mangled file: " + mFile);
        }
    }
}

serializer 或 main finalize 抛 IOException 时,Settings 调 failWrite(str):关闭全部 FD并删除部分 main。它不把 temporary backup rename 回 main;backup 留在原路径,下一次 openRead() 会优先读取它。这样恢复动作集中在读路径,不要求失败现场继续完成更多可能失败的 rename。

写入状态可以按下面的阶段理解。图以“磁盘已有一次成功 main”的常规更新为主;首次写入没有旧 main,因此会跳过 BackupHeld,直接打开新的 main/reserve FD。

5. 读取协议 ​

5.1 文件优先级 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/ResilientAtomicFile.java

java
public FileInputStream openRead() throws IOException {
    if (mTemporaryBackup.exists()) {
        try {
            mCurrentFile = mTemporaryBackup;
            mCurrentInStream = new FileInputStream(mCurrentFile);
            if (mReadEventLogger != null) {
                mReadEventLogger.logEvent(Log.INFO,
                        "Need to read from backup " + mDebugName + " file");
            }
            if (mFile.exists()) {
                // If both the backup and normal file exist, we
                // ignore the normal one since it might have been
                // corrupted.
                Slog.w(LOG_TAG, "Cleaning up " + mDebugName + " file " + mFile);
                mFile.delete();
            }
            // Ignore reserve copy as well.
            mReserveCopy.delete();
        } catch (java.io.IOException e) {
            // We'll try for the normal settings file.
        }
    }

temporary backup 优先级最高,因为它证明上次写入未完成。成功打开 backup 后,代码删除同时存在的 main 和 reserve,防止把可能属于中断写入的新文件作为 fallback。注意 backup 被原地读取,并没有先 rename 回 packages.xml;下一次成功写入才会产生新的正常 pair。

java
if (mCurrentInStream != null) {
    return mCurrentInStream;
}

if (mFile.exists()) {
    mCurrentFile = mFile;
    mCurrentInStream = new FileInputStream(mCurrentFile);
} else if (mReserveCopy.exists()) {
    mCurrentFile = mReserveCopy;
    mCurrentInStream = new FileInputStream(mCurrentFile);
    if (mReadEventLogger != null) {
        mReadEventLogger.logEvent(Log.INFO,
                "Need to read from reserve copy " + mDebugName + " file");
    }
}

if (mCurrentInStream == null && mReadEventLogger != null) {
    mReadEventLogger.logEvent(Log.INFO, "No " + mDebugName + " file");
}
return mCurrentInStream;

没有 backup 时,main 存在就先读 main;只有 main 不存在才直接选择 reserve。若 main 能打开但内容损坏,fallback 由上层解析 catch 驱动:failRead() 删除当前 main,再递归调用 readSettingsLPw(),下一次 openRead() 才会选 reserve。

5.2 解析分派 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
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) {
            // nothing
        }

        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");
            Slog.wtf(PackageManagerService.TAG,
                    "No start tag found in package manager settings");
            return false;
        }

初次完全没有 global Settings 时返回 false,PMS 将其解释为 first boot,同时先创建当前 internal/external VersionInfo。存在文件但连 start tag 都没有时也直接返回 false;这个分支没有调用 failRead(),因此不会自动尝试 reserve copy。与抛出 parser/IO exception 的路径不同,故障现场需要区分“无 start tag 返回”和“解析异常 fallback”。

根级循环按 tag 名恢复 package、permissions、shared-user、updated-package、renamed-package、version、domain verification 与 keysets。未知 tag 只 warning 后跳过,提供向前兼容的降级能力。

5.3 异常重试 ​

源码文件: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);
}

源码文件:frameworks/base/services/core/java/com/android/server/pm/ResilientAtomicFile.java

java
public void failRead(FileInputStream str, Exception e) {
    if (mCurrentInStream != str) {
        throw new IllegalStateException("Invalid incoming stream.");
    }
    mCurrentInStream = null;
    IoUtils.closeQuietly(str);

    if (mReadEventLogger != null) {
        mReadEventLogger.logEvent(Log.ERROR,
                "Error reading " + mDebugName + ", removing " + mCurrentFile + '\n'
                        + Log.getStackTraceString(e));
    }

    if (!mCurrentFile.delete()) {
        throw new IllegalStateException("Failed to remove " + mCurrentFile);
    }
    mCurrentFile = null;
}

当前副本解析异常时,failRead() 删除它;删除失败本身升级为 IllegalStateException,避免递归一直读同一坏文件。成功删除后重新创建 ResilientAtomicFile 并按 backup→main→reserve 的规则选择下一副本。

外层故意忽略递归返回值。假设 main 和 reserve 都损坏:第一次删 main,递归删 reserve,再次递归发现无文件并返回 false;该 false 不会传回最外层,最外层最终返回 true。目的写在源码注释中:不要把“已有 Settings 损坏”误标为 first boot。此时系统依靠扫描重新收敛状态,但不会执行所有新设备 first-boot 行为。

6. 读后重建 ​

6.1 SharedUser关联 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
final int N = mPendingPackages.size();

for (int i = 0; i < N; 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();

readPackageLPw() 遇到 sharedUserId 时不能立即把 package 注册到 appId map,因为对应 <shared-user> 可能出现在 XML 后面。它先创建 PackageSetting 放入 mPendingPackages;根文件解析完后再按 appId 关联。不存在的 shared UID 与 appId 被普通对象占用是两种不同错误,都会留下可诊断消息。

这说明 XML tag 顺序并不直接等于内存构建顺序。Settings 使用 pending list 解决前向引用;签名和 keyset 也通过共享索引在整份文档解析后校验。

6.2 旧Schema迁移 ​

读取器保留多条旧格式兼容路径:

  • last-platform-version 和 database-version 转换为按 volume 的 VersionInfo;
  • flags 拆成新的 public/private flags,并迁移 pre-M hidden/privileged bit;
  • 缺少 domainSetId 时生成新 ID;
  • legacy 单用户 enabled/components/preferred state 迁往 user 0;
  • packages-stopped.xml 存在时读取后删除旧文件,并同步写 user 0 restrictions;
  • runtime permissions APEX 文件缺失时读取旧 /data/system/users/<id>/runtime-permissions.xml。

迁移不是只读兼容:某些路径会立刻或异步写回新格式,使下一次启动不再依赖旧 schema。

6.3 后续文件 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
if (mBackupStoppedPackagesFilename.exists()
        || mStoppedPackagesFilename.exists()) {
    // Read old file
    readStoppedLPw();
    mBackupStoppedPackagesFilename.delete();
    mStoppedPackagesFilename.delete();
    // Migrate to new file format
    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));
}

全局 packages.xml 成功后,Settings 才有包名/appId/shared UID owner,之后才能把 per-user restrictions 和 runtime grants 合并到对应 PackageSetting/SharedUserSetting。读取顺序与文件职责一致:全局身份先建立,用户覆盖层后叠加。

最后 Settings 重新关联 disabled system package 的 SharedUser,记录读取包数,并调用 writeKernelMappingLPr() 更新可选 kernel mapping。读取不是简单反序列化结束,而包含跨文件关联和派生输出重建。

7. 用户状态 ​

7.1 首次用户状态 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
synchronized (mPackageRestrictionsLock) {
    str = atomicFile.openRead();
    if (str == null) {
        // At first boot, make sure no packages are stopped.
        // We usually want to have third party apps initialize
        // in the stopped state, but not at first boot.  Also
        // consider all applications to be installed.
        for (PackageSetting pkg : mPackages.values()) {
            pkg.setUserState(userId, pkg.getCeDataInode(userId),
                    pkg.getDeDataInode(userId),
                    pkg.getPccCeDataInode(userId),
                    pkg.getPccDeDataInode(userId),
                    COMPONENT_ENABLED_STATE_DEFAULT,
                    true  /*installed*/,
                    false /*stopped*/,
                    false /*notLaunched*/,
                    false /*hidden*/,
                    0 /*distractionFlags*/,
                    null /*suspendParams*/,
                    false /*instantApp*/,
                    false /*virtualPreload*/,
                    null /*lastDisableAppCaller*/,
                    null /*enabledComponents*/,
                    null /*disabledComponents*/,
                    PackageManager.INSTALL_REASON_UNKNOWN,
                    PackageManager.UNINSTALL_REASON_UNKNOWN,
                    null /*harmfulAppWarning*/,
                    null /*splashScreenTheme*/,
                    0 /*firstInstallTime*/,
                    PackageManager.USER_MIN_ASPECT_RATIO_UNSET,
                    null /*archiveState*/,
                    false /*appLockEnabled*/,
                    PackageManager.VIRTUAL_GAMEPAD_USER_OPTION_UNSET,
                    PackageManager.PERSONAL_CONTEXT_MODE_UNSET);
        }
        return;
    }
}

某用户没有 restrictions 文件时,不是简单“保持 PackageSetting 默认值”。Settings 显式把所有已知包视为 installed、not stopped、default enabled,并清空用户覆盖状态。这一行为服务 first boot/新用户初始化;之后扫描和 UserManager policy 还会继续修正允许安装的系统包。

7.2 写入调度 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
void writePackageRestrictionsLPr(int userId, boolean sync) {
    invalidatePackageCache(
            PackageMetrics.INVALIDATION_REASON_WRITE_PACKAGE_RESTRICTIONS);

    final long startTime = SystemClock.uptimeMillis();

    if (sync) {
        writePackageRestrictions(userId, startTime, sync);
    } else {
        synchronized (mPackageRestrictionsLock) {
            int pending = mPendingAsyncPackageRestrictionsWrites.get(userId, 0) + 1;
            mPendingAsyncPackageRestrictionsWrites.put(userId, pending);
        }
        Runnable r = () -> writePackageRestrictions(userId, startTime, sync);
        mHandler.obtainMessage(WRITE_USER_PACKAGE_RESTRICTIONS, r).sendToTarget();
    }
}

每次异步请求都会增加该 userId 的 pending count 并投递一个 Runnable。Runnable 真正执行时才在 mLock 内读取当前 PackageUserStateInternal,所以它序列化的是执行时状态,不是调度时 snapshot。

全用户同步写会先清空 pending count 并移除 handler message:

java
if (sync) {
    // Cancel all pending per-user writes.
    synchronized (mPackageRestrictionsLock) {
        mPendingAsyncPackageRestrictionsWrites.clear();
    }
    mHandler.removeMessages(WRITE_USER_PACKAGE_RESTRICTIONS);
}

已经被测试代码拿出但稍后执行的旧 Runnable 会把 pending 从 0 减到 -1,命中 cancel 分支直接返回,不能覆盖较新的同步写。这是同步 flush 对过期异步任务的版本屏障。

7.3 用户删除 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
void removeUserLPw(int userId) {
    Set<Entry<String, PackageSetting>> entries = mPackages.entrySet();
    for (Entry<String, PackageSetting> entry : entries) {
        entry.getValue().removeUser(userId);
    }
    mPreferredActivities.remove(userId);

    synchronized (mPackageRestrictionsLock) {
        getUserPackagesStateFile(userId).delete();
        mPendingAsyncPackageRestrictionsWrites.delete(userId);
    }

    removeCrossProfileIntentFiltersLPw(userId);

    mRuntimePermissionsPersistence.onUserRemoved(userId);
    mDomainVerificationManager.clearUser(userId);

    writePackageListLPr();
    writeKernelRemoveUserLPr(userId);
}

ResilientAtomicFile.delete() 同时删除 main、temporary backup 与 reserve copy。用户删除还取消 pending restrictions、删除 runtime permission 文件、清 domain state,并更新 packages.list/kernel mapping。只删除 package-restrictions.xml 会留下其他 owner 的用户状态。

8. 权限持久化 ​

8.1 Settings包装层 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
private static final class RuntimePermissionPersistence {
    // 700-1300ms delay to avoid monopolizing PMS lock when written for multiple users.
    private static final long WRITE_PERMISSIONS_DELAY_MILLIS = 1000;
    private static final double WRITE_PERMISSIONS_DELAY_JITTER = 0.3;

    private static final long MAX_WRITE_PERMISSIONS_DELAY_MILLIS = 2000;

    @GuardedBy("mPersistenceLock")
    private final RuntimePermissionsPersistence mPersistence;
    private final Object mPersistenceLock = new Object();

    // Low-priority handlers running on SystemBg thread.
    private final Handler mAsyncHandler = new MyHandler();
    private final Handler mPersistenceHandler = new PersistenceHandler();

Settings 内部 wrapper 负责从 PMS PackageSetting/SharedUserSetting 抽取 per-user grant/flag,合并频繁变化并串行调用 Permission APEX persistence。首次 mutation 随机延迟 700~1300ms;连续 mutation 最多推迟到 2 秒,避免多个用户权限写长期占用 PMS 锁。

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
public void writeStateForUser(int userId,
        @NonNull LegacyPermissionDataProvider legacyPermissionDataProvider,
        @NonNull WatchedArrayMap<String, ? extends PackageStateInternal> packageStates,
        @NonNull WatchedArrayMap<String, SharedUserSetting> sharedUsers,
        @Nullable Handler pmHandler, @NonNull PackageManagerTracedLock pmLock,
        boolean sync) {
    synchronized (mLock) {
        mAsyncHandler.removeMessages(userId);
        mWriteScheduled.delete(userId);
    }

    Runnable writer = () -> {
        Map<String, List<RuntimePermissionsState.PermissionState>> packagePermissions;
        Map<String, List<RuntimePermissionsState.PermissionState>> sharedUserPermissions;

        synchronized (pmLock) {
            packagePermissions = getPackagePermissions(userId, packageStates);
            sharedUserPermissions = getShareUsersPermissions(userId, sharedUsers);
        }
        synchronized (mLock) {
            int version = mVersions.get(userId, INITIAL_VERSION);
            String fingerprint = mFingerprints.get(userId);

            RuntimePermissionsState runtimePermissions = new RuntimePermissionsState(
                    version, fingerprint, packagePermissions, sharedUserPermissions);
            mPendingStatesToWrite.put(userId, runtimePermissions);
        }
        if (pmHandler != null) {
            mPersistenceHandler.obtainMessage(userId).sendToTarget();
        } else {
            writePendingStates();
        }
    };

锁内只抽取 immutable RuntimePermissionsState 放进 staging map;真正文件 I/O 在 mPersistenceLock 下串行执行。这样 Permission APEX 写文件不长期持有 PMS mLock。shared UID 权限按 shared-user name 单独序列化,不重复到成员 package 下。

8.2 APEX写入 ​

源码文件:packages/modules/Permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java

java
public void writeForUser(@NonNull RuntimePermissionsState runtimePermissions,
        @NonNull UserHandle user) {
    File file = getFile(user);
    AtomicFile atomicFile = new AtomicFile(file);
    FileOutputStream outputStream = null;
    try {
        outputStream = atomicFile.startWrite();

        XmlSerializer serializer = Xml.newSerializer();
        serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
        serializer.startDocument(null, true);

        serializeRuntimePermissions(serializer, runtimePermissions);

        serializer.endDocument();
        atomicFile.finishWrite(outputStream);
    } catch (Exception e) {
        Log.wtf(LOG_TAG,
                "Failed to write runtime-permissions.xml, restoring backup: " + file, e);
        atomicFile.failWrite(outputStream);
        return;
    } finally {
        IoUtils.closeQuietly(outputStream);
    }

Runtime permissions 使用 android.util.AtomicFile 写主文件,不复用 PMS 的 ResilientAtomicFile。主文件成功后再复制 reserve、sync,并尝试 fs-verity。reserve/verity 失败只记录 error,不回滚主文件,与 global Settings 的最低成功条件相似,但实现独立。

序列化 <runtime-permissions> 的 version/fingerprint,并分别写 package 与 shared-user permission。FLAG_PERMISSION_ONE_TIME 存在时,即使当前 permission state 为 granted,持久化的 granted 也写 false,防止一次性授权跨重启保留。

8.3 读取迁移 ​

源码文件:packages/modules/Permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java

java
public RuntimePermissionsState readForUser(@NonNull UserHandle user) {
    File file = getFile(user);
    try (FileInputStream inputStream = new AtomicFile(file).openRead()) {
        XmlPullParser parser = Xml.newPullParser();
        parser.setInput(inputStream, null);
        return parseXml(parser);
    } catch (FileNotFoundException e) {
        Log.i(LOG_TAG, "runtime-permissions.xml not found");
        return null;
    } catch (Exception e) {
        File reserveFile = getReserveCopyFile(user);
        Log.wtf(LOG_TAG, "Reading from reserve copy: " + reserveFile, e);
        try (FileInputStream inputStream = new AtomicFile(reserveFile).openRead()) {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(inputStream, null);
            return parseXml(parser);
        } catch (Exception exceptionReadingReserveFile) {
            Log.e(LOG_TAG, "Failed to read reserve copy: " + reserveFile,
                    exceptionReadingReserveFile);
            throw new IllegalStateException(
                    "Failed to read runtime-permissions.xml: " + file, e);
        }
    }
}

主文件不存在返回 null,Settings wrapper 再尝试 legacy 路径;主文件存在但损坏时直接读 reserve;reserve 也失败则抛 IllegalStateException,不是静默创建空权限状态。权限授予具有安全含义,无法恢复时选择 fail hard,避免把未知状态当作全部允许或全部未设置。

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
runtimePermissions = mPersistence.readForUser(UserHandle.of(userId));
if (runtimePermissions == null) {
    readLegacyStateForUserSync(userId, userRuntimePermissionsFile, packageSettings,
            sharedUsers);
    writeStateForUserAsync(userId);
    return;
}

只有 current APEX file 不存在才读取 /data/system/users/<id>/runtime-permissions.xml,随后异步写入新 owner。旧文件解析异常会抛 IllegalStateException,不会自动忽略权限损坏。

9. 辅助投影 ​

9.1 packages.list ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
private void writePackageListLPrInternal(int creatingUserId) {
    // Only derive GIDs for active users (not dying)
    final List<UserInfo> users = getActiveUsers(UserManagerService.getInstance());
    int[] userIds = new int[users.size()];
    for (int i = 0; i < userIds.length; i++) {
        userIds[i] = users.get(i).id;
    }

    // Write package list file now, use a JournaledFile.
    File tempFile = new File(mPackageListFilename.getAbsolutePath() + ".tmp");
    JournaledFile journal = new JournaledFile(mPackageListFilename, tempFile);

    final File writeTarget = journal.chooseForWrite();

packages.list 是从最终 PackageSetting/permission GID 派生的文本投影,使用 JournaledFile,不是 ResilientAtomicFile。每行写 package name、UID、debuggable、data directory、seInfo、GID 列表、profileable、version code、安装来源等;APEX 和缺少 metadata 的 setting 被跳过。

源码文件:system/core/libpackagelistparser/packagelistparser.cpp

cpp
static bool parse_line(const char* path, size_t line_number, const char* line, pkg_info* info) {
  int debuggable;
  char* gid_list;
  int profileable_from_shell = 0;
  int fields =
      sscanf(line, "%ms %u %d %ms %ms %ms %d %ld", &info->name, &info->uid,
             &debuggable, &info->data_dir, &info->seinfo, &gid_list,
             &profileable_from_shell, &info->version_code);
  // ... parse gids and validate field count ...
}

bool packagelist_parse(bool (*callback)(pkg_info*, void*), void* user_data) {
  return packagelist_parse_file("/data/system/packages.list", callback, user_data);
}

native 消费者通过 libpackagelistparser 读取固定字段,无需加载 framework XML parser 或 Binder 调 PMS。Settings 写入时设置专用 SELinux context 与 0640/system:package_info 权限;sepolicy 再按消费者授予只读访问。

9.2 Kernel映射 ​

mKernelMappingFilename 只有 /config/sdcardfs 存在时才非空。Settings 为每个包写 appId 与 excluded userIds,并在用户删除时写 remove_userid。现代设备上该目录可能不存在,此时所有方法快速返回。它是条件性的 legacy projection,不能作为当前所有 Android 17 设备的数据源。

9.3 一致性边界 ​

全局写入后的辅助顺序形成以下可观察窗口:

任一后置阶段失败不会撤销前面已经提交的 packages.xml。恢复策略依赖“全局身份可由扫描 reconcile,用户状态与权限各自有原子文件/备份”,而不是跨六类文件的分布式事务。调试不一致时,要按消费者读取哪个文件来定位,不能只比较所有文件 mtime 是否完全相同。

10. 故障判读 ​

10.1 文件组合 ​

相关源码:

  • frameworks/base/services/core/java/com/android/server/pm/ResilientAtomicFile.java
  • frameworks/base/services/core/java/com/android/server/pm/Settings.java

启动现场常见组合可以直接反推写入阶段:

现场可能阶段下次读取动作
main + reserve,无 backup上次完整写入优先 main
backup + partial mainmain 尚未稳定或进程中断优先 backup,删除 main/reserve
backup,无 mainfailWrite() 已删 partial main优先 backup
main,无 backup/reservemain 已稳定,reserve 复制失败或中断读取 main
reserve,无 main/backupmain 被解析失败删除读取 reserve
main 与 reserve 都存在但 main 内容损坏正常写后主文件损坏解析失败删 main,递归读 reserve

只看文件是否存在还不够。fs-verity I/O 错误可能让打开/读取抛异常,从而进入同一 failRead fallback;“文件大小非零”不能证明内容可解析。

10.2 日志来源 ​

Settings 实现 ResilientAtomicFile.ReadEventLogger:

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java

java
@Override
public void logEvent(int priority, String msg) {
    mReadMessages.append(msg + "\n");
    PackageManagerService.reportSettingsProblem(priority, msg);
}

Need to read from backup package manager settings file、Need to read from reserve copy...、Error reading..., removing ... 会进入 PMS Settings problem 日志与 mReadMessages。dumpsys package 的 settings problem/history 可与 boot EventLog、文件组合交叉判断。

Unable to write package manager settings, current changes will be lost at reboot 表示当前内存状态仍可能正常,但持久化提交失败;Failed to write reserve copy 表示主文件已经 durable,只丢失额外恢复副本;两者严重程度不同。

10.3 只读检查 ​

以下命令需要可 root 的 userdebug/eng 设备。输入是设备当前持久化文件,输出只用于检查文件组合与 XML,不会修改状态。

bash
# 输入:全局 Settings 目录。
# 输出:main、temporary backup、reserve copy 与 packages.list 的存在和时间。
adb shell su 0 ls -l \
  /data/system/packages.xml \
  /data/system/packages-backup.xml \
  /data/system/packages.xml.reservecopy \
  /data/system/packages.list

# 输入:可能为 Android binary XML 的 packages.xml。
# 输出:转换后的文本 XML 前 80 行;不要用 cat 假设文件一定是文本格式。
adb shell 'su 0 abx2xml /data/system/packages.xml -' \
  | head -n 80

# 输入:用户 0 的 restrictions 文件组合。
# 输出:该用户 main/backup/reserve 的元数据。
adb shell su 0 ls -l \
  /data/system/users/0/package-restrictions.xml \
  /data/system/users/0/package-restrictions-backup.xml \
  /data/system/users/0/package-restrictions.xml.reservecopy

# 输入:Permission APEX 的用户 0 DE 持久化目录。
# 输出:当前 runtime permissions 主文件和 reserve copy。
adb shell \
  'su 0 ls -l /data/misc_de/0/apexdata/com.android.permission/runtime-permissions.xml*'

通配符可能同时显示 AtomicFile 内部 backup;判断 current owner 时仍以源码的 getFile()/getReserveCopyFile() 为准。生产 user build 通常无法读取这些目录,permission denied 只说明调试权限不足,不说明文件缺失。

11. 测试证据 ​

11.1 全局恢复 ​

源码文件: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);
}

输入先建立合法旧 Settings,执行一次真实 writeLPr(sync=true),再把主 packages.xml 写坏;断言正常提交后 main 与 reserve 字节相同、backup 已删除,并在主文件损坏后恢复完整 keyset metadata。它覆盖 finishWrite() 产物与解析异常 fallback,不覆盖断电发生在每一个单独指令之间。

host test com.android.server.pm.test.SettingsTest 更接近块设备故障:设备侧先写大于 4096 字节的 packages.xml 与 reserve,host 启用 fs-verity,再直接损坏第 0 页或第 1 页并 drop caches,最后运行 testReadSettings。第 0 页触发初始 parser/verity 读取错误,第 1 页触发 XML 中段读取错误;两者都要求 keyset metadata 可恢复。它验证了 fallback 不只对人工 malformed XML 有效。

11.2 用户状态 ​

源码文件: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);

    // Corrupt primary file.
    writeCorruptedPackageRestrictions(0);

    // now read and verify
    populateDefaultSettings(settingsUnderTest);
    settingsUnderTest.readPackageRestrictionsLPr(0, mOrigFirstInstallTimes);
    verifyDistractionFlags(settingsUnderTest);
}

输入是 user 0 的非默认 distraction flags;动作是同步写 restrictions、损坏主文件、把内存重置为默认后重新读取;断言原 flags 被恢复。它验证 per-user 文件也使用 main→failRead→reserve 路径,而不是只测试 global packages.xml。

testReadWritePackageRestrictionsSyncAfterAsync() 先排队两个异步空状态写,运行第一个,再执行包含 flags 的全用户同步写;断言 handler pending message 被移除,随后手工运行旧第二 Runnable 也不能覆盖同步结果。该测试反向验证 pending count 的取消屏障与“异步 Runnable 读取执行时状态”的语义。

11.3 权限恢复 ​

源码文件:packages/modules/Permission/tests/apex/java/com/android/permission/persistence/RuntimePermissionsPersistenceTest.kt

kotlin
@Test
fun testWriteCorruptReadFromReserveCopy() {
    persistence.writeForUser(state, user)
    // Corrupt the primary file.
    RuntimePermissionsPersistenceImpl.getFile(user)
        .writeText(
            "<runtime-permissions version=\"10\"><package name=\"com.foo.bar\"><permission"
        )
    val persistedState = persistence.readForUser(user)

    checkPersistedState(persistedState!!)
}

测试 state 同时包含 package permission、shared-user permission、version 和 fingerprint。主文件损坏后,readForUser() 必须从 reserve 恢复;checkPersistedState() 逐项比较 grant、flags、version、fingerprint 与两个 map。它支持 Permission APEX reserve copy 的数据完整性结论,但没有覆盖 Settings legacy 文件迁移。

testDelete() 写入后调用 deleteForUser(),再读取断言 null,验证用户删除同时移除主文件与 reserve。Settings 的 removeUserLPw() 正是该消费者之一。

11.4 Schema迁移 ​

testReadSettingsVersionCodeWithNoSdkVersionFull_fromSdkVersion() 写入只有 sdkVersion=36 的旧 Settings,读取后断言 sdkVersionFull == Build.parseFullVersion("36")。testReadWriteSettingsVersionCodeWithSdkVersionFull() 则写入当前 full/major,再重新读取核对两者。它们证明 version tag 的兼容默认值与 round-trip,而不覆盖其他 PackageSetting 字段。

12. 源码复盘 ​

给定“安装成功后立即查询正常,但重启后包状态丢失”,首先沿 scheduleWriteSettings()、PackageHandler WRITE_SETTINGS、writeSettings()、writeSettingsLPrTEMP() 追到 Settings.writeLPr(),再区分日志是 main startWrite/finalize 失败,还是只失败在 reserve/辅助文件。前者意味着 packages.xml 未提交,后者不应直接推断全局状态丢失。

给定“user 10 的应用被禁用状态丢失,但包仍存在”,应查看 user 10 的 package-restrictions.xml 三件套,而不是只检查 packages.xml。包存在由 global PackageSetting 决定,enabled/stopped/hidden 属于 per-user owner。

给定“runtime permission 主文件损坏”,应先确认当前文件位于 Permission APEX DE 目录,再沿 RuntimePermissionsPersistenceImpl.readForUser() 检查 reserve;只有 current file 不存在时,Settings 才进入 legacy /data/system/users/<id>/runtime-permissions.xml 迁移。主文件和 reserve 都无法解析会抛异常,不会静默当成新用户权限状态。

最后,看到 main、backup、reserve 三个文件时,不要凭文件时间猜“最新的就是正确的”。openRead() 明确把 temporary backup 视为中断写入证据并赋予最高优先级;恢复顺序、删除动作与 first-boot 语义必须以 ResilientAtomicFile 和 readSettingsLPw() 的真实控制流为准。