扫描并行化
本文面向已经读过 scanDirTracedLI、ScanRequest 与 Result 的读者,继续追踪 PMS 如何把多个 APK 的解析工作交给线程池,又如何把解析结果重新带回受保护的包注册路径。前文解释单目录输入和单包请求/结果;本文只讨论“多包之间怎样并行、结果怎样收敛”,不重新讲 ParsedPackage 字段解析。
Android 17 的实现不是简单地把 addForInitLI() 放进线程池。ParallelPackageParser 只负责调用 PackageParser2 并传递 ParseResult;InstallPackageHelper 决定目录项是否提交、选择无序队列还是有序 Future,然后在持有 mPm.mInstallLock 与 mPm.mLock 的路径中逐个处理结果。解析可以并行,policy、Settings、组件和共享库注册仍然在调用方的顺序与锁边界内发生。
读完后,读者应能回答几个实际问题:为什么单目录扫描可以按完成顺序消费,而系统多目录扫描必须按提交顺序消费;有界队列的背压发生在哪里;工作线程的异常怎样跨线程传回;shutdownNow() 清理的是什么;以及一次 APK 解析失败为什么可能删除 data 包,却不会删除 system 分区输入。
1. 并行边界
1.1 解析边界
源码文件:
frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.javaframeworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
并行化的 owner 和消费者可以分成四层:
| 层 | Owner | 并行做什么 | 结果由谁消费 |
|---|---|---|---|
| 目录枚举 | InstallPackageHelper | 遍历 File[]、过滤 APK/目录和 stage name | ParallelPackageParser |
| APK 解析 | ParallelPackageParser 的线程池 | 调用 PackageParser2.parsePackage() | ParseResult 或 Future |
| 扫描注册 | InstallPackageHelper | addForInitLI()、签名/设置/reconcile | PMS lock 保护的状态 |
| 线程池生命周期 | InitAppsHelper | 创建、复用、最终关闭 executor | system/data 扫描阶段 |
这个划分避免了一个常见误判:线程池中完成的是 parser 工作,不是 package registration。ParsedPackage 即使已经解析成功,也还没有进入 mPm.mPackages,更没有完成 AppId、共享库和组件注册。
1.2 两种消费模型
Android 17 有两个不同入口:
installPackagesFromDir()面向一次目录扫描,使用submit()把结果放入ArrayBlockingQueue,调用take()按任务完成顺序消费。parallelInstallPackagesFromDirs()面向 system overlay/framework/app/priv-app 等多个目录,使用orderedSubmit()保存每个Future,最后按目录列表和文件提交顺序future.get()。
前者优先让消费线程尽快拿到任何完成的 APK;后者允许所有目录同时解析,却保留 scanParamsList 的提交顺序,避免系统包注册顺序因磁盘和 CPU 时序变化而漂移。
1.3 性能边界
源码没有承诺固定的启动时间收益,也没有把线程数做成设备配置项。性能影响取决于 APK 数量、Manifest/资源大小、文件系统和 parser cache 命中情况;代码能直接证明的是并行任务数量、有界结果缓存、提交/消费顺序和 trace 名称。任何“加速了几倍”的结论都需要设备实验,不能从 MAX_THREADS = 4 推导出来。
2. 线程池模型
2.1 常量与字段
源码文件:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java
/**
* Helper class for parallel parsing of packages using {@link PackageParser2}.
* <p>Parsing requests are processed by a thread-pool of {@link #MAX_THREADS}.
* At any time, at most {@link #QUEUE_CAPACITY} results are kept in RAM</p>
*/
class ParallelPackageParser {
private static final int QUEUE_CAPACITY = 30;
private static final int MAX_THREADS = 4;
private volatile String mInterruptedInThread;
private final BlockingQueue<ParseResult> mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
private final PackageParser2 mPackageParser;
private final ExecutorService mExecutorService;MAX_THREADS 限制同时运行的 parser 任务数,QUEUE_CAPACITY 限制无序模型中已经完成但尚未被消费的结果数。队列只保存 ParseResult 引用,结果中的 ParsedPackage 仍可能占用较多内存;有界队列避免生产者无限制地把解析结果堆在内存中。
mInterruptedInThread 是跨线程可见的中断标记。它不是取消 token,也不记录每个任务的取消状态;它只用于工作线程在向队列 put() 时被中断后,通知后续 take() 不要永久等待。
2.2 executor 创建
源码文件:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java
static ExecutorService makeExecutorService() {
return ConcurrentUtils.newFixedThreadPool(MAX_THREADS, "package-parsing-thread",
Process.THREAD_PRIORITY_FOREGROUND);
}
ParallelPackageParser(PackageParser2 packageParser, ExecutorService executorService) {
mPackageParser = packageParser;
mExecutorService = executorService;
}线程池通过构造函数注入,ParallelPackageParser 自己不创建也不关闭 executor。InitAppsHelper 在构造时调用 makeExecutorService(),随后把同一个 executor 传给 APEX、system 分区和 data 分区扫描。这个生命周期设计使 parser helper 可以短生命周期创建多个实例,而线程池仍由启动助手统一管理。
源码文件: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();
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();
}线程优先级由 ConcurrentUtils.newFixedThreadPool() 的参数指定为 THREAD_PRIORITY_FOREGROUND。这只能说明 parser 线程创建时使用该优先级,不能说明系统启动中所有扫描工作都以同一优先级运行,因为消费、锁等待、I/O 和 commit 仍发生在调用线程。
2.3 结果结构
源码文件:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java
static class ParseResult {
ParsedPackage parsedPackage; // Parsed package
File scanFile; // File that was parsed
Throwable throwable; // Set if an error occurs during parsing
ScanParams scanParams; // Parameters used when scanning and parsing this scanFile
@Override
public String toString() {
return "ParseResult{" +
"parsedPackage=" + parsedPackage +
", scanFile=" + scanFile +
", throwable=" + throwable +
'}';
}
}
static class OrderedResult {
public final File scanFile;
public final Future<ParseResult> future;
OrderedResult(File scanFile, Future<ParseResult> future) {
this.scanFile = scanFile;
this.future = future;
}
}ParseResult 同时携带输入文件、原始 ScanParams、成功结果或异常。这样消费端可以在解析失败时仍知道具体 code path 和 scan flags,并在处理 data 包、APEX 内 APK 或 system 包时走不同清理/报告分支。OrderedResult 不复制解析结果,只保存文件路径和一个 Future;它把“顺序”交给调用方的 Future.get() 循环。
3. 任务生产
3.1 无序提交
源码文件:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java
public void submit(File scanFile, ScanParams scanParams) {
mExecutorService.submit(() -> {
ParseResult pr = new ParseResult();
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallel parsePackage [" + scanFile + "]");
try {
pr.scanFile = scanFile;
pr.scanParams = scanParams;
pr.parsedPackage = parsePackage(scanFile, scanParams.parseFlags);
} catch (Throwable e) {
pr.throwable = e;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
try {
mQueue.put(pr);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// Propagate result to callers of take().
// This is helpful to prevent main thread from getting stuck waiting on
// ParallelPackageParser to finish in case of interruption
mInterruptedInThread = Thread.currentThread().getName();
}
});
}任务线程把 parser 异常捕获到 ParseResult.throwable,而不是直接让 executor 的 Future 抛出;只有向有界队列放入结果时的中断才通过 mInterruptedInThread 影响消费者。Trace.traceBegin() 与 traceEnd() 包住 parser 调用,所以 systrace 能看到每个文件的 parallel parsePackage [...] 区间。
mQueue.put(pr) 是背压点:当 30 个结果已经等待读取时,完成解析的工作线程会阻塞在这里。此时线程池不会继续无限制地产生已完成结果,但未完成任务仍可能占用线程和文件描述符。
3.2 有序提交
源码文件:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java
public OrderedResult orderedSubmit(File scanFile, ScanParams scanParams) {
return new OrderedResult(scanFile, mExecutorService.submit(() -> {
ParseResult pr = new ParseResult();
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallel parsePackage [" + scanFile + "]");
try {
pr.scanFile = scanFile;
pr.scanParams = scanParams;
pr.parsedPackage = parsePackage(scanFile, scanParams.parseFlags);
} catch (Throwable e) {
pr.throwable = e;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
return pr;
}));
}有序提交不使用 mQueue。每个任务完成后把 ParseResult 设置到自己的 Future,调用方随后按保存的 OrderedResult 列表调用 get()。如果第一个提交的 APK 较慢,消费循环会在第一个 future 上等待,即使后面的 APK 已经解析完成;这正是“并行解析、按提交顺序注册”的代价。
3.3 parser 委托
源码文件:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java
@VisibleForTesting
protected ParsedPackage parsePackage(File scanFile, int parseFlags)
throws PackageManagerException {
try {
return mPackageParser.parsePackage(scanFile, parseFlags, true);
} catch (PackageParserException e) {
throw new PackageManagerException(e.error, e.getMessage(), e);
}
}ParallelPackageParser 不实现 Manifest 解析;它只把 parseFlags 和文件交给注入的 PackageParser2,并把 PackageParserException 转成 PMS 使用的 PackageManagerException。因此 parser 线程的异常类型可以分成“预期的包解析失败”和“未预期的其他 Throwable”,消费端会区别处理。
4. 目录扫描
4.1 单目录入口
源码文件: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());
}
}单目录入口在调用方锁保护下创建 helper,先根据是否为 APK-in-APEX 构造 ScanParams,再枚举并提交文件。fileCount 是成功提交的任务数,不是目录中文件总数;空目录、非 APK 文件和 stage name 都不会增加它。消费循环恰好调用同样次数的 take(),因此正常情况下不会因过滤项等待不存在的结果。
4.2 文件过滤
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private int scanDirectoryForFilesToParse(
ParallelPackageParser parallelPackageParser, ScanParams scanParams) {
final File[] files = scanParams.scanDir.listFiles();
if (ArrayUtils.isEmpty(files)) {
Log.d(TAG, "No files in app dir " + scanParams.scanDir);
return 0;
}
int fileCount = 0;
for (File file : files) {
final boolean isPackage = (isApkFile(file) || file.isDirectory())
&& !PackageInstallerService.isStageName(file.getName());
if (!isPackage) {
continue;
}
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);
fileCount++;
}
return fileCount;
}过滤发生在进入线程池之前。目录包(例如 cluster APK)也会提交,因此不能把 isApkFile(file) 当成唯一条件。SCAN_DROP_CACHE 的 cache 清理同样在提交前完成;它不在线程池内异步清理,避免 parser 读取旧缓存与清理操作竞态。
4.3 多目录入口
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.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 all 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);
}
}
}
}多目录入口先把所有目录的任务提交完,再开始消费任何结果。resultsList 的外层顺序对应 scanParamsList,内层顺序对应单目录枚举时调用 orderedSubmit() 的顺序;因此 overlay/framework/app/priv-app 的收集顺序在 parser 完成时仍被保留。
这里的 ExecutionException 会把 parser task 的异常包在 future 中,方法统一转成 IllegalStateException;与无序模型不同,ParseResult.throwable 不会通过 processParseResult() 的常规分支处理。多目录入口的异常边界必须单独阅读,不能把两种模型的错误处理混为一谈。
5. 结果消费
5.1 队列读取
源码文件:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java
public ParseResult take() {
try {
if (mInterruptedInThread != null) {
throw new InterruptedException("Interrupted in " + mInterruptedInThread);
}
return mQueue.take();
} catch (InterruptedException e) {
// We cannot recover from interrupt here
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
}take() 在队列为空时阻塞;消费线程如果自己被中断,源码选择恢复 interrupt 标志并抛出 IllegalStateException,而不是返回部分结果继续扫描。如果工作线程在 put() 时被中断,mInterruptedInThread 会让下一次 take() 立即走同一异常路径,避免主线程永远等待一个永远不会入队的结果。
5.2 解析结果处理
源码文件: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);
}
if ((scanParams.scanFlags & SCAN_AS_APK_IN_APEX) != 0
&& errorCode != INSTALL_SUCCEEDED) {
mApexManager.reportErrorWithApkInApex(scanParams.scanDir.getAbsolutePath(), errorMsg);
}
if ((scanParams.scanFlags & SCAN_AS_SYSTEM) == 0
&& errorCode != PackageManager.INSTALL_SUCCEEDED) {
logCriticalInfo(Log.WARN,
"Deleting invalid package at " + result.scanFile + " (" + errorMsg + ")");
mRemovePackageHelper.removeCodePath(result.scanFile);
}
}结果处理有三条分支:
throwable == null:parser 成功,进入addForInitLI();如果扫描/注册失败,日志前缀是Failed to scan。PackageManagerException:parser 失败,日志前缀是Failed to parse,不会进入addForInitLI()。- 其他
Throwable:认为是不可预期错误,直接抛IllegalStateException,不静默吞掉。
清理条件只删除非 SCAN_AS_SYSTEM 的无效 code path。也就是说 data/app 解析失败会触发 removeCodePath(),system/vendor/product 等系统分区输入不会因为同一分支被删除。APK-in-APEX 失败还会先报告给 ApexManager,让 APEX owner 知道具体 module path 和错误。
5.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<>();
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);
}scanSystemDirs() 先构造完整的 scanParamsList,再一次性调用 parallelScanDirTracedLI()。并行化改变的是每个目录内部的解析执行时机,不改变这个列表的顺序:overlay 仍倒序收集,framework 仍在 app/priv-app 之前,partition 的 priv-app 仍在该 partition 的 app 之前。
6. 生命周期
6.1 system 与 data
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
public OverlayConfig initSystemApps(PackageParser2 packageParser,
WatchedArrayMap<String, PackageSetting> packageSettings,
int[] userIds, long startTime) {
final List<ApexManager.ScanResult> apexScanResults = scanApexPackagesTraced(packageParser);
mApexManager.notifyScanResult(apexScanResults);
scanSystemDirs(packageParser, mExecutorService);
// overlay configuration and system package cleanup follow...
}
public void initNonSystemApps(PackageParser2 packageParser, @NonNull int[] userIds,
long startTime) {
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();
}同一个 executor 先服务 APEX 和 system 分区,再服务 data/app。initNonSystemApps() 完成 data 目录扫描后调用 shutdownNow(),并检查返回的未开始任务列表;如果还有任务未完成,直接抛出异常。源码没有在每个 ParallelPackageParser helper 上调用 shutdown,因为 helper 不拥有 executor 生命周期。
6.2 锁与等待
installPackagesFromDir()、parallelInstallPackagesFromDirs() 和目录枚举方法都标记为同时受 mPm.mInstallLock、mPm.mLock 保护;take() 或 future.get() 发生在这条调用路径中。结果消费因此可能在 PMS 锁下等待 parser 任务。
这不是遗漏锁,而是当前实现的边界:parser 工作线程主要操作 PackageParser2,消费线程需要在锁下进入 addForInitLI()、Settings 和注册逻辑。分析启动卡顿时,应区分“parser 线程还在读文件”和“消费线程持锁等待首个 Future/队列结果”两种时间段。
6.3 cache 与背压
SCAN_DROP_CACHE 的缓存删除发生在任务提交前,ArrayBlockingQueue 的阻塞发生在解析完成后。二者解决不同问题:前者保证 parser 不读取指定旧缓存,后者限制解析结果积压。调大队列容量不会消除 APK 解析 I/O,也不会改变注册顺序;调大线程数也不能绕过 future.get() 对慢任务的顺序等待。
7. 测试与实验
7.1 并发完成序
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java
@Before
public void setUp() {
mParser = new TestParallelPackageParser(new TestPackageParser2(),
ParallelPackageParser.makeExecutorService());
}
@Test(timeout = 1000)
public void test() {
Set<File> submittedFiles = new HashSet<>();
int fileCount = 15;
for (int i = 0; i < fileCount; i++) {
File file = new File("f" + i);
mParser.submit(file, ScanParams.forApkPartitionScan(null, 0, 0));
submittedFiles.add(file);
}
for (int i = 0; i < fileCount; i++) {
ParallelPackageParser.ParseResult result = mParser.take();
Assert.assertNotNull(result);
File parsedFile = result.scanFile;
Assert.assertNotNull(parsedFile);
boolean removeSuccessful = submittedFiles.remove(parsedFile);
Assert.assertTrue("Unexpected file " + parsedFile
+ ". Expected submitted files: " + submittedFiles, removeSuccessful);
}
}
private class TestParallelPackageParser extends ParallelPackageParser {
@Override
protected ParsedPackage parsePackage(File scanFile, int parseFlags) {
// Do not actually parse the package for testing
return null;
}
}测试安排 15 个任务、逐个 submit(),动作是调用 15 次 take(),断言是每个返回的 scanFile 都来自已提交集合并最终全部移除。它证明无序队列不会丢结果、重复结果或返回未提交文件;它没有证明返回顺序等于提交顺序,也没有证明真实 PackageParser2 的解析性能。
7.2 ScanTests 结果
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
ScanTests 通过测试 builder 直接调用 scanPackageOnly(),验证的是并行解析完成后的单包结果,而不是线程调度本身。可用于交叉验证并行管线消费者的测试包括:
newInstallSimpleAllNominal:没有旧设置时断言mExistingSettingCopied == false。updateSimpleNominal:注入已有 ABI,断言结果设置与输入对象不是同一引用,证明扫描副本可供后续提交。installDynamicLibraries:断言 dynamic library 名称、类型和 base/split code paths,证明结果对象仍携带库声明。
这些测试的输入、断言和证明范围属于 ScanPackageUtils;它们不能替代 ParallelPackageParserTest 对队列消费的验证,也不能证明 parallelInstallPackagesFromDirs() 的跨目录顺序。
7.3 设备实验
要测量真实设备上的并行边界,可用启动 trace 和日志组合观察:
adb shell atrace --async_start package_manager sched freq idle
adb shell logcat -s PackageManager PackageManagerService
adb shell dumpsys package packages
adb shell atrace --async_stop -z -o /data/local/tmp/pms-scan.html实验时应记录:
parallel parsePackage [...]trace 区间是否重叠,说明 parser 任务是否真正并行执行;parallelScanDir、scanDir和addForInitLI区间的先后,说明消费是否受顺序等待;Finished scanning system apps. Time: ... cached: ...日志中的总耗时与 cache 计数;- 解析失败时是
Failed to parse还是Failed to scan,以及 data code path 是否被清理。
这些实验可以证明某台设备某次启动的时序和耗时,不能外推所有硬件或所有 APK 集合。尤其不要把 parser trace 的并行重叠直接当成包已经按并行顺序注册。
8. 故障边界
8.1 parser 异常
parsePackage() 把 PackageParserException 包装成 PackageManagerException,无序消费会把它转换成安装错误码并继续处理清理;未知 Throwable 则升级为 IllegalStateException。如果只看到“扫描失败”日志,先确认错误前缀和 result.throwable 来源,才能判断失败发生在 parser 还是 addForInitLI()。
8.2 中断路径
工作线程在 mQueue.put() 被中断时,只设置 mInterruptedInThread 并恢复自身 interrupt;它没有把一个特殊 ParseResult 入队。消费者下一次 take() 看到标记后主动抛出异常。这个设计避免队列容量耗尽或线程池关闭时,主线程永远阻塞在等待结果。
8.3 未完成任务
shutdownNow() 返回尚未开始的 Runnable 列表。Android 17 的 initNonSystemApps() 要求这个列表为空,否则抛出 IllegalStateException。因此“扫描结束”不仅意味着目录循环退出,还意味着 executor 没有遗留未启动任务;已经开始但尚未完成的任务则必须在 shutdown 行为和 trace 中进一步定位。
8.4 data 清理
processParseResult() 只对不带 SCAN_AS_SYSTEM 的失败结果调用 removeCodePath()。这保护了 system/vendor/product 等只读系统输入,却会清理 data/app 中无法解析或扫描失败的用户包。恢复系统包的后续逻辑由 fixSystemPackages() 和 checkExistingBetterPackages() 负责,不是 parser 线程的职责。
9. 源码路线
定位并行扫描问题时,可以沿下面的顺序阅读:
InitAppsHelper构造函数:确认 executor 创建和初始 scan flags。scanSystemDirs()或initNonSystemApps():确认单目录/多目录入口和关闭时机。InstallPackageHelper.installPackagesFromDir()、parallelInstallPackagesFromDirs():确认消费模型。scanDirectoryForFilesToParse()、orderedScanDirectoryForFilesToParse():确认过滤、cache 清理和提交数量。ParallelPackageParser.submit()、orderedSubmit()、take():确认任务、异常和中断传播。processParseResult():确认 parse/scan 错误分类和 data 清理。addForInitLI()与 ScanRequest 与 Result:确认结果怎样进入设置、reconcile 和 commit。
一个实用的复述练习是:给定 /data/app/foo/base.apk 和 /system/app/bar.apk 同时解析失败,分别指出它们会在哪个消费分支记录错误、是否调用 removeCodePath()、哪个锁保护注册,以及哪一段代码负责下一次启动恢复。能够回答这些问题,才算真正理解了“并行解析”与“有序状态提交”的边界。
10. 设计收束
Android 17 的并行扫描可以归纳为一条严格的数据流:
InitAppsHelper拥有 executor 生命周期,ParallelPackageParser只借用它执行 parser 任务。submit()通过容量 30 的ArrayBlockingQueue按完成顺序交付结果;orderedSubmit()通过Future把结果交给调用方按提交顺序读取。InstallPackageHelper在提交前过滤文件并按需删除 parser cache,在消费时进入addForInitLI(),所以 parser 并行不等于注册并行。ParseResult区分ParsedPackage、文件、scan params 和异常;processParseResult()再区分 parse 失败、scan 失败和未知异常。- system 失败不会走 data code path 清理;APK-in-APEX 失败会额外通知
ApexManager;data 扫描结束后 executor 必须没有未开始任务。 - 测试证明了队列结果完整性和单包扫描结果语义,设备 trace 才能证明具体硬件上的任务重叠与等待时间。
后续文章会继续把并行结果带入扫描时的签名校验,解释为什么证书收集、已知设置和系统包信任边界仍然由消费/校验阶段决定,而不是由 parser 线程提前决定。
