增量扫描
本文面向已经读过 系统包扫描、扫描并行化、扫描签名校验 和 扫描冲突处理 的读者。前文解释了扫描入口、并行 parser、签名收集和 system/data 冲突;本文继续追踪 PMS 怎样判断“首次启动、普通启动还是系统分区更新后的启动”,以及这个判断如何影响 ABI、证书、parser cache、权限、代码缓存和 system 包恢复。
Android 17 的“增量扫描”不是只扫描变更 APK,也不是检测到 fingerprint 变化就简单地全量重建所有状态。PMS 仍会扫描 system、APEX 和 data 目录;差异体现在 scan flags、PackageSetting/证书/ABI 的复用、parser cache 的命中、OTA 后的 code cache 清理,以及是否执行权限重授、默认首选项初始化和旧 system package 恢复。
读完后,读者应能从 PMS 构造函数定位 mFirstBoot、mIsUpgrade、mPriorSdkVersion 的来源;从 InitAppsHelper 看出 SCAN_FIRST_BOOT_OR_UPGRADE 何时加入 system/data 扫描;从 PackageCacher 区分 fingerprint 目录、文件 mtime、路径和 AConfig flag 的缓存失效条件;还能解释 packages.xml 损坏、OTA 后 code cache 清理和“expected better package 未出现”分别由哪个 owner 处理。
1. 启动状态
1.1 两个布尔值
源码文件:
frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.javaframeworks/base/services/core/java/com/android/server/pm/Settings.java
PMS 使用两个不同状态描述启动:
| 状态 | 来源 | 含义 | 影响 |
|---|---|---|---|
mFirstBoot | Settings.readLPw() 返回值 | packages.xml 缺失时为首次启动 | Installer first boot、预置文件、默认首选项 |
mIsUpgrade | 当前 partitions fingerprint 与 Settings 记录比较 | 包相关系统分区发生变化 | 权限重授、代码缓存清理、升级迁移 |
mIsMockUpgrade | persist.pm.mock-upgrade | 测试/调试模拟升级 | isDeviceUpgrading() 返回 true |
首次启动和升级不是互斥状态:实际代码分别计算它们。isDeviceUpgrading() 返回 mIsUpgrade || mIsMockUpgrade,而 InitAppsHelper 的 mScanFlags 同时检查 isDeviceUpgrading() 和 isFirstBoot()。
1.2 VersionInfo
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
public static class VersionInfo {
int sdkVersionFull;
int sdkVersion;
int databaseVersion;
String buildFingerprint;
String fingerprint;
public void forceCurrent() {
sdkVersion = Build.VERSION.SDK_INT;
sdkVersionFull = Build.VERSION.SDK_INT_FULL;
if (Build.getMajorSdkVersion(sdkVersionFull) != sdkVersion) {
throw new RuntimeException("Build.VERSION.SDK_INT_FULL:" + sdkVersionFull
+ " and Build.VERSION.SDK_INT: " + sdkVersion + " don't match.");
}
databaseVersion = CURRENT_DATABASE_VERSION;
buildFingerprint = Build.FINGERPRINT;
fingerprint = PackagePartitions.FINGERPRINT;
}
}VersionInfo 按 volume 存在于 Settings 的 mVersion 中。buildFingerprint 主要用于诊断;PMS 判定 package partition 更新使用的是 fingerprint。forceCurrent() 不只是写 fingerprint,还会同步 SDK、full SDK、database version,并校验 full SDK 的 major 版本与当前 SDK 一致。
1.3 分区 fingerprint
源码文件:frameworks/base/core/java/android/content/pm/PackagePartitions.java
public static final String FINGERPRINT = getFingerprint();
private static String getFingerprint() {
final String[] digestProperties = new String[SYSTEM_PARTITIONS.size() + 1];
for (int i = 0; i < SYSTEM_PARTITIONS.size(); i++) {
final String partitionName = SYSTEM_PARTITIONS.get(i).getName();
digestProperties[i] = "ro." + partitionName + ".build.fingerprint";
}
digestProperties[SYSTEM_PARTITIONS.size()] = "ro.build.fingerprint";
return SystemProperties.digestOf(digestProperties);
}当前值由各 package partition 的 ro.<partition>.build.fingerprint 加上 ro.build.fingerprint 共同摘要得到。它不是简单的 Build.FINGERPRINT,也不是根据 APK mtime 逐包计算;只要 package 相关分区或整体 build fingerprint 改变,PMS 的升级判断就可能变为 true。
2. PMS 初始化
2.1 Settings 读取
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java、frameworks/base/services/core/java/com/android/server/pm/Settings.java
t.traceBegin("read user settings");
mFirstBoot = !mSettings.readLPw(computer,
mInjector.getUserManagerInternal().getUsers(/* excludeDying= */ false));
t.traceEnd();
if (mFirstBoot) {
t.traceBegin("setFirstBoot: ");
try {
mInstaller.setFirstBoot();
} catch (InstallerException e) {
Slog.w(TAG, "Could not set First Boot: ", e);
}
t.traceEnd();
}Settings.readLPw() 返回 false 的官方注释是“settings file missing(首次启动)”。PMS 将其取反写入 mFirstBoot,然后通知 Installer。这一步只决定 packages.xml 是否存在,不代表 parser cache 一定不存在,也不代表每个 APK 都必须重新解析。
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
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();
}
}
return true;
}Settings 读取完成后,如果 internal/external VersionInfo 缺失,会创建并 forceCurrent(),避免后续没有 fingerprint 基线。packages.xml 损坏时 readSettingsLPw() 会进入 atomicFile.failRead() 后重试读取;这条路径与文件完全不存在不同,不能简单称为“必然首次启动”。
2.2 升级比较
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
final VersionInfo ver = mSettings.getInternalVersion();
mIsMockUpgrade = SystemProperties.getBoolean("persist.pm.mock-upgrade", false /* default */);
mIsUpgrade = !partitionsFingerprint.equals(ver.fingerprint);
if (mIsUpgrade) {
PackageManagerServiceUtils.logCriticalInfo(Log.INFO,
"Upgrading from " + ver.fingerprint + " (" + ver.buildFingerprint + ") to "
+ PackagePartitions.FINGERPRINT + " (" + Build.FINGERPRINT + ")");
}
mPriorSdkVersion = mIsUpgrade ? ver.sdkVersion : -1;
mPriorSdkVersionFull = mIsUpgrade ? ver.sdkVersionFull : -1;
mPromoteSystemApps = mIsUpgrade && ver.sdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1;
mIsPreNMR1Upgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N_MR1;
mIsPreQUpgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.Q;升级比较只使用 internal volume 的 VersionInfo.fingerprint 与当前 partitionsFingerprint。mPriorSdkVersion、mIsPreNMR1Upgrade、mIsPreQUpgrade 都从旧 SDK 版本派生,分别影响旧权限、签名和 launcher 图标等迁移路径。mIsMockUpgrade 不会改变 mIsUpgrade 本身,但会让 isDeviceUpgrading() 对外返回 true。
2.3 扫描调度
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java、frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
int scanFlags = SCAN_BOOTING | SCAN_INITIAL;
if (mIsDeviceUpgrading || mPm.isFirstBoot()) {
mScanFlags = scanFlags | SCAN_FIRST_BOOT_OR_UPGRADE;
} else {
mScanFlags = scanFlags;
}
mSystemParseFlags = mPm.getDefParseFlags()
| ParsingPackageUtils.PARSE_IS_SYSTEM_DIR;
mSystemScanFlags = mScanFlags | SCAN_AS_SYSTEM;PMS 创建 InitAppsHelper 后,先取得 caching parser,再调用 initSystemApps() 和 initNonSystemApps();两者仍会扫描目录。flag 的差异由 ScanPackageUtils 消费:first boot/upgrade 重新 derive ABI,普通启动倾向于复用已有设置。
3. 扫描 flags
3.1 定义
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
static final int SCAN_NO_DEX = 1 << 0;
static final int SCAN_UPDATE_SIGNATURE = 1 << 1;
static final int SCAN_NEW_INSTALL = 1 << 2;
static final int SCAN_UPDATE_TIME = 1 << 3;
static final int SCAN_BOOTING = 1 << 4;
static final int SCAN_REQUIRE_KNOWN = 1 << 7;
static final int SCAN_MOVE = 1 << 8;
static final int SCAN_INITIAL = 1 << 9;
static final int SCAN_FIRST_BOOT_OR_UPGRADE = 1 << 12;
static final int SCAN_AS_SYSTEM = 1 << 16;
static final int SCAN_AS_PRIVILEGED = 1 << 17;
static final int SCAN_AS_APK_IN_APEX = 1 << 23;
static final int SCAN_DROP_CACHE = 1 << 24;这些 bit 是扫描上下文,不是“是否需要全量扫描”的单一枚举。SCAN_BOOTING 控制启动期间的依赖传播,SCAN_INITIAL 表示初始扫描,SCAN_FIRST_BOOT_OR_UPGRADE 触发昂贵派生,SCAN_REQUIRE_KNOWN 要求 data 包 path 与 Settings 一致,SCAN_DROP_CACHE 在提交 parser task 前清理指定 entry。
3.2 data 扫描
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
if ((mScanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) == SCAN_FIRST_BOOT_OR_UPGRADE) {
fixInstalledAppDirMode();
}
scanDirTracedLI(mPm.getAppInstallDir(), 0,
mScanFlags | SCAN_REQUIRE_KNOWN, packageParser, mExecutorService, null);
List<Runnable> unfinishedTasks = mExecutorService.shutdownNow();
if (!unfinishedTasks.isEmpty()) {
throw new IllegalStateException("Not all tasks finished before calling close: "
+ unfinishedTasks);
}
fixSystemPackages(userIds);
mExpectingBetter.clear();
mPm.mSettings.pruneRenamedPackagesLPw();first boot/upgrade 时 data app 目录会先修正安装目录权限;扫描使用 SCAN_REQUIRE_KNOWN,但 mExpectingBetter 为 OTA 新 system package 提供放宽路径。data 扫描结束后关闭 parser executor、修复 system packages、清空 expecting-better 并裁剪 renamed packages。
3.3 flags 与派生
SCAN_FIRST_BOOT_OR_UPGRADE 影响 ScanPackageUtils 的 needToDeriveAbi,也影响 InstallPackageHelper.scanPackageForInitLI() 的证书强制收集和系统更新判断。普通启动如果已有 PackageSetting 且不是 stub,会复用 ABI;路径、mtime 和 Settings 版本未变时也可复用签名缓存。flags 是条件输入,不是“增量扫描结果”本身。
4. parser cache
4.1 cache 目录
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
public static @Nullable File preparePackageParserCache(boolean forEngBuild,
boolean isUserDebugBuild, String incrementalVersion) {
if (!FORCE_PACKAGE_PARSED_CACHE_ENABLED) {
if (!DEFAULT_PACKAGE_PARSER_CACHE_ENABLED) {
return null;
}
if (forEngBuild) {
return null;
}
if (SystemProperties.getBoolean("pm.boot.disable_package_cache", false)) {
Slog.i(TAG, "Disabling package parser cache due to system property.");
return null;
}
}
final File cacheBaseDir = Environment.getPackageCacheDirectory();
if (!FileUtils.createDir(cacheBaseDir)) {
return null;
}
final String cacheName = FORCE_PACKAGE_PARSED_CACHE_ENABLED ? "debug"
: PackagePartitions.FINGERPRINT;
for (File cacheDir : FileUtils.listFilesOrEmpty(cacheBaseDir)) {
if (!Objects.equals(cacheName, cacheDir.getName())) {
FileUtils.deleteContentsAndDir(cacheDir);
}
}
return FileUtils.createDir(cacheBaseDir, cacheName);
}parser cache 默认位于 package cache directory 下,并以当前 PackagePartitions.FINGERPRINT 作为目录名;旧 fingerprint 目录会被删除。eng build、系统属性 pm.boot.disable_package_cache 或编译配置可以关闭 cache。cache 目录 fingerprint 与 VersionInfo.fingerprint 都使用分区 fingerprint,但一个服务于 parser cache 目录生命周期,一个服务于 PMS 升级状态判断。
4.2 cache key
源码文件:frameworks/base/services/core/java/com/android/server/pm/parsing/PackageCacher.java
private String getCacheKey(File packageFile, int flags) {
String name = packageFile.getName();
String absPath = packageFile.getAbsolutePath();
if (absPath.startsWith("/dev/block/dm-")) {
try {
name = IoUtils.readFileAsString("/sys/block/" + name + "/dm/name").trim();
absPath = null;
} catch (IOException e) {
Slog.w("Error while reading device name of " + name, e);
}
}
StringBuilder sb = new StringBuilder(name);
sb.append('-').append(flags);
if (absPath != null) {
sb.append('-').append(absPath.hashCode());
}
return sb.toString();
}cache key 同时包含文件名、parse flags 和绝对路径 hash;APEX dm 设备使用稳定的 device-mapper name,避免每次启动的 dm-NN 文件名变化导致 cache miss。key 变化会产生不同 cache entry,即使 APK 内容没有变化。
4.3 freshness 与内容
源码文件:frameworks/base/services/core/java/com/android/server/pm/parsing/PackageCacher.java
private static boolean isCacheFileUpToDate(File packageFile, File cacheFile) {
try {
if (packageFile.toPath().startsWith(Environment.getApexDirectory().toPath())) {
File backingApexFile = ApexManager.getInstance().getBackingApexFile(packageFile);
if (backingApexFile != null) {
packageFile = backingApexFile;
}
}
final StructStat pkg = Os.stat(packageFile.getAbsolutePath());
final StructStat cache = Os.stat(cacheFile.getAbsolutePath());
return pkg.st_mtime < cache.st_mtime;
} catch (ErrnoException ee) {
return false;
}
}
@Override
public ParsedPackage getCachedResult(File packageFile, int flags) {
final String cacheKey = getCacheKey(packageFile, flags);
final File cacheFile = new File(mCacheDir, cacheKey);
try {
if (!isCacheFileUpToDate(packageFile, cacheFile)) {
return null;
}
final ParsedPackage parsed = fromCacheEntry(
IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath()));
if (!packageFile.getAbsolutePath().equals(parsed.getPath())) {
return null;
}
if (android.content.pm.Flags.includeFeatureFlagsInPackageCacher()) {
for (var entry : ((PackageImpl) parsed).getFeatureFlagState().entrySet()) {
if (!Objects.equals(AconfigFlags.getInstance().getFlagValue(entry.getKey()),
entry.getValue())) {
return null;
}
}
}
return parsed;
} catch (Throwable e) {
Slog.w(TAG, "Error reading package cache: ", e);
cacheFile.delete();
return null;
}
}cache freshness 使用 package 与 cache 文件的 mtime;APEX mount point 会追到 backing APEX file。反序列化后再次检查 path;若 feature flag 记录已启用,还要逐项比较当前 AConfig 值。读取、路径或 flag 校验失败都回退真实解析,损坏 entry 会被删除。
5. OTA 收尾
5.1 权限重授
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
if (mIsUpgrade) {
Slog.i(TAG, "Partitions fingerprint changed from " + ver.fingerprint + " to "
+ PackagePartitions.FINGERPRINT
+ "; regranting permissions for internal storage");
}
mPermissionManager.onStorageVolumeMounted(
StorageManager.UUID_PRIVATE_INTERNAL, mIsUpgrade);
ver.sdkVersion = mSdkVersion;
ver.sdkVersionFull = mSdkVersionFull;internal storage mount 通知接收 mIsUpgrade,权限管理器据此决定是否执行升级后的权限处理。PMS 随后更新 VersionInfo 的 SDK 字段;fingerprint 的写回并不是在比较点立即发生,而是在 OTA 收尾阶段完成。
这张时序图强调一个容易混淆的顺序:mIsUpgrade 在比较点确定,权限重授和 code cache 清理在扫描收尾执行,VersionInfo.fingerprint 在清理分支之后才更新;parser cache 目录的准备则发生在扫描开始前。
5.2 code cache 清理
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
if (mIsUpgrade) {
Slog.i(TAG, "Build fingerprint changed; clearing code caches");
for (int i = 0; i < packageSettings.size(); i++) {
final PackageSetting ps = packageSettings.valueAt(i);
if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, ps.getVolumeUuid())) {
// No apps are running this early, so no need to freeze
mAppDataHelper.clearAppDataLIF(ps.getPkg(), USER_ALL,
FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
| Installer.FLAG_CLEAR_CODE_CACHE_ONLY
| Installer.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES);
}
}
ver.buildFingerprint = Build.FINGERPRINT;
ver.fingerprint = PackagePartitions.FINGERPRINT;
}OTA 后清理的是 code cache,不是所有 app data;FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES 保留 ART profiles,供 post-OTA profile verification/compilation 使用。清理只遍历 internal volume package settings,并且发生在应用启动前,因此无需 freeze package。清理完成后才把新 build/partition fingerprint 写入 VersionInfo。
5.3 首次启动迁移
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
if (mPromoteSystemApps || mFirstBoot) {
final List<UserInfo> users = mInjector.getUserManagerInternal().getUsers(true);
for (int i = 0; i < users.size(); i++) {
mSettings.applyDefaultPreferredAppsLPw(users.get(i).id);
}
}默认 preferred apps 初始化只在首次启动或 pre-M 升级迁移时执行,不是每次 fingerprint 变化都执行。mIsPreQUpgrade 还会触发旧非 system app 的 launcher icon 隐藏迁移。每条迁移路径都有自己的旧 SDK 条件,不能统称为“OTA 全量重置”。
6. 恢复路径
6.1 expected better
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
@GuardedBy("mPm.mLock")
public void checkExistingBetterPackages(ArrayMap<String, File> expectingBetterPackages,
List<String> stubSystemApps, int systemScanFlags, int systemParseFlags) {
for (int i = 0; i < expectingBetterPackages.size(); i++) {
final String packageName = expectingBetterPackages.keyAt(i);
if (mPm.mPackages.containsKey(packageName)) {
continue;
}
final File scanFile = expectingBetterPackages.valueAt(i);
logCriticalInfo(Log.WARN, "Expected better " + packageName
+ " but never showed up; reverting to system");
final Pair<Integer, Integer> flags =
mPm.getSystemPackageRescanFlagsAndReparseFlags(
scanFile, systemScanFlags, systemParseFlags);
if (flags.first == 0) {
Slog.e(TAG, "Ignoring unexpected fallback path " + scanFile);
continue;
}
mPm.mSettings.enableSystemPackageLPw(packageName);
try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
initPackageTracedLI(scanFile, flags.second, flags.first);
} catch (PackageManagerException e) {
Slog.e(TAG, "Failed to parse original system package: " + e.getMessage());
}
}
}mExpectingBetter 记录“应该由 data 版本替代”的 system package。如果 data 版本没有出现在 mPackages,PMS 用 code path 反向恢复 scan/reparse flags,启用 system setting 并重新扫描。未知 fallback path 被忽略,原始 system APK 解析失败只记录错误。
6.2 cache 清理
源码文件:frameworks/base/services/core/java/com/android/server/pm/parsing/PackageCacher.java、frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if ((scanParams.scanFlags & SCAN_DROP_CACHE) != 0) {
final PackageCacher cacher = new PackageCacher(mPm.getCacheDir(),
mPm.mPackageParserCallback);
Log.w(TAG, "Dropping cache of " + file.getAbsolutePath());
cacher.cleanCachedResult(file);
}
parallelPackageParser.submit(file, scanParams);SCAN_DROP_CACHE 是按文件提交前删除对应 parser cache 的显式标志,常见于 APEX active 状态变化等路径。它与 OTA 通过 fingerprint 切换 cache 根目录不同:前者清理指定包的 entry,后者在 PMS 初始化时清理旧 fingerprint 目录。
6.3 XML 损坏
Settings 读取 XML 时,atomicFile.failRead() 会标记损坏读取并再次调用 readSettingsLPw()。PMS 不会仅凭一次 XML 解析异常就把所有包当作首次启动;最终是否 mFirstBoot=true 取决于 readLPw() 对文件读取结果的返回。诊断时应同时查看 packages.xml、backup 文件、VersionInfo 和后续扫描日志。
7. 测试与诊断
7.1 VersionInfo 测试
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@Test
public void testReadWriteSettingsVersionCodeWithSdkVersionFull() {
final Settings settingsUnderTest = makeSettings();
final String uuid = UUID.randomUUID().toString();
Settings.VersionInfo versionInfo = settingsUnderTest.findOrCreateVersion(uuid);
versionInfo.forceCurrent();
settingsUnderTest.writeLPr(computer, /*sync=*/ true);
settingsUnderTest.onVolumeForgotten(uuid);
assertThat(settingsUnderTest.readLPw(computer, createFakeUsers()), is(true));
Settings.VersionInfo readVersionInfo = settingsUnderTest.findOrCreateVersion(uuid);
assertThat(readVersionInfo.sdkVersionFull, is(Build.VERSION.SDK_INT_FULL));
assertThat(readVersionInfo.sdkVersion, is(Build.VERSION.SDK_INT));
}该测试 arrange 一个 volume 的 VersionInfo,action 是 forceCurrent()、写入 Settings、忘记 volume 再重新读取,assert 检查 SDK 与 full SDK 持久化。它证明版本字段能读写,不证明真实 OTA fingerprint 变化会触发完整 PMS 初始化。
7.2 cache 测试
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageCacherTest.java
Android 17 的 cache 测试应覆盖:package/cache mtime、cache path、损坏 bytes、parse flags/path key、APEX backing file、AConfig feature flag 和 cleanCachedResult()。这些测试证明 parser cache 的局部失效条件,不证明 mIsUpgrade 或 SCAN_FIRST_BOOT_OR_UPGRADE 的启动判定。
7.3 启动诊断
adb shell logcat -s PackageManager PackageManagerService Settings PackageCacher
adb shell dumpsys package version
adb shell dumpsys package packages
adb shell dumpsys package <package-name>诊断顺序建议是:
Settings是否成功读取,internalVersionInfo.fingerprint是什么;- 当前
PackagePartitions.FINGERPRINT与Build.FINGERPRINT是否改变; - PMS 日志是否打印
Upgrading from ...、regranting permissions、clearing code caches; InitAppsHelper是否加入SCAN_FIRST_BOOT_OR_UPGRADE,data 扫描是否带SCAN_REQUIRE_KNOWN;PackageCacher是否命中、因 mtime/path/feature flag 失效或被显式 drop;- system/data 冲突是否进入
mExpectingBetter,最终是否由checkExistingBetterPackages()恢复; - OTA 后 code cache 被清理而 ART profiles 保留的状态是否符合预期。
7.4 状态复盘
| 现象 | 首先看 | 不要直接推断 |
|---|---|---|
| 每次都重新 parse | PackageCacher mtime、key、fingerprint 目录 | 一定发生 OTA |
| 打印 upgrading | VersionInfo.fingerprint 与当前 partitions fingerprint | 所有 APK 都重新安装 |
| 权限变化 | onStorageVolumeMounted(..., mIsUpgrade) | parser cache 一定失效 |
| code cache 消失 | clearAppDataLIF flags | 用户数据被清空 |
| system 包回来了 | mExpectingBetter、rescan flags | data 包一定被删除 |
| packages.xml 异常 | atomicFile.failRead、重试日志 | 必然首次启动 |
8. 源码路线
定位增量扫描问题时,可以沿以下顺序阅读:
Settings.readLPw():确认 packages.xml 是否读取成功、VersionInfo 是否补齐。PackageManagerService构造函数:确认 partitions fingerprint、mock upgrade、prior SDK 和迁移标志。InitAppsHelper构造函数:确认SCAN_FIRST_BOOT_OR_UPGRADE的加入条件。initSystemApps()/initNonSystemApps():确认 system/data 扫描顺序、SCAN_REQUIRE_KNOWN和 executor 关闭。ScanPackageUtils.scanPackageOnly():确认 first boot/upgrade 对 ABI 和设置复用的影响。ScanPackageUtils.collectCertificatesLI():确认路径、mtime、Settings version 和 force collect。PackageCacher与preparePackageParserCache():确认 cache 根目录、key、mtime、path 和 feature flag 失效。- PMS OTA 收尾:确认权限重授、code cache 清理、VersionInfo 写回和默认 preferred apps。
checkExistingBetterPackages():确认 data/system 冲突后的 fallback 重扫。
一个实用练习是:设备上 packages.xml 正常、旧 partitions fingerprint 为 F1,当前为 F2;/data/app/foo 路径和 mtime 未变,parser cache 目录只有 F2,foo 是已有非 stub 包。请分别判断 mIsUpgrade、SCAN_FIRST_BOOT_OR_UPGRADE、ABI 是否重新 derive、证书是否可复用、code cache 是否清理,以及 VersionInfo.fingerprint 在哪个阶段写成 F2。
9. 设计收束
Android 17 的增量扫描由多个独立条件组成:
mFirstBoot由 Settings 文件读取结果决定;mIsUpgrade由 package partitions fingerprint 比较决定,mock upgrade 只影响isDeviceUpgrading()。- 普通启动仍扫描 system/APEX/data 目录,只是不带
SCAN_FIRST_BOOT_OR_UPGRADE,因此 ABI、证书和 parser cache 有机会复用。 PackagePartitions.FINGERPRINT是 build 与各 package partition fingerprint 的摘要;VersionInfo持久化上次值,PackageCacher则用它管理 parser cache 根目录。- parser cache 还受 parse flags、路径、mtime、APEX backing file 和 AConfig flag 状态约束;失效时回退真实解析,损坏 entry 会被删除。
- OTA 后 PMS 重新授予 internal storage 权限、清理 code cache 但保留 ART profiles,并更新 preferred apps/旧 SDK 迁移状态。
- system/data 冲突通过
mExpectingBetter、disabled setting 和checkExistingBetterPackages()恢复;packages.xml 损坏有 atomic file 重试路径,不能直接等同首次启动。
后续专题会继续拆解 packages.xml 恢复与一致性检查,把本文提到的 VersionInfo、disabled package、孤立包和磁盘 code path 对照到 Settings 的 XML 读写和清理流程。
