Skip to content

系统包扫描

追踪 PMS 启动时从版本判断、APEX、系统分区到 data/app 扫描和状态收尾的完整链路。

基于android-17.0.0_r1
AndroidPackageManagerService包扫描InitAppsHelperAPEX源码阅读

系统包扫描 ​

PMS 启动时的“扫描包”不是一次递归遍历目录。Android 17 把它组织成一个由 PackageManagerService 驱动、InitAppsHelper 编排、InstallPackageHelper 执行解析与注册的阶段化流程:先准备升级语境和扫描标志,再扫描 APEX,接着按 overlay/framework/priv-app/app 顺序扫描系统分区,最后扫描 /data/app 中的用户安装包,并处理旧系统更新、stub 包、重命名包和 usage 状态。

本文是包扫描篇的总览,回答的是“启动扫描如何把磁盘内容变成 PMS 可查询状态”:

  • PMS 构造函数在什么时候决定这是首次启动、OTA 还是普通启动;
  • SYSTEM_PARTITIONS 和活跃 APEX 如何转成 ScanPartition;
  • 为什么 APEX 必须先扫描,系统 overlay/framework/priv-app/app 又有固定顺序;
  • ScanParams 怎样把 parse flags、scan flags、目录和 APEX 上下文交给并行解析器;
  • 解析为何可以并行,而 addForInitLI()/processParseResult() 仍按确定顺序修改 PMS 状态;
  • /data/app 扫描为什么要求 SCAN_REQUIRE_KNOWN,扫描失败时如何清理孤立状态;
  • system package cleanup、stub 替换、shared UID 修复和 usage 读取分别在什么时候生效;
  • 每个阶段的锁、异常、统计和未完成状态由谁负责。

PMS017 将深入单目录 scanDirTracedLI() 和 flags;PMS018~PMS020 分别展开 system/priv-app、data/app、vendor/product/system_ext 分区;本文不重复这些篇目的逐目录细节。

1. 启动入口 ​

1.1 前置状态 ​

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

java
final VersionInfo ver = mSettings.getInternalVersion();

// Read isMockUpgrade for the first time.
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;
mInitAppsHelper = new InitAppsHelper(this, mApexManager, mInstallPackageHelper,
        mInjector.getSystemPartitions());

扫描前 PMS 先从 Settings 读取上一轮的 VersionInfo,用 partition fingerprint 判断 mIsUpgrade。这个判断影响后面的 scan flags、权限升级和 system package cleanup;它不是单纯的日志信息。

mInitAppsHelper 在这里创建,但还没有开始扫描。构造函数里已经把 PMS、APEX manager、Install helper 和系统分区列表交给 helper,后续由 helper 负责把这些输入转换成扫描阶段。

1.2 OTA 前的包名快照 ​

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

java
final WatchedArrayMap<String, PackageSetting> packageSettings =
        mSettings.getPackagesLocked();

if (isDeviceUpgrading()) {
    // Save the names of pre-existing packages prior to scanning, so we can determine
    // which system packages are completely new due to an upgrade.
    mExistingPackages = new ArraySet<>(packageSettings.size());
    for (int i = 0; i < packageSettings.size(); i++) {
        mExistingPackages.add(packageSettings.valueAt(i).getPackageName());
    }
}

mExistingPackages 记录扫描前 Settings 中已经存在的包名。它的消费者不是解析器,而是 system package/OTA 逻辑,用于区分“升级后新出现的系统包”和“原来就存在的包”。如果没有这份前置集合,扫描结果只能告诉 PMS 当前发现了什么,不能判断 OTA 带来的新增状态。

1.3 Parser生命周期 ​

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

java
final int[] userIds = mUserManager.getUserIds();
PackageParser2 packageParser = mInjector.getScanningCachingPackageParser();
mOverlayConfig = mInitAppsHelper.initSystemApps(packageParser, packageSettings, userIds,
        startTime);
mInitAppsHelper.initNonSystemApps(packageParser, userIds, startTime);
packageParser.close();

PMS 只创建一个 scanning-caching PackageParser2,把它传入 system apps 和 non-system apps 两个阶段,最后统一 close()。这样 parser cache、callback 和 executor 上下文可以贯穿一次启动扫描;如果某阶段失败,构造函数不会把 parser 留给后续服务继续使用。

1.4 总体时序 ​

这条链不是“先 parse 全部、再一次性写 Settings”。每个解析结果在 InstallPackageHelper 中被转换并注册到 PMS,随后下一阶段的 flags 和候选集合会看到前一阶段已经建立的状态。

2. InitAppsHelper ​

2.1 Helper职责 ​

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

java
/**
 * Part of PackageManagerService that handles init and system packages. This class still needs
 * further cleanup and eventually all the installation/scanning related logic will go to another
 * class.
 */
final class InitAppsHelper {
    private final PackageManagerService mPm;
    private final List<ScanPartition> mDirsToScanAsSystem;
    private final int mScanFlags;
    private final int mSystemParseFlags;
    private final int mSystemScanFlags;
    private final InstallPackageHelper mInstallPackageHelper;
    private final ApexManager mApexManager;
    private final ExecutorService mExecutorService;
    private long mSystemScanTime;
    private int mCachedSystemApps;
    private int mSystemPackagesCount;
    private final boolean mIsDeviceUpgrading;
    private final List<ScanPartition> mSystemPartitions;

    private final ArrayMap<String, File> mExpectingBetter = new ArrayMap<>();
    private final List<String> mPossiblyDeletedUpdatedSystemApps = new ArrayList<>();
    private final List<String> mStubSystemApps = new ArrayList<>();

helper 的字段分成四组:

  • 扫描输入:PMS、系统分区、APEX manager、parser executor;
  • flags:通用 scan flags、system parse flags、system scan flags;
  • 统计:系统扫描耗时、cache 命中数、系统包数量;
  • 收尾状态:OTA 中期望的 better package、可能被删除的 updated system app、stub system app。

mExpectingBetter、mPossiblyDeletedUpdatedSystemApps 和 mStubSystemApps 不是 parser 临时变量,而是贯穿 system scan 到 data scan/fix 阶段的状态容器。

2.2 flags 初始化 ​

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

java
InitAppsHelper(PackageManagerService pm, ApexManager apexManager,
        InstallPackageHelper installPackageHelper,
        List<ScanPartition> systemPartitions) {
    mPm = pm;
    mApexManager = apexManager;
    mInstallPackageHelper = installPackageHelper;
    mSystemPartitions = systemPartitions;
    mDirsToScanAsSystem = getSystemScanPartitions();
    mIsDeviceUpgrading = mPm.isDeviceUpgrading();

    // Set flag to monitor and not change apk file paths when scanning install directories.
    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;
    mExecutorService = ParallelPackageParser.makeExecutorService();
}

基础 flags 是 SCAN_BOOTING | SCAN_INITIAL。首次启动或 OTA 再加 SCAN_FIRST_BOOT_OR_UPGRADE,系统包扫描额外加 SCAN_AS_SYSTEM,解析 flags 加 PARSE_IS_SYSTEM_DIR。

这些 flags 会被后续 ScanParams 带到 InstallPackageHelper,影响 APK 路径处理、system app 身份、cache 使用和首次启动/升级修复。不能把它们当成只用于日志的标签。

2.3 扫描器线程模型 ​

mExecutorService 由 ParallelPackageParser.makeExecutorService() 创建,用于 parse 阶段并行工作。并行只发生在解析任务提交和等待阶段;向 mPackages、Settings、resolver 注册的 addForInitLI()/processParseResult() 仍在 helper 要求的 install/package lock 下按顺序执行。

解析并行并不等于状态注册并行。这样设计既利用 parser 的 CPU 并行度,又保留 package conflict、factory/update 覆盖和 resolver 注册的确定顺序。

3. ScanPartition ​

3.1 系统分区列表 ​

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

java
/**
 * The list of all system partitions that may contain packages in ascending order of
 * specificity (the more generic, the earlier in the list a partition appears).
 */
@VisibleForTesting(visibility = Visibility.PACKAGE)
public static final List<ScanPartition> SYSTEM_PARTITIONS = Collections.unmodifiableList(
        PackagePartitions.getOrderedPartitions(ScanPartition::new));

SYSTEM_PARTITIONS 由 PackagePartitions.getOrderedPartitions() 生成,并包装成不可修改列表。注释中的“ascending order of specificity”很重要:更通用的分区在前,更具体的分区在后,后续冲突和覆盖逻辑依赖这个顺序。

3.2 Partition字段 ​

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

java
/** List of partitions to be scanned during system boot. */
public class ScanPartition extends PackagePartitions.SystemPartition {
    @PackageManagerService.ScanFlags
    public final int scanFlag;

    @Nullable
    public final ApexManager.ActiveApexInfo apexInfo;

    public ScanPartition(@NonNull PackagePartitions.SystemPartition partition) {
        super(partition);
        scanFlag = scanFlagForPartition(partition);
        apexInfo = null;
    }

ScanPartition 继承 PackagePartitions.SystemPartition,因此复用 partition 的 folder/app/priv-app/overlay 路径;自身增加 scanFlag 和可选 apexInfo。普通系统分区的 apexInfo 为 null,APEX 内部派生分区则携带 active APEX 上下文。

3.3 分区flags ​

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

java
private static int scanFlagForPartition(PackagePartitions.SystemPartition partition) {
    switch (partition.type) {
        case PackagePartitions.PARTITION_SYSTEM:
            return 0;
        case PackagePartitions.PARTITION_VENDOR:
            return SCAN_AS_VENDOR;
        case PackagePartitions.PARTITION_ODM:
            return SCAN_AS_ODM;
        case PackagePartitions.PARTITION_OEM:
            return SCAN_AS_OEM;
        case PackagePartitions.PARTITION_PRODUCT:
            return SCAN_AS_PRODUCT;
        case PackagePartitions.PARTITION_SYSTEM_EXT:
            return SCAN_AS_SYSTEM_EXT;
        default:
            throw new IllegalStateException("Unable to determine scan flag for "
                    + partition.getFolder());
    }
}

system 分区使用 0 作为基线,vendor/odm/oem/product/system_ext 分别映射到对应 scan flag。这个映射会影响 package 的 isVendor()、isProduct()、system app 判定和分区权限规则;因此不能只根据路径字符串在文章里自行推导。

3.4 APEX分区 ​

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

java
private List<ScanPartition> getSystemScanPartitions() {
    final List<ScanPartition> scanPartitions = new ArrayList<>();
    scanPartitions.addAll(mSystemPartitions);
    scanPartitions.addAll(getApexScanPartitions());
    Slog.d(TAG, "Directories scanned as system partitions: " + scanPartitions);
    return scanPartitions;
}

private List<ScanPartition> getApexScanPartitions() {
    final List<ScanPartition> scanPartitions = new ArrayList<>();
    final List<ApexManager.ActiveApexInfo> activeApexInfos = mApexManager.getActiveApexInfos();
    for (int i = 0; i < activeApexInfos.size(); i++) {
        final ScanPartition scanPartition = resolveApexToScanPartition(activeApexInfos.get(i));
        if (scanPartition != null) {
            scanPartitions.add(scanPartition);
        }
    }
    return scanPartitions;
}

private static @Nullable ScanPartition resolveApexToScanPartition(
        ApexManager.ActiveApexInfo apexInfo) {
    for (int i = 0, size = SYSTEM_PARTITIONS.size(); i < size; i++) {
        ScanPartition sp = SYSTEM_PARTITIONS.get(i);
        if (apexInfo.preInstalledApexPath.getAbsolutePath().equals(
                sp.getFolder().getAbsolutePath())
                || apexInfo.preInstalledApexPath.getAbsolutePath().startsWith(
                sp.getFolder().getAbsolutePath() + File.separator)) {
            return new ScanPartition(apexInfo.apexDirectory, sp, apexInfo);
        }
    }
    return null;
}

系统扫描列表不是只包含固定分区。helper 先复制 injector 提供的 system partitions,再把活跃 APEX 映射到原分区,生成带 apexInfo 的派生 ScanPartition。只有 preInstalledApexPath 位于某个已知 system partition 下时才接受映射;无法映射的 APEX 不会被当作普通系统分区扫描。

3.5 APEX flags ​

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

java
public ScanPartition(@NonNull File folder, @NonNull ScanPartition original,
        @Nullable ApexManager.ActiveApexInfo apexInfo) {
    super(folder, original);
    var scanFlags = original.scanFlag;
    this.apexInfo = apexInfo;
    if (apexInfo != null) {
        // ScanPartitionUtils.isApkInUpdatedApex() relies on this combination.
        scanFlags |= SCAN_AS_APK_IN_APEX;
        if (apexInfo.isFactory) {
            scanFlags |= SCAN_AS_FACTORY;
        }
        if (apexInfo.activeApexChanged) {
            scanFlags |= SCAN_DROP_CACHE;
        }
    }
    this.scanFlag = scanFlags;
}

APEX 内 APK 至少增加 SCAN_AS_APK_IN_APEX;factory APEX 增加 SCAN_AS_FACTORY;active APEX 发生变化时增加 SCAN_DROP_CACHE。源码注释明确指出 ScanPartitionUtils.isApkInUpdatedApex() 依赖这组组合,flags 不能被拆开或凭语义重命名。

4. 系统应用阶段 ​

4.1 APEX先行 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
public OverlayConfig initSystemApps(PackageParser2 packageParser,
        WatchedArrayMap<String, PackageSetting> packageSettings,
        int[] userIds, long startTime) {
    // Prepare apex package info before scanning APKs, this information is needed when
    // scanning apk in apex.
    final List<ApexManager.ScanResult> apexScanResults = scanApexPackagesTraced(packageParser);
    mApexManager.notifyScanResult(apexScanResults);

    scanSystemDirs(packageParser, mExecutorService);

APEX scan 的结果会先通知 ApexManager,然后才扫描普通 APK。原因在源码注释中写得很直接:扫描 APK in APEX 时需要先准备 APEX package info。若顺序反过来,位于 APEX 内的 APK 无法获得正确的 module、factory/active 和路径上下文。

4.2 Overlay配置 ​

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

java
final ArrayMap<String, File> apkInApexPreInstalledPaths = new ArrayMap<>();
for (ApexManager.ActiveApexInfo apexInfo : mApexManager.getActiveApexInfos()) {
    final String apexPackageName = mApexManager.getActivePackageNameForApexModuleName(
            apexInfo.apexModuleName);
    for (String packageName : mApexManager.getApksInApex(apexPackageName)) {
        apkInApexPreInstalledPaths.put(packageName, apexInfo.preInstalledApexPath);
    }
}

final OverlayConfig overlayConfig = OverlayConfig.initializeSystemInstance(
        consumer -> mPm.forEachPackageState(mPm.snapshotComputer(), packageState -> {
            var pkg = packageState.getPkg();
            if (pkg != null) {
                consumer.accept(pkg, packageState.isSystem(),
                        apkInApexPreInstalledPaths.get(pkg.getPackageName()));
            }
        }));

系统包扫描完成后,helper 收集 APEX 内 APK 的预装路径,再遍历 package state 初始化 overlay config。overlay consumer 同时接收 package、是否 system 和 APEX 预装路径,因此 overlay 的默认 enable、mutability、priority 可以按实际来源计算。

4.3 目录顺序 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void scanSystemDirs(PackageParser2 packageParser, ExecutorService executorService) {
    File frameworkDir = new File(Environment.getRootDirectory(), "framework");
    List<ScanParams> scanParamsList = new ArrayList<>();

    // Collect overlay packages before scanning any apps.
    for (int i = mDirsToScanAsSystem.size() - 1; i >= 0; i--) {
        final ScanPartition partition = mDirsToScanAsSystem.get(i);
        if (partition.getOverlayFolder() == null) {
            continue;
        }
        collectScanParams(scanParamsList, partition.getOverlayFolder(),
                mSystemParseFlags, mSystemScanFlags | partition.scanFlag,
                packageParser, executorService, partition.apexInfo);
    }

    collectScanParams(scanParamsList, frameworkDir, mSystemParseFlags,
            mSystemScanFlags | SCAN_NO_DEX | SCAN_AS_PRIVILEGED,
            packageParser, executorService, null);

    for (int i = 0, size = mDirsToScanAsSystem.size(); i < size; i++) {
        final ScanPartition partition = mDirsToScanAsSystem.get(i);
        if (partition.getPrivAppFolder() != null) {
            collectScanParams(scanParamsList, partition.getPrivAppFolder(),
                    mSystemParseFlags,
                    mSystemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag,
                    packageParser, executorService, partition.apexInfo);
        }
        collectScanParams(scanParamsList, partition.getAppFolder(), mSystemParseFlags,
                mSystemScanFlags | partition.scanFlag, packageParser, executorService,
                partition.apexInfo);
    }

    parallelScanDirTracedLI(scanParamsList, packageParser, executorService);

    if (!mPm.mPackages.containsKey("android")) {
        throw new IllegalStateException(
                "Failed to load frameworks package; check log for warnings");
    }
}

目录参数按四步加入列表:

  1. 各 system partition 的 overlay 目录,倒序收集;
  2. /system/framework,附加 SCAN_NO_DEX | SCAN_AS_PRIVILEGED;
  3. 各 partition 的 priv-app 目录,附加 SCAN_AS_PRIVILEGED;
  4. 各 partition 的 app 目录。

然后统一交给 parallelScanDirTracedLI()。注意“收集顺序”和“解析并行”并不冲突:参数列表保留目录优先级,解析器可以并行,结果处理仍按列表/提交顺序进行。

4.4 Framework失败 ​

scanSystemDirs() 完成后检查 mPm.mPackages.containsKey("android")。如果 framework package 没有进入 package map,helper 抛 IllegalStateException,PMS 启动不能继续把系统当作正常 package manager 使用。这个检查比“日志里有 warning”更强:它把核心 framework APK 缺失升级为构造阶段失败。

5. APEX 扫描 ​

5.1 apexd镜像 ​

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

java
/**
 * Scans APEX packages and registers them with the system.
 *
 * apexd has its own policy to decide which APEX to activate and which not. The policy might
 * conflict with that of PMS. The APEX package info stored in PMS is a mirror of that managed by
 * apexd. To keep activation status in sync, we don't persist APEX in settings and always scan
 * APEX from scratch during boot.
 */
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
public List<ApexManager.ScanResult> scanApexPackages(ApexInfo[] allPackages,
        int parseFlags, int scanFlags, PackageParser2 packageParser,
        ExecutorService executorService) {

APEX 的激活策略由 apexd 管理,PMS 保存的是镜像信息。源码选择启动时从头扫描 APEX,而不是把 APEX 完全依赖于 Settings 持久化;代价是某些字段(注释特别提到 lastUpdateTime)不能直接从 PMS Settings 恢复。

5.2 并行parse ​

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

java
if (allPackages == null) {
    return Collections.EMPTY_LIST;
}

ParallelPackageParser parallelPackageParser =
        new ParallelPackageParser(packageParser, executorService);
ScanParams scanParams = ScanParams.forApexDirScan(parseFlags, scanFlags);

ArrayMap<File, ApexInfo> parsingApexInfo = new ArrayMap<>();
for (ApexInfo ai : allPackages) {
    File apexFile = new File(ai.modulePath);
    parallelPackageParser.submit(apexFile, scanParams);
    parsingApexInfo.put(apexFile, ai);
}

List<ParallelPackageParser.ParseResult> parseResults =
        new ArrayList<>(parsingApexInfo.size());
for (int i = 0; i < parsingApexInfo.size(); i++) {
    parseResults.add(parallelPackageParser.take());
}

Collections.sort(parseResults, (a, b) -> {
    ApexInfo i1 = parsingApexInfo.get(a.scanFile);
    ApexInfo i2 = parsingApexInfo.get(b.scanFile);
    return Boolean.compare(i2.isFactory, i1.isFactory);
});

所有 APEX 文件可以并行 parse,但结果会按 isFactory 排序,让 factory package 先于 updated/non-factory package 处理。这个顺序为后续 system package 覆盖和 disabled factory state 建立确定基础。

5.3 注册与失败 ​

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

java
for (int i = 0; i < parseResults.size(); i++) {
    ParallelPackageParser.ParseResult parseResult = parseResults.get(i);
    Throwable throwable = parseResult.throwable;
    ApexInfo ai = parsingApexInfo.get(parseResult.scanFile);
    int newParseFlags = parseFlags;
    int newScanFlags = scanFlags | SCAN_AS_APEX
            | mPm.getSystemPackageScanFlags(parseResult.scanFile);
    if (!ai.isFactory) {
        newParseFlags &= ~ParsingPackageUtils.PARSE_IS_SYSTEM_DIR;
        newScanFlags |= SCAN_NEW_INSTALL;
    }

    if (throwable == null) {
        try {
            addForInitLI(parseResult.parsedPackage, newParseFlags, newScanFlags,
                    null, new ApexManager.ActiveApexInfo(ai));
            AndroidPackage pkg = parseResult.parsedPackage.hideAsFinal();
            if (ai.isFactory && !ai.isActive) {
                disableSystemPackageLPw(pkg);
            }
            results.add(new ApexManager.ScanResult(ai, pkg, pkg.getPackageName()));
        } catch (PackageManagerException e) {
            throw new IllegalStateException("Failed to scan: " + ai.modulePath, e);
        }
    } else if (throwable instanceof PackageManagerException) {
        throw new IllegalStateException("Unable to parse: " + ai.modulePath, throwable);
    } else {
        throw new IllegalStateException("Unexpected exception occurred while parsing "
                + ai.modulePath, throwable);
    }
}

factory APEX 使用 system parse flags;非 factory APEX 清除 PARSE_IS_SYSTEM_DIR 并增加 SCAN_NEW_INSTALL。解析成功后调用 addForInitLI() 注册 package;factory 但 inactive 的 APEX 还会调用 disableSystemPackageLPw()。任何 parse 或注册异常都会抛出 IllegalStateException,启动扫描不会静默跳过坏 APEX。

6. data/app 阶段 ​

6.1 目录权限修复 ​

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

java
/**
 * Fix up the previously-installed app directory mode - they can't be readable by non-system
 * users to prevent them from listing the dir to discover installed package names.
 */
void fixInstalledAppDirMode() {
    try (var files = Files.newDirectoryStream(mPm.getAppInstallDir().toPath())) {
        files.forEach(dir -> {
            try {
                Os.chmod(dir.toString(), 0771);
            } catch (ErrnoException e) {
                Slog.w(TAG, "Failed to fix an installed app dir mode", e);
            }
        });
    } catch (Exception e) {
        Slog.w(TAG, "Failed to walk the app install directory to fix the modes", e);
    }
}

首次启动或 OTA 时,initNonSystemApps() 会先修复 /data/app 子目录 mode。注释给出安全原因:非 system 用户不能通过列目录发现已安装 package name。权限修复失败只记录 warning,随后仍会继续扫描;这与 framework package 缺失的硬失败不同。

6.2 非系统包入口 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
public void initNonSystemApps(PackageParser2 packageParser, @NonNull int[] userIds,
        long startTime) {
    EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,
            SystemClock.uptimeMillis());

    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);
    logNonSystemAppScanningTime(startTime);
    mExpectingBetter.clear();
    mPm.mSettings.pruneRenamedPackagesLPw();
}

data/app 阶段以 BOOT_PROGRESS_PMS_DATA_SCAN_START 打点,随后扫描 PMS app install dir,并在 scan flags 中加入 SCAN_REQUIRE_KNOWN。这个 flag 让 data 包扫描必须与已知 Settings/package 状态协调,而不是把任意新文件无条件当成合法系统状态。

扫描完成后立即 shutdownNow() parser executor,并把未完成任务作为硬错误;然后才进入 system package fix、统计、清理 mExpectingBetter 和重命名 package。parser 生命周期在这里正式结束。

6.3 单目录扫描代理 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void scanDirTracedLI(File scanDir, int parseFlags, int scanFlags,
        PackageParser2 packageParser, ExecutorService executorService,
        @Nullable ApexManager.ActiveApexInfo apexInfo) {
    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER,
            "scanDir [" + scanDir.getAbsolutePath() + "]");
    try {
        mInstallPackageHelper.installPackagesFromDir(scanDir, parseFlags,
                scanFlags, packageParser, executorService, apexInfo);
    } finally {
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    }
}

scanDirTracedLI() 自己不遍历文件,也不解析 APK;它只建立 trace section,把目录、flags、parser、executor 和可选 APEX 信息交给 InstallPackageHelper.installPackagesFromDir()。具体 listFiles()、stage name 排除、cache drop 和 ParseResult 处理属于 PMS017。

6.4 生效边界 ​

InstallPackageHelper.installPackagesFromDir() 会为每个文件创建 ScanParams,提交 parser,等待结果,再调用 processParseResult();后者最终进入 addForInitLI()。因此 data/app 中一个 APK 的生效至少包括:文件被识别为 package、Manifest 解析成功、签名/版本/分区规则通过、PackageSetting 更新、组件 resolver 注册和 snapshot invalidation。目录扫描结束只表示输入文件已处理,不表示所有后续广播、权限授予或 dexopt 已完成。

7. ScanParams ​

7.1 三种参数工厂 ​

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

java
static class ScanParams {
    public final @Nullable File scanDir;
    public final int parseFlags;
    public final int scanFlags;
    public final @Nullable ApexManager.ActiveApexInfo apexInfo;

    private ScanParams(File scanDir, int parseFlags, int scanFlags,
            ApexManager.ActiveApexInfo apexInfo) {
        this.scanDir = scanDir;
        this.parseFlags = parseFlags;
        this.scanFlags = scanFlags;
        this.apexInfo = apexInfo;
    }

    public static ScanParams forApexDirScan(int parseFlags, int scanFlags) {
        return new ScanParams(null, parseFlags, scanFlags, null);
    }

    public static ScanParams forApkPartitionScan(File scanDir, int parseFlags, int scanFlags) {
        return new ScanParams(scanDir, parseFlags, scanFlags, null);
    }

    public static ScanParams forApkInApexScan(File scanDir, int parseFlags, int scanFlags,
            ApexManager.ActiveApexInfo apexInfo) {
        // When scanning apk in apexes, we want to check the maxSdkVersion.
        return new ScanParams(scanDir, parseFlags | PARSE_APK_IN_APEX, scanFlags, apexInfo);
    }
}

三个工厂表达三种输入:

  • APEX 文件本身:没有目录,后续通过 ApexInfo.modulePath 解析;
  • 普通 APK 分区目录:目录存在,没有 APEX context;
  • APEX 内 APK 目录:目录存在,增加 PARSE_APK_IN_APEX 并携带 active APEX info。

7.2 参数收集 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void collectScanParams(List<ScanParams> scanParamsList, File scanDir,
        int parseFlags, int scanFlags, PackageParser2 packageParser,
        ExecutorService executorService, ApexManager.ActiveApexInfo apexInfo) {
    scanParamsList.add((scanFlags & SCAN_AS_APK_IN_APEX) != 0
            ? ScanParams.forApkInApexScan(scanDir, parseFlags, scanFlags, apexInfo)
            : ScanParams.forApkPartitionScan(scanDir, parseFlags, scanFlags));
}

collectScanParams() 只把参数加入列表,不直接调用 parser。这样 scanSystemDirs() 可以先收集 overlay/framework/priv-app/app 的全部上下文,再由 parallelInstallPackagesFromDirs() 统一提交和按列表顺序处理。

7.3 结果顺序 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
public void installPackagesFromDir(File scanDir, int parseFlags, int scanFlags,
        PackageParser2 packageParser, ExecutorService executorService,
        @Nullable ApexManager.ActiveApexInfo apexInfo) {
    ParallelPackageParser parallelPackageParser =
            new ParallelPackageParser(packageParser, executorService);
    ScanParams scanParams = (scanFlags & SCAN_AS_APK_IN_APEX) != 0
            ? ScanParams.forApkInApexScan(scanDir, parseFlags, scanFlags, apexInfo)
            : ScanParams.forApkPartitionScan(scanDir, parseFlags, scanFlags);
    int fileCount = scanDirectoryForFilesToParse(parallelPackageParser, scanParams);

    // Process results one by one.
    for (; fileCount > 0; fileCount--) {
        processParseResult(parallelPackageParser.take());
    }
}

单目录入口先提交该目录的解析任务,再逐个消费完成结果;多目录入口则先为每个目录建立有序 future 列表。

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
public void parallelInstallPackagesFromDirs(List<ScanParams> scanParamsList,
        PackageParser2 packageParser, ExecutorService executorService) {
    ParallelPackageParser parallelPackageParser =
            new ParallelPackageParser(packageParser, executorService);
    List<List<OrderedResult>> resultsList = new ArrayList<>();

    // Submit scan and parse tasks across multiple directories.
    for (ScanParams scanParams : scanParamsList) {
        resultsList.add(orderedScanDirectoryForFilesToParse(
                parallelPackageParser, scanParams));
    }

    // Process results in order.
    for (List<OrderedResult> partitionResults : resultsList) {
        for (OrderedResult result : partitionResults) {
            try {
                processParseResult(result.future.get());
            } catch (ExecutionException | InterruptedException e) {
                throw new IllegalStateException("Unable to parse: " + result.scanFile);
            }
        }
    }
}

单目录和多目录都先提交解析任务,再由当前扫描线程消费结果;下面的说明比较两种消费顺序。

单目录使用 take() 逐个消费完成结果;多目录使用 OrderedResult.future 按提交顺序等待。两者都把状态注册集中到 processParseResult(),避免 parser 完成先后改变 package conflict 的结果。

7.4 解析失败的处理 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void processParseResult(ParallelPackageParser.ParseResult result) {
    Throwable throwable = result.throwable;
    int errorCode = PackageManager.INSTALL_SUCCEEDED;
    String errorMsg = null;
    ScanParams scanParams = result.scanParams;

    if (throwable == null) {
        try {
            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "addForInitLI");
            addForInitLI(result.parsedPackage, scanParams.parseFlags,
                    scanParams.scanFlags,
                    new UserHandle(UserHandle.USER_SYSTEM), scanParams.apexInfo);
        } catch (PackageManagerException e) {
            errorCode = e.error;
            errorMsg = "Failed to scan " + result.scanFile + ": " + e.getMessage();
            Slog.w(TAG, errorMsg);
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }
    } else if (throwable instanceof PackageManagerException) {
        PackageManagerException e = (PackageManagerException) throwable;
        errorCode = e.error;
        errorMsg = "Failed to parse " + result.scanFile + ": " + e.getMessage();
        Slog.w(TAG, errorMsg);
    } else {
        throw new IllegalStateException("Unexpected exception occurred while parsing "
                + result.scanFile, throwable);
    }
}

解析失败不总是终止启动:可预期的 PackageManagerException 会转换成安装错误码和 warning,交给扫描流程继续处理;未预期的 Throwable 则升级为 IllegalStateException。具体是否移除旧 PackageSetting、保留 disabled state 或触发 cleanup,要继续追 addForInitLI() 和 system fix owner。

8. 扫描收尾 ​

8.1 Stub与旧更新 ​

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

java
// do this first before mucking with mPackages for the "expecting better" case
updateStubSystemAppsList(mStubSystemApps);
mInstallPackageHelper.prepareSystemPackageCleanUp(
        packageSettings, mPossiblyDeletedUpdatedSystemApps, mExpectingBetter, userIds);

logSystemAppsScanningTime(startTime);

stub system app 列表必须在处理 expecting better 前收集,因为后续 cleanup 会修改 mPackages。这类顺序依赖是扫描总览中最容易被“目录遍历”叙述遗漏的部分。

8.2 System修复 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void fixSystemPackages(@NonNull int[] userIds) {
    mInstallPackageHelper.cleanupDisabledPackageSettings(
            mPossiblyDeletedUpdatedSystemApps, userIds, mScanFlags);
    mInstallPackageHelper.checkExistingBetterPackages(
            mExpectingBetter, mStubSystemApps, mSystemScanFlags, mSystemParseFlags);

    // Uncompress and install any stubbed system applications.
    // This must be done last to ensure all stubs are replaced or disabled.
    mInstallPackageHelper.installSystemStubPackages(mStubSystemApps, mScanFlags);
}

收尾顺序是:

  1. 清理不再存在的 disabled package settings;
  2. 检查 OTA 中期望的 better package 和 stub 替换关系;
  3. 最后解压/安装 stub system app,确保 stub 已被替换或禁用。

stub 安装必须最后执行,否则前面检查时可能把一个尚未替换的 stub 当作最终 system package。

8.3 重命名清理 ​

initNonSystemApps() 扫描和 fix 完成后调用:

java
mExpectingBetter.clear();
mPm.mSettings.pruneRenamedPackagesLPw();

mExpectingBetter 是本次启动扫描的临时状态,完成 fix 后清空;重命名 package 的持久化记录则由 Settings 在所有 package 处理结束后裁剪。清理动作发生在 data/app 阶段末尾,说明重命名状态必须等 system/data 两侧都扫描完才能判断是否仍被引用。

8.4 usage 与扫描结束 ​

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

java
// Now that we know all the packages we are keeping,
// read and update their last usage times.
mPackageUsage.read(packageSettings);

EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SCAN_END,
        SystemClock.uptimeMillis());
Slog.i(TAG, "Time to scan packages: "
        + ((SystemClock.uptimeMillis() - startTime) / 1000f)
        + " seconds");

usage 读取放在“确定要保留哪些包”之后,避免把已清理或已被替换的 Setting 当成当前 package。BOOT_PROGRESS_PMS_SCAN_END 是整个 system/data 扫描与修复结束的时间点,不是 APEX parse 或 /data/app list 完成的时间点。

8.5 system scan 统计 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void logSystemAppsScanningTime(long startTime) {
    mCachedSystemApps = PackageCacher.sCachedPackageReadCount.get();
    mPm.mSettings.pruneSharedUsersLPw();
    mSystemScanTime = SystemClock.uptimeMillis() - startTime;
    mSystemPackagesCount = mPm.mPackages.size();
    Slog.i(TAG, "Finished scanning system apps. Time: " + mSystemScanTime
            + " ms, packageCount: " + mSystemPackagesCount
            + " , timePerPackage: "
            + (mSystemPackagesCount == 0 ? 0 : mSystemScanTime / mSystemPackagesCount)
            + " , cached: " + mCachedSystemApps);
}

system scan 统计前还会 pruneSharedUsersLPw(),删除没有关联 package 的 shared UID。统计中的 package count 是当前 mPackages.size(),不是磁盘 APK 文件数;cache 命中数来自 PackageCacher 全局计数。

8.6 data scan 统计 ​

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

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void logNonSystemAppScanningTime(long startTime) {
    final int cachedNonSystemApps = PackageCacher.sCachedPackageReadCount.get()
            - mCachedSystemApps;
    final long dataScanTime = SystemClock.uptimeMillis() - mSystemScanTime - startTime;
    final int dataPackagesCount = mPm.mPackages.size() - mSystemPackagesCount;
    Slog.i(TAG, "Finished scanning non-system apps. Time: " + dataScanTime
            + " ms, packageCount: " + dataPackagesCount
            + " , timePerPackage: "
            + (dataPackagesCount == 0 ? 0 : dataScanTime / dataPackagesCount)
            + " , cached: " + cachedNonSystemApps);
}

data scan 时间通过总耗时减去 system scan 时间计算;data package count 是最终 map count 与 system count 的差。它反映阶段统计,不代表每个 package 的 parse 时间,也不包含后续 systemReady 工作。

9. 锁与失败边界 ​

9.1 双锁要求 ​

initSystemApps()、initNonSystemApps()、scanSystemDirs()、scanDirTracedLI()、parallelScanDirTracedLI() 和 InstallPackageHelper 的核心扫描方法都标注:

java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})

这与 PMS012 的锁序合同一致:先持有 mInstallLock,再使用 mLock 保护 package state;不能在只持有 mLock 时进入 Installer/扫描长操作。扫描是启动阶段的特例,临界区可能很长,但调用方仍必须遵守统一顺序。

9.2 解析并行不绕过锁 ​

ParallelPackageParser 的 worker 主要读取 APK 并构造 parse result;真正调用 addForInitLI()、更新 Settings、修改 mPackages 的 processParseResult() 仍在双锁保护下执行。若把 addForInitLI() 放到 parser worker,package conflict、shared UID、resolver 注册和 snapshot watcher 都会失去顺序保证。

9.3 错误分级 ​

阶段错误处理
/data/app mode 修复ErrnoException/目录遍历异常warning,继续扫描
单 APK parsePackageManagerException记录 install error,按扫描流程继续/cleanup
未预期 parser Throwable非 PackageManagerExceptionIllegalStateException,终止当前扫描
framework package 缺失mPackages 没有 androidIllegalStateException,PMS 启动失败
APEX parse/registerparse 或 addForInitLI() 失败IllegalStateException,不能静默跳过
parser executor 未完成shutdownNow() 返回未完成任务IllegalStateException

这些错误的 owner 不同:目录权限修复属于安全加固,可降级;核心 framework/APEX 属于系统启动基础,必须硬失败;普通 APK 解析错误则通过 install error 和后续 cleanup 处理。

9.4 扫描后阶段 ​

扫描结束后 PMS 还会解析 required verifier/installer、修复 protected filter priority、更新 shared libraries、修复 shared user seInfo/processes、读取 usage、挂载 permission storage volume,并继续 systemReady 流程。BOOT_PROGRESS_PMS_SCAN_END 只标记包扫描阶段结束,不能当作“所有系统服务已可使用”。

10. 测试与证明范围 ​

10.1 ScanTests ​

源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java

java
final ApplicationInfo applicationInfo = PackageInfoUtils.generateApplicationInfo(
        pkgSetting.getPkg(), 0, pkgSetting.getUserStateOrDefault(0), 0, pkgSetting);
assertBasicApplicationInfo(scanResult, applicationInfo);

ScanTests 直接使用扫描结果中的 PackageSetting 和 AndroidPackage 生成 ApplicationInfo,断言 ABI、路径、flags 等派生字段。它证明的是 scan result 到 framework object 的转换,不证明 PMS 构造函数真的按 APEX→system→data 顺序运行。

10.2 Parser测试 ​

测试文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java

应重点验证:提交多个 APK 后,每个 ParseResult.scanFile 与输入文件对应;解析异常保存在 throwable;orderedSubmit() 返回的 future 按提交顺序可等待。此类测试证明 parser 并行层的队列/Future 合同,不证明 InstallPackageHelper.processParseResult() 的锁和 Settings mutation。

10.3 PMS构造测试 ​

PackageManagerService 的测试构造函数允许注入 PackageManagerServiceTestParams,包括 InitAppsHelper、packages、parser、ApexManager 和 handler。测试可以验证:

  • 注入的 helper 收到同一个 parser 和 userIds;
  • constructor 结束后 parser 被 close;
  • system package 缺失时抛出失败;
  • OTA 语境下 SCAN_FIRST_BOOT_OR_UPGRADE 生效。

但 mock helper 会绕过真实目录扫描、Watchable、Settings persistence 和 executor,因此不能替代设备上的启动扫描集成测试。

10.4 真实设备验证 ​

要证明本文的完整链路,至少需要组合:

  1. logcat/EventLog 中的 BOOT_PROGRESS_PMS_DATA_SCAN_START 与 BOOT_PROGRESS_PMS_SCAN_END;
  2. dumpsys package apex 的 active/inactive/factory APEX 分组;
  3. dumpsys package <package> 中 system/data package 的 path、flags、user state;
  4. parser cache 命中统计和 system/data scan timing;
  5. packages.xml、package list、shared UID 和 renamed package 的最终状态;
  6. 对一个故意损坏 APK 或缺失 framework package 的失败启动实验。

单独看到 Packages: 输出只能证明 dump 阶段存在 package state,不能证明每个扫描阶段的顺序和失败策略。

11. 阅读路线 ​

阅读 Android 17 包扫描源码时,按下面顺序可以避免迷失在 PackageManagerService 的大类中:

  1. 从 PMS 构造函数定位 VersionInfo、mIsUpgrade、mInitAppsHelper 和 parser 创建。
  2. 进入 InitAppsHelper 构造函数,记录 mScanFlags、system flags、分区列表和 executor。
  3. 先读 getSystemScanPartitions()/ScanPartition,确认普通分区和 APEX 派生分区的 flags。
  4. 读 initSystemApps(),明确 APEX scan、notifyScanResult()、system dirs、overlay config 和 cleanup 顺序。
  5. 读 scanSystemDirs(),把 overlay/framework/priv-app/app 转成 ScanParams,再进入 parallelInstallPackagesFromDirs()。
  6. 读 initNonSystemApps(),记录 SCAN_REQUIRE_KNOWN、executor shutdown、system fix 和 renamed package prune。
  7. 进入 InstallPackageHelper 的 scanApexPackages()、installPackagesFromDir()、parallelInstallPackagesFromDirs() 和 processParseResult(),确认 parse 并行与状态顺序的分离。
  8. 最后追 fixSystemPackages()、usage read、permission/shared library 更新和 BOOT_PROGRESS_PMS_SCAN_END,判断“扫描完成”之后还剩哪些初始化。

小结 ​

Android 17 的系统包扫描是一条有顺序、有锁、有状态 owner 的启动管线:

  • PMS 先根据 VersionInfo 和 partition fingerprint 判断首次启动/OTA,准备 mExistingPackages、扫描 flags 和 parser。
  • InitAppsHelper 把固定 system partitions 与活跃 APEX 派生 partitions 合并成 ScanPartition,并用 flags 区分 vendor/product、factory APEX、APEX 内 APK 和 cache drop。
  • 系统阶段先扫描 APEX,再扫描 overlay、framework、priv-app、app;APEX 信息必须先通知 manager,framework package 缺失则硬失败。
  • ScanParams 把目录、parse flags、scan flags 和 APEX context 传给 InstallPackageHelper;parser 可以并行,processParseResult()/addForInitLI() 仍按确定顺序在双锁下注册状态。
  • data/app 阶段使用 SCAN_REQUIRE_KNOWN,可在首次启动/OTA 时修复目录 mode,扫描完成后关闭 parser executor,再执行旧 updated system app、better package、stub 和 renamed package 修复。
  • APEX 从 apexd 镜像状态重新扫描,factory 优先处理;parse/register 失败和未完成 parser 任务会终止扫描。
  • 扫描统计、usage 读取和 BOOT_PROGRESS_PMS_SCAN_END 只描述 package scan 阶段,后面仍有权限、shared library、required package 和 systemReady 初始化。
  • 教材级调试必须把源码阶段、flags、锁、状态 owner、失败分支和最终 dump/文件状态连起来,不能把“目录里发现 APK”当作“包已生效”。

下一篇 PMS017 将从 scanDirTracedLI() 进入单目录扫描,详细拆解 scanDirectoryForFilesToParse()、stage name 排除、PackageCacher、ParallelPackageParser 和 processParseResult() 的输入输出。