Skip to content

扫描并行化

解析 ParallelPackageParser 的线程池、队列、提交模型、异常传播和有序注册边界。

基于android-17.0.0_r1
AndroidPackageManagerService包扫描并行解析ParallelPackageParser性能源码阅读

扫描并行化 ​

本文面向已经读过 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.java
  • frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java

并行化的 owner 和消费者可以分成四层:

层Owner并行做什么结果由谁消费
目录枚举InstallPackageHelper遍历 File[]、过滤 APK/目录和 stage nameParallelPackageParser
APK 解析ParallelPackageParser 的线程池调用 PackageParser2.parsePackage()ParseResult 或 Future
扫描注册InstallPackageHelperaddForInitLI()、签名/设置/reconcilePMS lock 保护的状态
线程池生命周期InitAppsHelper创建、复用、最终关闭 executorsystem/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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 和日志组合观察:

text
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

实验时应记录:

  1. parallel parsePackage [...] trace 区间是否重叠,说明 parser 任务是否真正并行执行;
  2. parallelScanDir、scanDir 和 addForInitLI 区间的先后,说明消费是否受顺序等待;
  3. Finished scanning system apps. Time: ... cached: ... 日志中的总耗时与 cache 计数;
  4. 解析失败时是 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. 源码路线 ​

定位并行扫描问题时,可以沿下面的顺序阅读:

  1. InitAppsHelper 构造函数:确认 executor 创建和初始 scan flags。
  2. scanSystemDirs() 或 initNonSystemApps():确认单目录/多目录入口和关闭时机。
  3. InstallPackageHelper.installPackagesFromDir()、parallelInstallPackagesFromDirs():确认消费模型。
  4. scanDirectoryForFilesToParse()、orderedScanDirectoryForFilesToParse():确认过滤、cache 清理和提交数量。
  5. ParallelPackageParser.submit()、orderedSubmit()、take():确认任务、异常和中断传播。
  6. processParseResult():确认 parse/scan 错误分类和 data 清理。
  7. 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 线程提前决定。