Skip to content

PackageParser2 架构

追踪 Android 17 PackageParser2 的线程隔离、缓存注入、Manifest 解析、回调依赖和资源清理边界。

基于android-17.0.0_r1
AndroidPackageManagerServicePackageParser2ParsingPackageUtilsPackageCacher源码阅读

PackageParser2 架构 ​

本文面向已经读过 扫描并行化、ScanRequest 与 Result 和 扫描性能分析 的读者。前文已经说明 parser 线程如何提交任务、扫描结果如何承接,以及 Trace 如何观察 parse 区段;本文把镜头推进到 PackageParser2 内部,解释它如何隔离线程状态、注入 PMS 回调、接入 parser cache,并把文件/目录转换成 ParsedPackage。

Android 17 中,PackageParser2 不是 Manifest 解析算法本身,而是 server-side parsing 的门面。真正的 XML、资源、组件、split 和包约束解析由 ParsingPackageUtils 完成;PackageParser2 负责准备 ParseInput、决定是否查缓存、处理 ParseResult 错误、调用 hideAsParsed(),并在关闭时清理当前线程的复用对象。PackageCacher、PackageParser2.Callback 和 ParsingPackageUtils.Callback 则把服务状态与解析器解耦。

读完后,读者应能从 PackageParser2.parsePackage() 找到缓存命中与真实解析的分叉;解释为什么同一个 parser 实例可以被多个线程调用;指出 PackageImpl 在 parsing、parsed、final 三种视图之间的转换;理解 parsePackageFromPackageLite() 为什么不走同一条文件解析路径;还能根据回调、cache 和异常边界定位“解析结果不一致”或“线程退出后资源未释放”的问题。

1. 组件边界 ​

1.1 五个角色 ​

源码文件:

  • frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java
  • frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
  • frameworks/base/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
  • frameworks/base/core/java/com/android/internal/pm/parsing/IPackageCacher.java
  • frameworks/base/services/core/java/com/android/server/pm/parsing/PackageCacher.java
组件owner输入输出/副作用
PackageParser2调用方持有的 parser 实例文件、flags、useCachesParsedPackage 或 PackageParserException
ParsingPackageUtilsparser 实例内部ParseInput、文件/PackageLiteParseResult<ParsingPackage>
PackageImpl当前解析对象Manifest/Parcel 字段parsing/parsed/final 状态转换
IPackageCacher注入实现文件、flags、ParsedPackagecache hit/miss、序列化写回
CallbackPMS/调用方feature、compat、package 起点解析时动态查询系统能力

这五层的责任不能互换:cache 不决定 Manifest 语义,ParsingPackageUtils 不拥有 PMS 全局状态,PackageParser2.close() 不关闭线程池,ParsedPackage 也不代表已经注册到 Settings。

1.2 解析与注册 ​

PackageParser2 的返回点是 ParsedPackage,而 PMS 的注册入口通常是 InstallPackageHelper.addForInitLI() 或安装流程的 scanPackageNew()。解析成功只说明对象已经构造出来;签名收集、ABI、scan policy、冲突 reconcile、AppId 和组件注册仍在 parser 之外。

因此一条完整调用链是:

ParallelPackageParser → PackageParser2.parsePackage() → ParsingPackageUtils.parsePackage() → PackageImpl.hideAsParsed() → ScanRequest/ScanResult → PMS 注册。

2. 线程隔离 ​

2.1 ThreadLocal 字段 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java

java
private final ThreadLocal<ApplicationInfo> mSharedAppInfo =
        ThreadLocal.withInitial(() -> {
            ApplicationInfo appInfo = new ApplicationInfo();
            appInfo.uid = -1; // Not a valid UID since the app will not be installed yet
            return appInfo;
        });

private final ThreadLocal<ParseTypeImpl> mSharedResult;

@Nullable
protected IPackageCacher mCacher;

private final ParsingPackageUtils mParsingUtils;

mSharedAppInfo 只为兼容性回调提供临时 package name、target SDK 和无效 UID;mSharedResult 为每个调用线程提供一个可 reset 的 ParseTypeImpl。两者都是 parser 实例字段,但实际对象按线程隔离,因此并行 parser 可以共享一个 PackageParser2 实例而不共享 ParseInput 的可变错误状态。

2.2 构造函数 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java

java
public PackageParser2(String[] separateProcesses, DisplayMetrics displayMetrics,
        @Nullable IPackageCacher cacher, @NonNull Callback callback) {
    if (displayMetrics == null) {
        displayMetrics = new DisplayMetrics();
        displayMetrics.setToDefaults();
    }

    List<PermissionManager.SplitPermissionInfo> splitPermissions = null;
    final Application application = ActivityThread.currentApplication();
    if (application != null) {
        final PermissionManager permissionManager =
                application.getSystemService(PermissionManager.class);
        if (permissionManager != null) {
            splitPermissions = permissionManager.getSplitPermissions(true);
        }
    }
    if (splitPermissions == null) {
        splitPermissions = new ArrayList<>();
    }

    mCacher = cacher;
    mParsingUtils = new ParsingPackageUtils(separateProcesses, displayMetrics,
            splitPermissions, callback);

    ParseInput.Callback enforcementCallback = (changeId, packageName, targetSdkVersion) -> {
        ApplicationInfo appInfo = mSharedAppInfo.get();
        appInfo.packageName = packageName;
        appInfo.targetSdkVersion = targetSdkVersion;
        return callback.isChangeEnabled(changeId, appInfo);
    };
    mSharedResult = ThreadLocal.withInitial(() -> new ParseTypeImpl(enforcementCallback));
}

构造时收集 split permission 信息,创建一次 ParsingPackageUtils,再把兼容性变更查询包装为 ParseInput.Callback。回调使用当前线程的 ApplicationInfo,调用后不会把完整 ApplicationInfo 暴露给 parser;它只填充 package name、target SDK 和 uid 初始值。

2.3 reset 规则 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java

java
ParseInput input = mSharedResult.get().reset();
ParseResult<ParsingPackage> result = mParsingUtils.parsePackage(input, packageFile, flags);
if (result.isError()) {
    throw new PackageParserException(result.getErrorCode(), result.getErrorMessage(),
            result.getException());
}

每次真实解析都从当前线程的 ParseTypeImpl.reset() 开始,清除上一次 parse 的 success/error 状态。ParseResult 不跨任务复用;ParsedPackage 成功返回后由调用方持有。若开发者绕过 reset() 直接复用 ParseInput,错误信息和 callback 状态可能污染下一次解析。

3. 解析入口 ​

3.1 parsePackage ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java

java
@AnyThread
public ParsedPackage parsePackage(File packageFile, int flags, boolean useCaches)
        throws PackageParserException {
    var files = packageFile.listFiles();
    // Apk directory is directly nested under the current directory
    if (ArrayUtils.size(files) == 1 && files[0].isDirectory()) {
        packageFile = files[0];
    }

    if (useCaches && mCacher != null) {
        ParsedPackage parsed = mCacher.getCachedResult(packageFile, flags);
        if (parsed != null) {
            return parsed;
        }
    }

    long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
    ParseInput input = mSharedResult.get().reset();
    ParseResult<ParsingPackage> result = mParsingUtils.parsePackage(input, packageFile, flags);
    if (result.isError()) {
        throw new PackageParserException(result.getErrorCode(), result.getErrorMessage(),
                result.getException());
    }

    ParsedPackage parsed = (ParsedPackage) result.getResult().hideAsParsed();
    long cacheTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
    if (mCacher != null) {
        mCacher.cacheResult(packageFile, flags, parsed);
    }
    if (LOG_PARSE_TIMINGS) {
        parseTime = cacheTime - parseTime;
        cacheTime = SystemClock.uptimeMillis() - cacheTime;
        if (parseTime + cacheTime > LOG_PARSE_TIMINGS_THRESHOLD_MS) {
            Slog.i(TAG, "Parse times for '" + packageFile + "': parse=" + parseTime
                    + "ms, update_cache=" + cacheTime + " ms");
        }
    }
    return parsed;
}

入口有四个明确步骤:一层嵌套目录归一化、可选 cache 查询、真实解析、结果缓存。useCaches=false 只禁止读取 cache,不禁止在解析成功后调用 mCacher.cacheResult();当前源码在 mCacher != null 时仍会写 cache。因此“关闭 cache”若要完全成立,调用方需要传入 null cacher 或使用不写入的实现,而不能只把 useCaches 设为 false。

3.2 文件与目录 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java

java
/**
 * Parse the package at the given location. Automatically detects if the package is a monolithic
 * style (single APK file) or cluster style (directory of APKs).
 *
 * Note that this does not perform signature verification; that must be done separately.
 */
public ParseResult<ParsingPackage> parsePackage(ParseInput input, File packageFile, int flags) {
    if (packageFile.isDirectory()) {
        return parseClusterPackage(input, packageFile, flags);
    } else {
        return parseMonolithicPackage(input, packageFile, flags);
    }
}

目录被视为 cluster package,文件被视为 monolithic package。ParsingPackageUtils.parsePackage() 本身不做签名验证;签名由后续 getSigningDetails() 收集。这个边界解释了为什么 parser cache 命中后,PMS 仍可能在扫描阶段重新收集或复用签名。

3.3 cluster 解析 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java

java
private ParseResult<ParsingPackage> parseClusterPackage(ParseInput input, File packageDir,
        int flags) {
    int liteParseFlags = 0;
    if ((flags & PARSE_APK_IN_APEX) != 0) {
        liteParseFlags |= PARSE_APK_IN_APEX;
    }
    if ((flags & PARSE_APEX) != 0) {
        liteParseFlags |= PARSE_APEX;
    }
    final ParseResult<PackageLite> liteResult =
            ApkLiteParseUtils.parseClusterPackageLite(input, packageDir, liteParseFlags);
    if (liteResult.isError()) {
        return input.error(liteResult);
    }

    final PackageLite lite = liteResult.getResult();
    // Build the split dependency tree.
    SparseArray<int[]> splitDependencies = null;
    final SplitAssetLoader assetLoader;
    if (lite.isIsolatedSplits() && !ArrayUtils.isEmpty(lite.getSplitNames())) {
        splitDependencies = SplitAssetDependencyLoader.createDependenciesFromPackage(lite);
        assetLoader = new SplitAssetDependencyLoader(lite, splitDependencies, flags);
    } else {
        assetLoader = new DefaultSplitAssetLoader(lite, flags);
    }
    // parse base and split APKs, then close assetLoader in finally
}

cluster 先做 PackageLite 级别校验和 split dependency tree,再选择 isolated split loader 或 default loader,之后解析 base/split。loader 是临时资源 owner,必须在 finally 关闭;它与 PackageParser2.close() 无关。

3.4 monolithic 解析 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java

java
private ParseResult<ParsingPackage> parseMonolithicPackage(ParseInput input, File apkFile,
        int flags) {
    // The signature parsing will be done later in method parseBaseApk.
    int liteParseFlags = flags & ~PARSE_COLLECT_CERTIFICATES;
    final ParseResult<PackageLite> liteResult =
            ApkLiteParseUtils.parseMonolithicPackageLite(input, apkFile, liteParseFlags);
    if (liteResult.isError()) {
        return input.error(liteResult);
    }

    final PackageLite lite = liteResult.getResult();
    final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags);
    try {
        boolean shouldSkipComponents = lite.isIsSdkLibrary() && disallowSdkLibsToBeApps();
        return parseBaseApk(input, apkFile, apkFile.getCanonicalPath(), assetLoader,
                flags, shouldSkipComponents);
    } finally {
        IoUtils.closeQuietly(assetLoader);
    }
}

monolithic 入口显式移除 PARSE_COLLECT_CERTIFICATES,因为签名收集由独立方法负责;先解析 lite 信息,再创建 split asset loader 并进入 base APK 解析。SDK library 的组件解析还会受 disallowSdkLibsToBeApps() 条件影响。

4. 回调与对象状态 ​

4.1 Callback 接口 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java

java
public interface Callback {
    boolean hasFeature(String feature);

    ParsingPackage startParsingPackage(@NonNull String packageName,
            @NonNull String baseApkPath, @NonNull String path,
            @NonNull TypedArray manifestArray, boolean isCoreApp);

    @NonNull Set<String> getHiddenApiWhitelistedApps();

    @NonNull Set<String> getInstallConstraintsAllowlist();
}

解析器需要外部能力时通过 callback 查询 feature、hidden API allowlist、install constraints allowlist,并通过 startParsingPackage() 让调用方决定具体 ParsingPackage 实现。这样 parser 不需要直接依赖 PMS 的 Settings、PackageManagerService 或全局包表。

4.2 parser callback ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java

java
public abstract static class Callback implements ParsingPackageUtils.Callback {
    @Override
    public final ParsingPackage startParsingPackage(@NonNull String packageName,
            @NonNull String baseCodePath, @NonNull String codePath,
            @NonNull TypedArray manifestArray, boolean isCoreApp) {
        return PackageImpl.forParsing(packageName, baseCodePath, codePath, manifestArray,
                isCoreApp, Callback.this);
    }

    public abstract boolean isChangeEnabled(long changeId,
            @NonNull ApplicationInfo appInfo);
}

PackageParser2.Callback 将 parser 的通用 callback 扩展为兼容性变更判断,并固定用 PackageImpl.forParsing() 创建对象。PMS 可以继承该 callback 提供系统状态;测试则可以注入轻量实现。

4.3 对象状态 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java

java
@Override
public PackageImpl hideAsParsed() {
    assignDerivedFields();
    return this;
}

@Override
public AndroidPackageInternal hideAsFinal() {
    if (mStorageUuid == null) {
        assignDerivedFields();
    }
    assignDerivedFields2();
    makeImmutable();
    return this;
}

ParsingPackage 是解析过程使用的可变接口,hideAsParsed() 计算第一批派生字段并返回 ParsedPackage 视图;hideAsFinal() 在 PMS 注册完成前后进一步计算字段并冻结对象。PackageParser2 只调用 hideAsParsed(),不会替调用方完成 hideAsFinal();测试和 PMS commit 路径需要在合适时机执行 finalization。

4.4 PackageLite 快路 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java

java
@AnyThread
public ParsedPackage parsePackageFromPackageLite(PackageLite packageLite, int flags)
        throws PackageParserException {
    ParseInput input = mSharedResult.get().reset();
    ParseResult<ParsingPackage> result = mParsingUtils.parsePackageFromPackageLite(
            input, packageLite, flags);
    if (result.isError()) {
        throw new PackageParserException(result.getErrorCode(), result.getErrorMessage(),
                result.getException());
    }
    return result.getResult().hideAsParsed();
}

这个入口只使用已存在的 PackageLite,不重新读取完整 APK Manifest,也不使用 mCacher。它用于需要构造“已归档/仅保留包信息”对象的路径;返回对象中的缺失字段由 ParsingPackageUtils.parsePackageFromPackageLite() 填充默认值,不能当成完整 Manifest 解析结果。

5. 缓存与关闭 ​

5.1 cacher 接口 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/IPackageCacher.java

java
public interface IPackageCacher {
    ParsedPackage getCachedResult(File packageFile, int flags);
    void cacheResult(File packageFile, int flags, ParsedPackage parsed);
}

接口只定义读/写两个动作,不规定 mtime、fingerprint、序列化格式或清理策略。服务端 PackageCacher 才决定 cache key、文件时间、Apex backing file、AConfig flag 和损坏 entry 的行为;测试可以完全替换实现。

5.2 关闭语义 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java

java
/**
 * Removes the cached value for the thread the parser was created on.
 * Threads created for parallel parsing are expected to be released by their owner.
 */
@Override
public void close() {
    mSharedResult.remove();
    mSharedAppInfo.remove();
}

close() 只移除调用 close() 的线程上的 ThreadLocal 值。并行扫描线程由 executor 生命周期管理,不能期待主线程调用 parser.close() 就清理其他 worker 的 ThreadLocal;InitAppsHelper 仍需关闭 executor。cache 目录、PackageImpl 和 SplitAssetLoader 也有各自的 owner。

5.3 缓存失败 ​

PackageParser2.parsePackage() 对 cache 读取没有捕获 IPackageCacher 自身异常;服务端 PackageCacher.getCachedResult() 会捕获读取/反序列化异常、删除损坏文件并返回 null。若注入的 cacher 抛出未处理异常,parser 会直接失败,说明 cache 实现的容错属于 cacher owner,不是 parser 门面。

6. PMS 接入 ​

6.1 parser 创建 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java
(i, pm) -> new PackageParser2(pm.mSeparateProcesses, i.getDisplayMetrics(),
        new PackageCacher(pm.mCacheDir, pm.mPackageParserCallback),
        pm.mPackageParserCallback) /* scanningCachingPackageParserProducer */,
(i, pm) -> new PackageParser2(pm.mSeparateProcesses, i.getDisplayMetrics(), null,
        pm.mPackageParserCallback) /* scanningPackageParserProducer */,
(i, pm) -> new PackageParser2(pm.mSeparateProcesses, i.getDisplayMetrics(), null,
        pm.mPackageParserCallback) /* preparingPackageParserProducer */,

PMS 注入三类 parser:扫描 caching parser、扫描 non-caching parser 和 preparing parser。扫描缓存 parser 使用 PackageCacher,其他两类传入 null cacher。parser 的缓存能力由创建方决定,而不是由 PackageParser2 类级别常量决定。

6.2 并行扫描接入 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java、frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java

java
// ParallelPackageParser
pr.parsedPackage = parsePackage(scanFile, scanParams.parseFlags);

// InstallPackageHelper
ParallelPackageParser parallelPackageParser =
        new ParallelPackageParser(packageParser, executorService);
int fileCount = scanDirectoryForFilesToParse(parallelPackageParser, scanParams);
for (; fileCount > 0; fileCount--) {
    processParseResult(parallelPackageParser.take());
}

并行 helper 复用注入的 PackageParser2,每个线程通过 ThreadLocal 获得独立 ParseTypeImpl。解析结果由 InstallPackageHelper 消费,调用 processParseResult() 继续扫描和注册;parser 本身不持有 executor,也不决定结果顺序。

6.3 解析错误转换 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java

java
ParseResult<ParsingPackage> result = mParsingUtils.parsePackage(input, packageFile, flags);
if (result.isError()) {
    throw new PackageParserException(result.getErrorCode(), result.getErrorMessage(),
            result.getException());
}

ParsingPackageUtils 使用结构化 ParseResult 返回错误,PackageParser2 在 public 入口把它转换成 PackageParserException。并行 helper 再把 PackageParserException 转换为 PackageManagerException 或写入 ParseResult.throwable。同一个错误在不同层可能有不同包装,诊断时要追溯最内层 error code。

7. 测试与诊断 ​

7.1 parser 测试入口 ​

源码文件:frameworks/base/core/tests/coretests/src/com/android/internal/pm/parsing/PackageParser2Test.java、frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java

测试重点应覆盖:

  • monolithic 文件和 cluster 目录都能返回 ParsedPackage;
  • useCaches=true 时 cacher 命中可跳过 ParsingPackageUtils;
  • cache miss 后成功解析会调用 cache write;
  • ParseResult 错误被转换为 PackageParserException;
  • 多线程调用同一个 parser 实例时,ThreadLocal ParseTypeImpl 不互相污染;
  • parser.close() 后当前线程的 ThreadLocal 被移除,executor worker 的退出由 executor 测试负责。

ParallelPackageParserTest 的真实测试通过 15 个提交文件和 take() 验证结果完整性,但其 parsePackage() 被测试子类覆盖为返回 null,因此不证明真实 Manifest 解析性能或 cache 命中。

7.2 性能日志 ​

text
adb shell atrace --async_start package_manager sched freq idle
adb shell atrace --async_stop -z -o /data/local/tmp/package-parser.html
adb shell logcat -s PackageParser PackageManager PackageCacher

debuggable 构建中,PackageParser2 对 parse + cache write 总时长超过 100 ms 时打印 Parse times for '<file>': parse=..., update_cache=...。该日志只在 Build.IS_DEBUGGABLE 为 true 时启用,且 cache hit 直接返回,不会产生同样的 parse timing 日志。

7.3 故障定位 ​

现象首先检查解释边界
每次都进入真实 parsecacher、cache key、mtime、flags不一定是 Manifest 变化
同一 parser 并发结果互串mSharedResult.get().reset()、线程复用不要共享外部 ParseInput
cache 读取异常PackageCacher.getCachedResult()parser 只负责接收 null/结果
split 资源错误cluster loader、isolated split tree与 parser.close 无关
package 已解析但不可查询ScanResult、reconcile、commitparser 不负责注册
close 后 worker 仍持有状态executor 生命周期主线程 close 不清 worker ThreadLocal

8. 源码路线 ​

定位 PackageParser2 问题时,可沿以下顺序阅读:

  1. PackageManagerService parser producer:确认使用 caching、non-caching 还是 preparing parser。
  2. PackageParser2.parsePackage():确认一层目录归一化、cache read、reset、真实 parse、cache write 和 timing。
  3. ParsingPackageUtils.parsePackage():确认 monolithic/cluster 分叉和签名验证边界。
  4. parseClusterPackage()/parseMonolithicPackage():确认 PackageLite、split loader、base/split 解析和 close。
  5. PackageParser2.Callback/ParsingPackageUtils.Callback:确认 feature、compat、allowlist 和 PackageImpl.forParsing() 来源。
  6. PackageImpl.hideAsParsed()/hideAsFinal():确认对象何时仍可变、何时冻结。
  7. PackageCacher:确认 cache key、mtime、Apex backing file、feature flag 和损坏清理。
  8. ParallelPackageParser/InstallPackageHelper:确认线程、异常包装、结果消费和注册入口。

一个实用练习是:同一 cluster package 在第一次启动时 cache miss,在第二次启动时 cache hit,但其中一个 split 的 Manifest target SDK 改变且文件 mtime 未改变。请判断 parser cache 是否一定有效、PackageCacher 哪个检查可能失效、ParsedPackage 是否会重新生成、签名是否由 parser 负责,以及最终应在哪个 PMS 阶段确认 package state 是否更新。

9. 设计收束 ​

Android 17 的 PackageParser2 可以概括为一条清晰的门面链路:

  • PackageParser2 通过 ThreadLocal 复用 ApplicationInfo 和 ParseTypeImpl,允许并发调用但要求每次 parse 先 reset;
  • IPackageCacher 是可选依赖,useCaches 只控制读取,cache 写回由 cacher 是否存在决定;
  • ParsingPackageUtils 负责 monolithic/cluster、split loader 和 Manifest 解析,不负责签名兼容性、ABI 或 PMS 注册;
  • PackageImpl 通过 hideAsParsed()/hideAsFinal() 逐步计算派生字段并从可变 parsing 对象转成最终对象;
  • PMS 通过 callback 提供 feature/compat/allowlist,通过不同 parser producer 决定是否接入 PackageCacher;
  • close() 只清当前线程的 ThreadLocal,SplitAssetLoader、executor、cache 目录和 PMS live state 各自由其他 owner 管理;
  • 解析成功、签名通过、ScanResult 生成、reconcile 成功和 package 可查询是不同状态,性能与故障诊断必须沿层次分别取证。

后续专题会继续深入 ParsingPackage/ParsedPackage/PackageImpl 数据模型,解释 Manifest 字段如何从可变解析对象进入 PMS 查询和持久化状态。