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.javaframeworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.javaframeworks/base/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.javaframeworks/base/core/java/com/android/internal/pm/parsing/IPackageCacher.javaframeworks/base/services/core/java/com/android/server/pm/parsing/PackageCacher.java
| 组件 | owner | 输入 | 输出/副作用 |
|---|---|---|---|
PackageParser2 | 调用方持有的 parser 实例 | 文件、flags、useCaches | ParsedPackage 或 PackageParserException |
ParsingPackageUtils | parser 实例内部 | ParseInput、文件/PackageLite | ParseResult<ParsingPackage> |
PackageImpl | 当前解析对象 | Manifest/Parcel 字段 | parsing/parsed/final 状态转换 |
IPackageCacher | 注入实现 | 文件、flags、ParsedPackage | cache hit/miss、序列化写回 |
Callback | PMS/调用方 | 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
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
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
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
@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
/**
* 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
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
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
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
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
@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
@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
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
/**
* 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
(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
// 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
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 性能日志
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 PackageCacherdebuggable 构建中,PackageParser2 对 parse + cache write 总时长超过 100 ms 时打印 Parse times for '<file>': parse=..., update_cache=...。该日志只在 Build.IS_DEBUGGABLE 为 true 时启用,且 cache hit 直接返回,不会产生同样的 parse timing 日志。
7.3 故障定位
| 现象 | 首先检查 | 解释边界 |
|---|---|---|
| 每次都进入真实 parse | cacher、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、commit | parser 不负责注册 |
| close 后 worker 仍持有状态 | executor 生命周期 | 主线程 close 不清 worker ThreadLocal |
8. 源码路线
定位 PackageParser2 问题时,可沿以下顺序阅读:
PackageManagerServiceparser producer:确认使用 caching、non-caching 还是 preparing parser。PackageParser2.parsePackage():确认一层目录归一化、cache read、reset、真实 parse、cache write 和 timing。ParsingPackageUtils.parsePackage():确认 monolithic/cluster 分叉和签名验证边界。parseClusterPackage()/parseMonolithicPackage():确认PackageLite、split loader、base/split 解析和 close。PackageParser2.Callback/ParsingPackageUtils.Callback:确认 feature、compat、allowlist 和PackageImpl.forParsing()来源。PackageImpl.hideAsParsed()/hideAsFinal():确认对象何时仍可变、何时冻结。PackageCacher:确认 cache key、mtime、Apex backing file、feature flag 和损坏清理。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 查询和持久化状态。
