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
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.xml | Settings | 全局包、版本、签名、shared UID、domain verification、keyset | ResilientAtomicFile |
/data/system/packages-backup.xml | ResilientAtomicFile | 写入前的旧主文件,或中断写入留下的恢复源 | rename + 保留 |
/data/system/packages.xml.reservecopy | ResilientAtomicFile | 最近一次成功主文件的复制品 | copy + sync + fs-verity |
/data/system/packages.list | Settings | native 消费者所需的扁平包/UID/GID/seInfo 列表 | JournaledFile |
/config/sdcardfs/<package> | Settings | 可选的 legacy sdcardfs 映射 | configfs 节点写入 |
packages-stopped*.xml | legacy migration | 旧单用户 stopped 状态 | 只读迁移后删除 |
packages-backup.xml 不是长期轮换的“上一版本备份”。注释明确说明:当前设置成功存储后会删除它。正常完成一次写入后,磁盘应保留主文件与 reserve copy,而没有 temporary backup。
1.2 用户文件
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.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
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
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 用户目录:
/data/misc_de/<userId>/apexdata/com.android.permission/runtime-permissions.xml
/data/misc_de/<userId>/apexdata/com.android.permission/runtime-permissions.xml.reservecopySettings 仍构造 /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
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
case WRITE_SETTINGS: {
mPm.writeSettings(/*sync=*/false);
} break;源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.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
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
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> 下的写入顺序为:
- 每个 volume 的
version; - verifier device identity;
- permission trees 与 legacy permission definitions;
- 当前
mPackages中的 package; - disabled system package;
- shared user;
- renamed package;
- domain verification state;
- 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
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
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
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
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
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
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
// 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
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。
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
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
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。
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
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
} 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
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
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
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
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
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:
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
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
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
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
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
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
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
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
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.javaframeworks/base/services/core/java/com/android/server/pm/Settings.java
启动现场常见组合可以直接反推写入阶段:
| 现场 | 可能阶段 | 下次读取动作 |
|---|---|---|
| main + reserve,无 backup | 上次完整写入 | 优先 main |
| backup + partial main | main 尚未稳定或进程中断 | 优先 backup,删除 main/reserve |
| backup,无 main | failWrite() 已删 partial main | 优先 backup |
| main,无 backup/reserve | main 已稳定,reserve 复制失败或中断 | 读取 main |
| reserve,无 main/backup | main 被解析失败删除 | 读取 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
@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,不会修改状态。
# 输入:全局 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
@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
@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
@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() 的真实控制流为准。
