Skip to content

增量扫描

追踪 Android 17 首次启动、普通启动和分区更新后的扫描 flags、解析缓存、权限重授、代码缓存清理与恢复路径。

基于android-17.0.0_r1
AndroidPackageManagerService包扫描首次启动OTAfingerprint源码阅读

增量扫描 ​

本文面向已经读过 系统包扫描、扫描并行化、扫描签名校验 和 扫描冲突处理 的读者。前文解释了扫描入口、并行 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.java
  • frameworks/base/services/core/java/com/android/server/pm/Settings.java

PMS 使用两个不同状态描述启动:

状态来源含义影响
mFirstBootSettings.readLPw() 返回值packages.xml 缺失时为首次启动Installer first boot、预置文件、默认首选项
mIsUpgrade当前 partitions fingerprint 与 Settings 记录比较包相关系统分区发生变化权限重授、代码缓存清理、升级迁移
mIsMockUpgradepersist.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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 启动诊断 ​

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

诊断顺序建议是:

  1. Settings 是否成功读取,internal VersionInfo.fingerprint 是什么;
  2. 当前 PackagePartitions.FINGERPRINT 与 Build.FINGERPRINT 是否改变;
  3. PMS 日志是否打印 Upgrading from ...、regranting permissions、clearing code caches;
  4. InitAppsHelper 是否加入 SCAN_FIRST_BOOT_OR_UPGRADE,data 扫描是否带 SCAN_REQUIRE_KNOWN;
  5. PackageCacher 是否命中、因 mtime/path/feature flag 失效或被显式 drop;
  6. system/data 冲突是否进入 mExpectingBetter,最终是否由 checkExistingBetterPackages() 恢复;
  7. OTA 后 code cache 被清理而 ART profiles 保留的状态是否符合预期。

7.4 状态复盘 ​

现象首先看不要直接推断
每次都重新 parsePackageCacher mtime、key、fingerprint 目录一定发生 OTA
打印 upgradingVersionInfo.fingerprint 与当前 partitions fingerprint所有 APK 都重新安装
权限变化onStorageVolumeMounted(..., mIsUpgrade)parser cache 一定失效
code cache 消失clearAppDataLIF flags用户数据被清空
system 包回来了mExpectingBetter、rescan flagsdata 包一定被删除
packages.xml 异常atomicFile.failRead、重试日志必然首次启动

8. 源码路线 ​

定位增量扫描问题时,可以沿以下顺序阅读:

  1. Settings.readLPw():确认 packages.xml 是否读取成功、VersionInfo 是否补齐。
  2. PackageManagerService 构造函数:确认 partitions fingerprint、mock upgrade、prior SDK 和迁移标志。
  3. InitAppsHelper 构造函数:确认 SCAN_FIRST_BOOT_OR_UPGRADE 的加入条件。
  4. initSystemApps()/initNonSystemApps():确认 system/data 扫描顺序、SCAN_REQUIRE_KNOWN 和 executor 关闭。
  5. ScanPackageUtils.scanPackageOnly():确认 first boot/upgrade 对 ABI 和设置复用的影响。
  6. ScanPackageUtils.collectCertificatesLI():确认路径、mtime、Settings version 和 force collect。
  7. PackageCacher 与 preparePackageParserCache():确认 cache 根目录、key、mtime、path 和 feature flag 失效。
  8. PMS OTA 收尾:确认权限重授、code cache 清理、VersionInfo 写回和默认 preferred apps。
  9. 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 读写和清理流程。