系统包扫描
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
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
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
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
/**
* 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
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
/**
* 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
/** 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
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
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
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
@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
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
@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");
}
}目录参数按四步加入列表:
- 各 system partition 的 overlay 目录,倒序收集;
/system/framework,附加SCAN_NO_DEX | SCAN_AS_PRIVILEGED;- 各 partition 的 priv-app 目录,附加
SCAN_AS_PRIVILEGED; - 各 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
/**
* 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
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
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
/**
* 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
@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
@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
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
@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
@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 列表。
@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
@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
// 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
@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);
}收尾顺序是:
- 清理不再存在的 disabled package settings;
- 检查 OTA 中期望的 better package 和 stub 替换关系;
- 最后解压/安装 stub system app,确保 stub 已被替换或禁用。
stub 安装必须最后执行,否则前面检查时可能把一个尚未替换的 stub 当作最终 system package。
8.3 重命名清理
initNonSystemApps() 扫描和 fix 完成后调用:
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
// 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
@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
@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 的核心扫描方法都标注:
@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 parse | PackageManagerException | 记录 install error,按扫描流程继续/cleanup |
| 未预期 parser Throwable | 非 PackageManagerException | IllegalStateException,终止当前扫描 |
| framework package 缺失 | mPackages 没有 android | IllegalStateException,PMS 启动失败 |
| APEX parse/register | parse 或 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
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 真实设备验证
要证明本文的完整链路,至少需要组合:
logcat/EventLog 中的BOOT_PROGRESS_PMS_DATA_SCAN_START与BOOT_PROGRESS_PMS_SCAN_END;dumpsys package apex的 active/inactive/factory APEX 分组;dumpsys package <package>中 system/data package 的 path、flags、user state;- parser cache 命中统计和 system/data scan timing;
packages.xml、package list、shared UID 和 renamed package 的最终状态;- 对一个故意损坏 APK 或缺失 framework package 的失败启动实验。
单独看到 Packages: 输出只能证明 dump 阶段存在 package state,不能证明每个扫描阶段的顺序和失败策略。
11. 阅读路线
阅读 Android 17 包扫描源码时,按下面顺序可以避免迷失在 PackageManagerService 的大类中:
- 从 PMS 构造函数定位
VersionInfo、mIsUpgrade、mInitAppsHelper和 parser 创建。 - 进入
InitAppsHelper构造函数,记录mScanFlags、system flags、分区列表和 executor。 - 先读
getSystemScanPartitions()/ScanPartition,确认普通分区和 APEX 派生分区的 flags。 - 读
initSystemApps(),明确 APEX scan、notifyScanResult()、system dirs、overlay config 和 cleanup 顺序。 - 读
scanSystemDirs(),把 overlay/framework/priv-app/app 转成ScanParams,再进入parallelInstallPackagesFromDirs()。 - 读
initNonSystemApps(),记录SCAN_REQUIRE_KNOWN、executor shutdown、system fix 和 renamed package prune。 - 进入
InstallPackageHelper的scanApexPackages()、installPackagesFromDir()、parallelInstallPackagesFromDirs()和processParseResult(),确认 parse 并行与状态顺序的分离。 - 最后追
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() 的输入输出。
