scanDirTracedLI
PMS016 说明了系统启动包扫描的阶段顺序。本篇把镜头推进到单目录入口 scanDirTracedLI():它如何把一个目录和一组 flags 交给 InstallPackageHelper,后者如何决定一个目录项是不是 APK package、如何排除 PackageInstaller stage 目录、何时丢弃 parser cache,最后又如何让并行 parser 的结果回到受锁保护的 package registration 路径。
这条路径值得单独学习,因为“扫描一个目录”包含了四个不同层次:
- 入口与 trace:
InitAppsHelper.scanDirTracedLI()不解析文件,只建立调用边界和 trace section。 - 输入筛选:
scanDirectoryForFilesToParse()把目录项变成有效 scan task,过滤临时 stage、非 APK 和空目录。 - 解析调度:
ParallelPackageParser使用最多 4 个前台解析线程;take()按完成顺序返回,orderedSubmit()则保留提交顺序。 - 结果生效:
processParseResult()把ParsedPackage交给addForInitLI(),处理错误码、APEX 错误回报和无效 data package 清理。
本文只讨论 Android 17 android-17.0.0_r1。系统分区为何按 overlay/framework/priv-app/app 排列已经在 PMS016 讲过;system app、data app 和 ScanRequest/ScanResult 的专题会继续拆分。
1. 入口边界
1.1 入口编排
源码文件: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);
}
}方法有三个责任:
- 通过
@GuardedBy声明调用者必须持有mInstallLock和mLock; - 用目录绝对路径建立 trace 名称,便于启动性能分析;
- 把解析器、executor、parse flags、scan flags 和可选 APEX 信息原样交给
InstallPackageHelper。
它没有 listFiles()、没有 PackageParser2.parsePackage(),也没有直接修改 mPackages。因此调试一个目录没有被扫描时,应先看调用方传入的目录和 flags,再进入 InstallPackageHelper,而不是在该方法里寻找文件遍历代码。
1.2 多目录入口的差异
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
@GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
private void parallelScanDirTracedLI(List<ScanParams> scanParamsList,
PackageParser2 packageParser, ExecutorService executorService) {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallelScanDir");
try {
mInstallPackageHelper.parallelInstallPackagesFromDirs(scanParamsList,
packageParser, executorService);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}系统分区扫描走 parallelScanDirTracedLI(),data/app 单目录走 scanDirTracedLI()。两者最终都进入 InstallPackageHelper,区别在于:
- 单目录用
installPackagesFromDir(),通过结果队列take()按完成顺序消费; - 多目录用
parallelInstallPackagesFromDirs(),为每个文件保存Future,再按目录和提交顺序处理。
这保证了单目录可以简单高效地消费结果,而跨 overlay/framework/priv-app/app 的系统扫描仍保持分区优先级。
2. 参数封装
2.1 ScanParams 字段
源码文件: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;
}
}一个 ScanParams 把“在哪里找”“如何解析”“如何注册”“是否处于 APEX context”绑定在一起。解析线程只需要消费这个对象,不需要重新从 PMS 推断分区身份。
2.2 三个工厂
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
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);
}forApexDirScan() 给 APEX 文件自身扫描使用,scanDir 为 null;forApkPartitionScan() 是普通 system/data 分区目录;forApkInApexScan() 在普通 parse flags 上增加 PARSE_APK_IN_APEX,让 parser 对 APEX 内 APK 执行专门的 maxSdkVersion 处理。
2.3 flags 的真实来源
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
static final int SCAN_NO_DEX = 1 << 0;
static final int SCAN_NEW_INSTALL = 1 << 2;
static final int SCAN_BOOTING = 1 << 4;
static final int SCAN_REQUIRE_KNOWN = 1 << 7;
static final int SCAN_INITIAL = 1 << 9;
static final int SCAN_FIRST_BOOT_OR_UPGRADE = 1 << 12;
static final int SCAN_AS_SYSTEM = 1 << 16;
static final int SCAN_AS_PRIVILEGED = 1 << 17;
static final int SCAN_AS_APK_IN_APEX = 1 << 23;
static final int SCAN_DROP_CACHE = 1 << 24;
static final int SCAN_AS_FACTORY = 1 << 25;
static final int SCAN_AS_APEX = 1 << 26;单目录入口不会自行解释所有 flags,但 InstallPackageHelper 会使用它们决定 ScanParams 类型、是否 drop parser cache、是否作为 system/privileged/APEX 处理,以及 parse 失败后是否删除 data package。PMS016 的 system/data 阶段负责组合这些 flags;PMS017 只追它们如何沿目录进入 parser 和结果处理。
3. 单目录扫描
3.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());
}
}主循环的输入输出很清晰:
- 输入:一个目录、parse flags、scan flags、parser、线程池、可选 APEX info;
- 中间值:有效文件提交数量
fileCount; - 输出:每次
take()取出一个ParseResult,交给processParseResult(); - 锁:整个方法要求 install/package 双锁,解析任务本身在 worker 中执行,但结果注册仍在当前扫描线程的锁语境中完成。
fileCount 不是目录文件总数,而是成功提交给 parser 的 package 数量。空目录、stage 目录、非 APK 条目都不会增加它。
3.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;
}listFiles() 返回 null 或空数组都会走同一分支。返回 0 后 installPackagesFromDir() 不调用 take(),目录扫描正常结束。这里的 debug 日志不能区分“目录不存在”和“目录存在但为空”,需要结合 File/权限信息和调用方目录创建逻辑判断。
3.3 有效文件
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
int fileCount = 0;
for (File file : files) {
final boolean isPackage = (isApkFile(file) || file.isDirectory())
&& !PackageInstallerService.isStageName(file.getName());
if (!isPackage) {
// Ignore entries which are not packages
continue;
}
parallelPackageParser.submit(file, scanParams);
fileCount++;
}
return fileCount;一个目录项只有同时满足两个条件才提交:
isApkFile(file)为真,或者它是目录(目录可能代表 cluster/split package);- 文件名不是 PackageInstaller stage name。
因此 lib/、日志、临时文件和不符合 APK 后缀的普通文件会被忽略;目录并不自动意味着它是合法 package,真正的 Manifest 解析仍在 parser worker 中完成。
3.4 stage name 排除
stage 目录被排除不是为了性能,而是为了避免把尚未提交的安装会话当成系统已安装 package。PackageInstaller 在 staging 期间可能写入不完整 APK、split 或 metadata;这些文件必须等 session commit 流程通过后,进入正式安装扫描路径。
3.5 Cache处理
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if ((scanParams.scanFlags & SCAN_DROP_CACHE) != 0) {
final PackageCacher cacher = new PackageCacher(mPm.getCacheDir(),
mPm.mPackageParserCallback);
Log.w(TAG, "Dropping cache of " + file.getAbsolutePath());
cacher.cleanCachedResult(file);
}
parallelPackageParser.submit(file, scanParams);
fileCount++;SCAN_DROP_CACHE 通常来自 active APEX 变化等需要重新解析的场景。cache 清理发生在提交 parser 之前,并且针对当前文件,而不是清空整个 parser cache。清理失败没有在这里抛出专门异常;后续 parser 会重新读取文件,真正的 parse failure 由 ParseResult.throwable 承载。
3.6 单目录时序
4. 并行解析
4.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);
static ExecutorService makeExecutorService() {
return ConcurrentUtils.newFixedThreadPool(MAX_THREADS, "package-parsing-thread",
Process.THREAD_PRIORITY_FOREGROUND);
}
private final PackageParser2 mPackageParser;
private final ExecutorService mExecutorService;Android 17 的解析池固定最多 4 个前台优先级线程,结果队列容量为 30。队列容量限制的是“已完成但尚未被扫描线程取走”的结果数量,不是目录最多只能有 30 个文件;submit 可以继续提交,worker 在队列满时会阻塞 put()。
4.2 ParseResult
源码文件: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 +
'}';
}
}ParseResult 同时保存成功和失败信息:成功时 parsedPackage 有值,失败时 throwable 有值;两种情况下都保留 scanFile 和 scanParams,使主线程能生成带文件路径和 flags 的错误处理。解析线程不直接抛异常给扫描线程,而是把 Throwable 写入 result。
4.3 submit队列
源码文件: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().
mInterruptedInThread = Thread.currentThread().getName();
}
});
}worker 的顺序是:创建 result→设置文件和参数→调用 parser→捕获 Throwable→关闭 trace→把 result 放入队列。若 put() 被中断,线程记录中断线程名;下一个 take() 会先检查 mInterruptedInThread 并抛出 IllegalStateException,避免主扫描线程永久等待一个永远不会入队的结果。
4.4 take中断
源码文件: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() 是阻塞操作。Android 17 选择把中断视为不可恢复:恢复线程 interrupt 标志后包装成 IllegalStateException。扫描调用方因此不能在 catch 后继续使用同一个 parser/executor 假设任务完整;PMS016 的 executor shutdown 和构造失败路径会接管清理。
4.5 orderedSubmit
源码文件:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java
static class OrderedResult {
public final File scanFile;
public final Future<ParseResult> future;
OrderedResult(File scanFile, Future<ParseResult> future) {
this.scanFile = scanFile;
this.future = future;
}
}
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;
}));
}orderedSubmit() 不把 result 放入共享队列,而是返回带文件名的 Future。parallelInstallPackagesFromDirs() 按 resultsList 的目录顺序和每个目录内部的提交顺序调用 future.get();即使 fileB 先 parse 完,也要等 fileA 先被处理。
4.6 异常转换
源码文件: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);
}
}parser 层的 PackageParserException 被转成 PMS 使用的 PackageManagerException,保留 error code 和 cause。这样 processParseResult() 可以把解析失败纳入统一安装错误码逻辑,而无需依赖 parser 的具体异常类型。
5. 结果生效
5.1 结果入口
源码文件: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);
}结果处理先区分“parse 成功但注册失败”和“parse 阶段失败”:
throwable == null:进入addForInitLI(),失败信息前缀是Failed to scan;throwable是PackageManagerException:不再调用 add,前缀是Failed to parse;- 其他 Throwable:升级为
IllegalStateException,不按普通 APK 错误继续吞掉。
这保证解析器的异常不会被误报成 package registration 异常,同时把 error code 保留下来供后续清理判断。
5.2 APEX失败
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if ((scanParams.scanFlags & SCAN_AS_APK_IN_APEX) != 0
&& errorCode != INSTALL_SUCCEEDED) {
mApexManager.reportErrorWithApkInApex(
scanParams.scanDir.getAbsolutePath(), errorMsg);
}只有带 SCAN_AS_APK_IN_APEX 的结果才会向 ApexManager 报告错误。普通 system/data APK 的 parse failure 不会进入这个回报路径;APEX manager 可以据此把“容器内 APK 无法扫描”与普通 package 错误区分开。
5.3 Data清理
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
// Delete invalid userdata apps
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);
}
}错误 package 只有在不是 system scan时才删除 code path。也就是说,data/app 中解析/注册失败的包会被清理,避免下次启动继续读取同一个坏 APK;system package 失败则保留路径并由更高层 system scan/fix 逻辑决定是否终止或恢复。
5.4 结果处理时序
5.5 路径对比
| 路径 | 解析提交 | 结果消费 | 适用场景 | 顺序保证 |
|---|---|---|---|---|
installPackagesFromDir | submit() | take() | /data/app 或单个 APEX 内目录 | 完成顺序 |
parallelInstallPackagesFromDirs | orderedSubmit() | future.get() | system overlay/framework/priv-app/app | 目录和提交顺序 |
scanApexPackages | submit() | take() 后按 factory 排序 | APEX 文件自身 | factory 优先 |
“完成顺序”并不等于随机状态:单目录下每个结果仍在同一双锁路径中处理;只是 parser 哪个先完成,哪个先进入 processParseResult()。跨目录路径额外保留目录优先级,避免 overlay/framework/app 的先后被线程调度改变。
6. 锁与生命周期
6.1 双锁范围
scanDirTracedLI()、installPackagesFromDir()、scanDirectoryForFilesToParse()、orderedScanDirectoryForFilesToParse()、processParseResult() 都标注 @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})。这意味着:
- 文件列表读取和 parser task 提交也发生在双锁范围内;
- parser worker 解析 APK 时不持有 PMS Java 锁,但主线程在等待/处理结果时持有锁;
addForInitLI()、Settings mutation、resolver 注册和 cache invalidation 在锁内完成。
启动扫描可以接受较长临界区,因为必须保证 package state 的确定顺序;运行时安装/卸载则通过 Handler、freeze 和更细的 helper 路径尽量缩短锁内工作。
6.2 Trace闭合
入口和 worker 都用 try/finally 结束 trace:
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanDir [" + scanDir + "]");
try {
mInstallPackageHelper.installPackagesFromDir(...);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}worker 侧的 parse trace 同样在 parser 调用外包住 finally:
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallel parsePackage [" + scanFile + "]");
try {
pr.parsedPackage = parsePackage(scanFile, scanParams.parseFlags);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}因此 parse 异常、目录为空和 take() 中断都不会留下未闭合的 trace section。性能分析时应区分 scanDir 外层耗时、parallel parsePackage worker 耗时和 addForInitLI 状态注册耗时。
6.3 executor 关闭
initNonSystemApps() 在 data/app 扫描后调用:
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java
List<Runnable> unfinishedTasks = mExecutorService.shutdownNow();
if (!unfinishedTasks.isEmpty()) {
throw new IllegalStateException("Not all tasks finished before calling close: "
+ unfinishedTasks);
}shutdownNow() 返回尚未开始执行的任务;非空即表示 parser 生命周期没有正常收束。PMS 不会忽略这些任务继续完成构造,因为此时 package scan 的完整性无法证明。
7. 测试与证明范围
7.1 Parser测试
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java
该测试应围绕 parser 的三个可观察 contract 设计:
- 输入有效 APK,断言
ParseResult.scanFile、scanParams和parsedPackage对应; - 输入解析失败文件,断言
throwable是PackageManagerException,且take()能返回失败 result; - 通过
orderedSubmit()提交多个文件,延迟其中一个 parser,断言Future仍按提交顺序被消费。
它证明的是线程池/队列/Future 交接,不证明 addForInitLI() 是否修改 Settings,也不证明错误 data package 已从磁盘删除。
7.2 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);输入是扫描结果中的 package setting 和 parsed package,动作是生成 ApplicationInfo,断言 ABI、路径、flags 等派生字段。它覆盖“注册后的状态可以生成 framework 对象”,没有覆盖目录筛选、stage 排除、parser 并行顺序或无效 data package cleanup。
7.3 目录筛选测试
目录筛选测试应准备:空目录、普通 APK、split/cluster 目录、stage name 目录、非 APK 文件和 SCAN_DROP_CACHE flags。断言:
- 空目录返回 0,不调用
take(); - APK 和目录提交 parser;
- stage name 与普通文件不提交;
- drop cache 只清理当前文件对应 cache;
- 返回值等于实际提交数量。
当前测试目录中若只有 parser 或 scan result 单测,不能把它们扩大为 scanDirectoryForFilesToParse() 的完整证明。
7.4 设备启动验证
真实设备上可以组合以下证据:
BOOT_PROGRESS_PMS_DATA_SCAN_START、BOOT_PROGRESS_PMS_SCAN_END;parallel parsePackage和addForInitLItrace section;dumpsys package中 system/data package 的 path、flags 和 user state;- parser cache 命中/丢弃日志;
- 一个故意损坏的
/data/appAPK 是否被removeCodePath()清理; - APEX 内 APK 失败时是否出现
reportErrorWithApkInApex()对应错误。
这些组合可以验证阶段和结果,但仍不能证明每个 worker 的精确调度顺序,因为并行 parse 本身不承诺完成顺序。
8. 阅读路线
阅读一个新目录扫描调用点时,按下面顺序追:
- 找
scanDirTracedLI()或parallelScanDirTracedLI()的调用方,记录目录、parse flags、scan flags、APEX info 和锁。 - 进入
installPackagesFromDir()或parallelInstallPackagesFromDirs(),确认使用take()还是 ordered future。 - 进入
scanDirectoryForFilesToParse(),检查listFiles()、isApkFile()、目录允许条件、stage name 和 cache drop。 - 进入
ParallelPackageParser,记录线程数、队列容量、异常承载、interrupt 行为和 parser exception 转换。 - 进入
processParseResult(),区分 parse failure、registration failure、APEX error report 和 data code path deletion。 - 回到
addForInitLI()/scanPackageForInitLI(),确认 package state、resolver、permissions、shared libraries 和 snapshot invalidation 的实际 owner。 - 检查 executor shutdown、trace close、锁释放和失败路径是否完成。
- 最后用 parser、scan result、目录筛选和设备启动测试分别验证,不要把单一测试的通过范围扩大。
小结
Android 17 的 scanDirTracedLI() 是一个由锁、解析器和状态注册共同组成的目录扫描入口:
InitAppsHelper只负责 trace 和参数转发;真正的文件遍历在InstallPackageHelper。- 有效 package 是 APK 文件或目录,且不能是 PackageInstaller stage name;空目录直接返回 0。
SCAN_DROP_CACHE在提交 parser 前清理当前文件 cache;它不会清空全局 parser cache。ParallelPackageParser最多使用 4 个前台解析线程,结果队列容量为 30;submit/take面向完成顺序,orderedSubmit/future.get保留提交顺序。- parser 异常被封装进
ParseResult;processParseResult()再区分 parse failure、addForInitLIfailure、APEX 内 APK 错误和 data package code path 清理。 - 普通 data package 扫描失败会调用
removeCodePath(),system package 失败不会走同一删除分支;APEX 内 APK 失败还要向ApexManager报错。 - 并行发生在 APK parse 阶段,package state/Settings/resolver 注册仍按扫描路径在双锁下处理;因此“解析并行”不等于“状态无序”。
- 测试必须分开证明目录筛选、parser 交接、结果注册、失败清理和设备启动时序,不能用一个 parser 单测代表整条扫描链。
下一篇 PMS018 将进入 system app 与 priv-app 扫描,解释 SCAN_AS_SYSTEM、SCAN_AS_PRIVILEGED、framework 目录和特权权限/分区规则如何影响 addForInitLI() 的结果。
