Skip to content

PackageFreezer 并发与解冻

追踪 Android 17 包冻结句柄、引用计数、进程终止、启动查询和异常解冻边界。

基于android-17.0.0_r1
AndroidPMSPackageFreezerConcurrencyInstall

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>。

java
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 的构造函数。

java
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 和窗口管理能力选择不同模式:

java
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。

java
@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 返回“包不存在”。它在启动性查询中有明确结果:

java
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。

java
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。这个设计避免“安装提交结束但清数据仍在进行时”提前允许启动。

text
时间       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。建议按以下顺序检查:

  1. 是否使用了 INSTALL_DONT_KILL_APP 或 DELETE_DONT_KILL_APP stub;
  2. Frozen packages 计数是否随每个句柄创建/关闭变化;
  3. 进程是普通 kill、同步 kill,还是 stop-and-kill;
  4. 查询是否先被 visibility/installed 条件拦截;
  5. 安装 future 或清理回调是否在异常路径释放 freezer。

10. 源码练习 ​

  1. 画出两个重叠 PackageFreezer 的引用计数时序,指出为什么第一个 close() 不能解冻包。
  2. 对比 stub freezer 与真实 freezer:说明为什么两者都能更新安装指标,但只有后者修改 mFrozenPackages。
  3. 从 getPackageStartability 追踪“不可见”“未安装”“冻结”三种返回值,解释它们为何不能用同一个错误提示替代。
  4. 观察一次安装失败路径,检查 CompletableFuture、InstallRequest.setFreezer 和 try-with-resources 是否形成完整释放闭环。