PackageFreezer 并发与解冻
本文面向已经读过 PMS 并发控制、安装原子性保证 和 DeletePackageHelper 入口 的读者。它不再复述安装或卸载全流程,而只回答一个并发问题:PMS 在修改包代码或数据时,怎样让目标包停止运行,如何把冻结状态暴露给查询,以及多个安装/清理操作重叠时为什么不会提前解冻。
1. 引用计数
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageFreezer.java、frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java。
PackageFreezer 的类注释定义了两个动作:创建时冻结并杀死目标包,关闭时解冻。PMS 的状态不是 Map<String, Boolean>,而是包名到引用计数的 WatchedArrayMap<String, Integer>。
final WatchedArrayMap<String, Integer> mFrozenPackages =
new WatchedArrayMap<>();
private final SnapshotCache<WatchedArrayMap<String, Integer>>
mFrozenPackagesSnapshot = new SnapshotCache.Auto(
mFrozenPackages, mFrozenPackages,
"PackageManagerService.mFrozenPackages");引用计数是核心不变量:只要计数大于零,包仍然被冻结;只有最后一个句柄关闭,包名才从 map 中移除。这样安装、清 profile、清用户数据和卸载可以同时持有冻结句柄,而其中一个操作结束不会破坏另一个操作的保护。
2. 创建句柄
源码符号:PackageFreezer 的构造函数。
synchronized (mPm.mLock) {
final int refCounts = mPm.mFrozenPackages
.getOrDefault(mPackageName, 0) + 1;
mPm.mFrozenPackages.put(mPackageName, refCounts);
ps = mPm.mSettings.getPackageLPr(mPackageName);
}
if (ps != null) {
if (waitAppStopped) {
mPm.stopAndKillApplication(...);
} else if (waitAppKilled) {
mPm.killApplicationSync(...);
} else {
mPm.killApplication(...);
}
}构造顺序很重要:先在 mLock 下登记冻结,再读取 PackageSetting 并杀进程。即使包状态已经被删除,句柄仍然完成引用计数;ps == null 只意味着没有进程可杀,不意味着冻结登记可以跳过。
杀进程有三种等待语义:普通 killApplication 立即发起杀进程;killApplicationSync 等待杀进程同步完成;stopAndKillApplication 用于需要确认应用已停止的更新路径。冻结状态和进程终止是两个不同阶段,mFrozenPackages 变更并不表示进程已经退出。
3. 三种调用模式
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java、frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java。
安装路径根据 flag 和窗口管理能力选择不同模式:
private PackageFreezer freezePackageForInstall(String packageName,
int userId, int installFlags, String killReason,
int exitReason, InstallRequest request) {
if ((installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) {
return new PackageFreezer(mPm, request);
}
if (Flags.enableAppRestartAfterUpdate()) {
return new PackageFreezer(packageName, UserHandle.USER_ALL,
"installPackageLI", mPm,
ApplicationExitInfo.REASON_PACKAGE_UPDATED,
request, false /* waitAppKilled */, true /* waitAppStopped */);
}
return mPm.freezePackage(packageName, userId, killReason,
exitReason, request);
}INSTALL_DONT_KILL_APP 返回的是 stub freezer:它没有包名,也不修改 mFrozenPackages,但仍通知 InstallRequest 冻结指标开始。启用 app restart after update 时,冻结范围是 USER_ALL,并要求 stop-and-kill;普通路径按调用传入的 userId 处理。
删除路径的 freezePackageForDelete 也有对应的 DELETE_DONT_KILL_APP stub。因而调试时不能看到 PackageFreezer 对象就断言包一定在 mFrozenPackages 中,必须检查 install/delete flag。
4. close 的幂等性
源码符号:PackageFreezer.close。
@Override
public void close() {
mCloseGuard.close();
if (mClosed.compareAndSet(false, true)) {
synchronized (mPm.mLock) {
final int refCounts = mPm.mFrozenPackages
.getOrDefault(mPackageName, 0) - 1;
if (refCounts > 0) {
mPm.mFrozenPackages.put(mPackageName, refCounts);
} else {
mPm.mFrozenPackages.remove(mPackageName);
}
}
}
if (mInstallRequest != null) {
mInstallRequest.onFreezeCompleted();
mInstallRequest = null;
}
}AtomicBoolean.compareAndSet 让重复 close() 不会重复递减;CloseGuard 用于发现忘记关闭的句柄,finalizer 最后仍会调用 close()。这使得 try-with-resources 不只是代码风格,而是冻结释放的生命周期契约。
5. 启动性查询
源码文件:frameworks/base/services/core/java/com/android/server/pm/ComputerEngine.java;符号:getPackageStartability。
冻结状态不会改变包是否安装,也不会直接让所有 PackageManager API 返回“包不存在”。它在启动性查询中有明确结果:
if (ps == null || shouldFilterApplication(ps, callingUid, userId)
|| !ps.getUserStateOrDefault(userId).isInstalled()) {
return PACKAGE_STARTABILITY_NOT_FOUND;
}
if (mFrozenPackages.containsKey(packageName)) {
return PACKAGE_STARTABILITY_FROZEN;
}顺序说明了诊断边界:不可见、未安装和冻结是不同结果;调用者如果先被 visibility 过滤,根本不会得到 frozen。冻结还会进入 Computer snapshot,因此 dump 或查询看到的是某个 snapshot 时点,不是对 mFrozenPackages 的无锁实时读取。
6. 安装并发
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java。
Android 17 在部分更新路径中把冻结放到 mInstallLock 之外的专用 executor 并行执行,所有包冻结完成后才回到 handler,取得 mInstallLock 和 mLock 执行 commitPackagesLocked。
CompletableFuture.runAsync(() -> {
PackageFreezer freezer = freezePackageForInstall(...);
installRequest.setFreezer(freezer);
}, sExecutorForStopAndKill);
CompletableFuture.allOf(freezerFutures...).thenRunAsync(() -> {
try (PackageManagerTracedLock installLock =
mPm.mInstallLock.acquireLock()) {
synchronized (mPm.mLock) {
commitPackagesLocked(reconciledPackages, userIds);
}
}
}, mPm.mHandler::post);这段代码表达两个并发约束:冻结本身不需要持有 mInstallLock;提交包状态时必须先拿 install lock,再进入 mLock。如果冻结 future 失败,whenCompleteAsync 只把安装完成回调置为失败,未完成的句柄仍必须由各自 InstallRequest 生命周期释放。
7. 多句柄重叠
假设安装和清理同时发生:安装创建句柄后计数为 1,清理创建第二个句柄后计数为 2;安装先关闭得到 1,清理完成后才变成 0。这个设计避免“安装提交结束但清数据仍在进行时”提前允许启动。
时间 mFrozenPackages[com.example.app]
t0 安装创建 1
t1 清数据创建 2
t2 安装 close 1
t3 清数据 close 0(移除)如果 dump 在 t2 观察到 1,这是正常的残留引用,不是泄漏。只有计数长期不归零,才需要查找没有执行 close() 的路径、异步 future 异常或 finalizer 警告。
8. 清理与恢复
clearApplicationUserData、clearApplicationProfileData、安装、删除和更新都可能使用冻结器。典型清数据路径是:先检查调用者可见性和 protected package,再在 handler 中创建 waitAppKilled=true 的 freezer,取得 install lock 执行数据清理,离开 try-with-resources 后解冻。
冻结失败和清理失败要分别定位:前者关注进程停止调用和句柄创建,后者关注 mInstallLock 下的 *LIF 方法。即使清理抛出异常,try-with-resources 仍会调用 close();若是异步链路提前返回,则应检查 InstallRequest 是否保留了 freezer 引用。
9. 调试检查
源码符号:ComputerEngine.dump、getFrozenPackages、checkPackageFrozen。
dumpsys package 的 Frozen packages: 区域打印包名和引用计数;getFrozenPackages() 把可观察 map 提供给 Computer snapshot;checkPackageFrozen 在调用者认为包应被冻结但 map 没有条目时记录 wtf。建议按以下顺序检查:
- 是否使用了
INSTALL_DONT_KILL_APP或DELETE_DONT_KILL_APPstub; Frozen packages计数是否随每个句柄创建/关闭变化;- 进程是普通 kill、同步 kill,还是 stop-and-kill;
- 查询是否先被 visibility/installed 条件拦截;
- 安装 future 或清理回调是否在异常路径释放 freezer。
10. 源码练习
- 画出两个重叠
PackageFreezer的引用计数时序,指出为什么第一个close()不能解冻包。 - 对比 stub freezer 与真实 freezer:说明为什么两者都能更新安装指标,但只有后者修改
mFrozenPackages。 - 从
getPackageStartability追踪“不可见”“未安装”“冻结”三种返回值,解释它们为何不能用同一个错误提示替代。 - 观察一次安装失败路径,检查
CompletableFuture、InstallRequest.setFreezer和 try-with-resources 是否形成完整释放闭环。
