PMS 性能分析
本文不把“性能优化”写成经验清单,而是回答三个源码问题:PMS 如何让高频查询避开全局写锁,安装为什么拆成锁内/锁外阶段,以及性能数据如何记录实际安装步骤。读者应先了解 PackageManagerShellCommand 和 PackageManager 实践。
Android 17 的 PMS 性能核心是读写分离:查询通过 Computer snapshot,写操作修改可观察状态并使 snapshot/cache 失效;安装则用 mInstallLock、批处理和 Future 把 I/O/dexopt 从主锁路径移开。
1. 读写模型
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java,符号:Snapshot
class Snapshot {
static final int LIVE = 1;
static final int SNAPPED = 2;
public final Settings settings;
public final WatchedArrayMap<String, AndroidPackage> packages;
public final AppsFilterSnapshot appsFilter;
public final ComponentResolverApi componentResolver;
public final SharedLibrariesRead sharedLibraries;
Snapshot(int type) {
if (type == Snapshot.SNAPPED) {
settings = mSettings.snapshot();
packages = mPackagesSnapshot.snapshot();
appsFilter = mAppsFilter.snapshot();
componentResolver = mComponentResolver.snapshot();
sharedLibraries = mSharedLibraries.snapshot();
} else {
settings = mSettings;
packages = mPackages;
appsFilter = mAppsFilter;
componentResolver = mComponentResolver;
sharedLibraries = mSharedLibraries;
}
}
}Live computer 引用 PMS 可变对象,只在受控锁内使用;snapped computer 深拷贝 settings、包 map、resolver、AppsFilter 和 shared libraries,供并发查询。快照不是“缓存一份 PackageInfo”,而是把一组相互关联的读取状态固定在同一个版本。
2. snapshotComputer
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java,符号:snapshotComputer、rebuildSnapshot
public Computer snapshotComputer() {
int pendingVersion =
sSnapshotPendingVersion.get();
Computer snapshot = sSnapshot.get();
if (snapshot != null
&& snapshot.getVersion() == pendingVersion) {
return snapshot.use();
}
synchronized (mSnapshotLock) {
snapshot = sSnapshot.get();
if (snapshot != null
&& snapshot.getVersion() == pendingVersion) {
return snapshot.use();
}
Computer rebuilt = rebuildSnapshot(
snapshot, pendingVersion);
sSnapshot.set(rebuilt);
return rebuilt.use();
}
}快照采用 double-check:先无锁读取 atomic snapshot,只有版本过期时才进入 mSnapshotLock 重建。多个 Binder 查询可以复用同一个版本;一次安装/状态写入只需推进 pending version,后续第一个读者承担重建成本。
3. 查询路径的成本
源码文件:frameworks/base/services/core/java/com/android/server/pm/ComputerEngine.java,符号:getPackageInfoInternal
public PackageInfo getPackageInfoInternal(
String packageName, long versionCode,
long flags, int filterCallingUid,
int userId) {
AndroidPackage pkg = mPackages.get(packageName);
if (pkg == null) {
pkg = resolvePackageName(packageName);
}
if (pkg == null) {
return null;
}
PackageSetting ps =
mSettings.getPackageLPr(
pkg.getPackageName());
if (ps == null
|| filterSharedLibPackage(
ps, filterCallingUid, userId, flags)
|| shouldFilterApplication(
ps, filterCallingUid, userId)) {
return null;
}
return generatePackageInfo(ps, flags, userId);
}快照消除了大锁竞争,但并没有让查询 O(1) 无条件成立:仍有 package map 查找、重命名解析、共享库过滤、AppsFilter、用户状态读取和 PackageInfo 组装。调用方请求的 flags 越多,generatePackageInfo 要复制的组件、权限和 metadata 越多。
4. 写操作与失效
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java,符号:cache invalidation
public static void invalidatePackageInfoCache(
int invalidationReason) {
PackageManager.invalidatePackageInfoCache();
onChanged();
PackageMetrics.reportCacheInvalidationEvent(
PackageMetrics
.CACHE_TYPE_APPLICATION_AND_PACKAGE_INFO,
invalidationReason);
}
public static void invalidateGetPackagesForUidCache(
int invalidationReason) {
ApplicationPackageManager
.invalidateGetPackagesForUidCache();
PackageMetrics.reportCacheInvalidationEvent(
PackageMetrics.CACHE_TYPE_GET_PACKAGES_FOR_UID,
invalidationReason);
}安装、删除、写 settings、权限 flag 变化和 AppsFilter 变化会让不同客户端缓存失效。缓存失效不是每次查询都重建所有对象;它先记录原因/推进版本,下一次需要时才生成新数据。
5. 安装锁与主锁
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:安装批处理
try (PackageManagerTracedLock installLock =
mPm.mInstallLock.acquireLock()) {
final int[] newUsers =
getNewUsers(request, allUsers);
mAppDataHelper.prepareAppDataPostCommitLIF(
ps, 0, newUsers);
}
CompletableFuture<Void> future =
mPm.getDexOptHelper()
.performDexoptIfNeededAsync(request);
completableFutures.add(future);安装锁保护 installd/app data 操作;dexopt 通过 Future 异步执行。PMS 主锁 mLock 用于包状态一致性,不能把文件 I/O、进程 stop/kill 或 dex2oat 长时间放在主锁中。
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:并发冻结
List<CompletableFuture<Void>> freezerFutures =
new ArrayList<>();
for (ReconciledPackage pkg : reconciledPackages) {
CompletableFuture<Void> future =
CompletableFuture.runAsync(() ->
freezePackageForInstall(pkg));
freezerFutures.add(future);
}
CompletableFuture.allOf(
freezerFutures.toArray(
new CompletableFuture[0]))
.thenRun(() -> continueInstall());多个包的冻结/停止可以并行等待,再回到受控锁内提交;这比在安装主线程逐包同步等待更容易放大 I/O 和进程生命周期延迟。
6. 安装耗时分段
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageMetrics.java,符号:步骤常量
public static final int STEP_PREPARE = 1;
public static final int STEP_SCAN = 2;
public static final int STEP_RECONCILE = 3;
public static final int STEP_COMMIT = 4;
public static final int STEP_DEXOPT = 5;
public static final int STEP_WAIT_DEXOPT = 8;
public static final int STEP_FREEZE_INSTALL = 6;
public static final int STEP_FREEZE_INSTALL_STOP_AND_KILL = 9;
public static final int STEP_RESTORE = 7;PMS 把一次安装拆成 prepare、scan、reconcile、commit、dexopt、等待 dexopt、freeze、restore 等步骤。性能分析应先看哪一步增长,而不是只看总安装耗时。
public void onInstallSucceed() {
reportInstallationStats(true /* success */);
}
private void reportInstallationStats(boolean success) {
long duration = System.currentTimeMillis()
- mInstallStartTimestampMillis;
Pair<int[], long[]> durations =
getInstallStepDurations();
FrameworkStatsLog.write(
FrameworkStatsLog
.PACKAGE_INSTALLATION_SESSION_REPORTED,
mInstallRequest.getSessionId(),
null, durations.first, durations.second);
}PackageMetrics 的价值是把“慢”归因到阶段;没有这些分段,只能猜测是解析、锁、数据目录还是 dexopt。
7. 启动扫描
PMS 启动扫描通过 InitAppsHelper 和 PackageParser2 扫描分区、APEX 和 data/app;扫描结果最终在受控锁内 reconcile/commit。不要用泛化的“线程数/CPU 核心数”推断实际收益,应以 trace section、扫描目录数量和 parse/commit 分段测量。
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java,符号:扫描入口
private void scanDirTracedLI(
File scanDir, int parseFlags,
int scanFlags, long currentTime,
int seq) {
Trace.traceBegin(
Trace.TRACE_TAG_PACKAGE_MANAGER,
"scanDir(" + scanDir + ")");
try {
// parse/scan packages under the partition
scanDirLI(scanDir, parseFlags, scanFlags,
currentTime, seq);
} finally {
Trace.traceEnd(
Trace.TRACE_TAG_PACKAGE_MANAGER);
}
}真实设备上应将 trace 中 scanDir(...)、APEX scan、reconcile 和 settings 写入时间对应到代码阶段,不要把旧版本文章中的固定秒数当作 Android 17 的稳定基线。
8. 查询性能实践
基于源码边界,调用方可遵循:
- 已知包名时使用单包 API,避免批量
PackageInfo的 Parcel 和对象生成。 - 只请求需要的 flags;组件、权限、metadata 会扩大
generatePackageInfo的工作量。 - 明确 userId;不要在业务层反复遍历
USER_ALL再自行过滤。 - 不要在主线程循环调用高成本查询;客户端 Binder 仍会等待 system_server。
- 处理
NameNotFoundException/空结果,把包可见性和 user state 当作合法结果分支。
9. 失效与缓存观测
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageMetrics.java,符号:cache invalidation reason
public static final int
INVALIDATION_REASON_INSTALL_PACKAGE = ...;
public static final int
INVALIDATION_REASON_DELETE_PACKAGE = ...;
public static final int
INVALIDATION_REASON_APP_FILTER_CHANGE = ...;
public static final int
INVALIDATION_REASON_WRITE_SETTINGS = ...;
public static final int
INVALIDATION_REASON_PERMISSION_FLAG_CHANGED = ...;性能异常若表现为查询延迟突然升高或缓存频繁失效,应关联 invalidation reason 与最近发生的安装、删除、AppsFilter、settings 或权限变更,而不是简单增加缓存容量。
10. 性能定位表
| 现象 | 先看 |
|---|---|
| 查询偶发长尾 | snapshot rebuild、generatePackageInfo flags、AppsFilter |
| 安装总耗时高 | PackageMetrics step durations |
| dexopt 阻塞安装 | STEP_DEXOPT/STEP_WAIT_DEXOPT、DexOptHelper Future |
| 写操作导致查询抖动 | cache invalidation reason、snapshot version |
| 启动慢 | scanDir(...)、APEX scan、reconcile、settings write trace |
| 多包安装慢 | freeze futures、install lock contention、post-install restore |
11. 阅读检查
复述:查询 → snapshot version 命中/重建 → ComputerEngine 读取和过滤;安装 → prepare/scan/reconcile/commit → async dexopt/restore;状态变化 → cache invalidation reason → 下一次查询重建。然后回答:snapshot 是否让写操作无锁?flags 越多是否一定更快?安装总耗时只看 onInstallSucceed 是否足够定位?答案分别是“不,写入仍需锁”“不,可能扩大对象生成”“不,必须看分段指标”。
