Skip to content

scanDirTracedLI

追踪单目录扫描从 scanDirTracedLI、文件筛选、并行解析到 processParseResult 的完整实现。

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

scanDirTracedLI ​

PMS016 说明了系统启动包扫描的阶段顺序。本篇把镜头推进到单目录入口 scanDirTracedLI():它如何把一个目录和一组 flags 交给 InstallPackageHelper,后者如何决定一个目录项是不是 APK package、如何排除 PackageInstaller stage 目录、何时丢弃 parser cache,最后又如何让并行 parser 的结果回到受锁保护的 package registration 路径。

这条路径值得单独学习,因为“扫描一个目录”包含了四个不同层次:

  1. 入口与 trace:InitAppsHelper.scanDirTracedLI() 不解析文件,只建立调用边界和 trace section。
  2. 输入筛选:scanDirectoryForFilesToParse() 把目录项变成有效 scan task,过滤临时 stage、非 APK 和空目录。
  3. 解析调度:ParallelPackageParser 使用最多 4 个前台解析线程;take() 按完成顺序返回,orderedSubmit() 则保留提交顺序。
  4. 结果生效: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

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

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

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

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

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

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

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

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;

一个目录项只有同时满足两个条件才提交:

  1. isApkFile(file) 为真,或者它是目录(目录可能代表 cluster/split package);
  2. 文件名不是 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

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

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

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

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

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

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

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

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

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

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 路径对比 ​

路径解析提交结果消费适用场景顺序保证
installPackagesFromDirsubmit()take()/data/app 或单个 APEX 内目录完成顺序
parallelInstallPackagesFromDirsorderedSubmit()future.get()system overlay/framework/priv-app/app目录和提交顺序
scanApexPackagessubmit()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

java
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanDir [" + scanDir + "]");
try {
    mInstallPackageHelper.installPackagesFromDir(...);
} finally {
    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}

worker 侧的 parse trace 同样在 parser 调用外包住 finally:

java
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

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

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。断言:

  1. 空目录返回 0,不调用 take();
  2. APK 和目录提交 parser;
  3. stage name 与普通文件不提交;
  4. drop cache 只清理当前文件对应 cache;
  5. 返回值等于实际提交数量。

当前测试目录中若只有 parser 或 scan result 单测,不能把它们扩大为 scanDirectoryForFilesToParse() 的完整证明。

7.4 设备启动验证 ​

真实设备上可以组合以下证据:

  • BOOT_PROGRESS_PMS_DATA_SCAN_START、BOOT_PROGRESS_PMS_SCAN_END;
  • parallel parsePackage 和 addForInitLI trace section;
  • dumpsys package 中 system/data package 的 path、flags 和 user state;
  • parser cache 命中/丢弃日志;
  • 一个故意损坏的 /data/app APK 是否被 removeCodePath() 清理;
  • APEX 内 APK 失败时是否出现 reportErrorWithApkInApex() 对应错误。

这些组合可以验证阶段和结果,但仍不能证明每个 worker 的精确调度顺序,因为并行 parse 本身不承诺完成顺序。

8. 阅读路线 ​

阅读一个新目录扫描调用点时,按下面顺序追:

  1. 找 scanDirTracedLI() 或 parallelScanDirTracedLI() 的调用方,记录目录、parse flags、scan flags、APEX info 和锁。
  2. 进入 installPackagesFromDir() 或 parallelInstallPackagesFromDirs(),确认使用 take() 还是 ordered future。
  3. 进入 scanDirectoryForFilesToParse(),检查 listFiles()、isApkFile()、目录允许条件、stage name 和 cache drop。
  4. 进入 ParallelPackageParser,记录线程数、队列容量、异常承载、interrupt 行为和 parser exception 转换。
  5. 进入 processParseResult(),区分 parse failure、registration failure、APEX error report 和 data code path deletion。
  6. 回到 addForInitLI()/scanPackageForInitLI(),确认 package state、resolver、permissions、shared libraries 和 snapshot invalidation 的实际 owner。
  7. 检查 executor shutdown、trace close、锁释放和失败路径是否完成。
  8. 最后用 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、addForInitLI failure、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() 的结果。