Skip to content

应用存储迁移

追踪 Android 17 应用跨存储卷迁移的冻结、容量测量、安装复用和成功失败清理。

基于android-17.0.0_r1
AndroidPMSMovePackageHelperStorageManagerPackageInstaller

应用存储迁移 ​

本文面向已经读过 PMS 并发控制、安装失败清理 和 PackageFreezer 并发与解冻 的读者。本文只讲 movePackage() 的真实执行链:PMS 如何选择目标卷、为什么要求所有相关用户解锁、怎样复用安装管线迁移代码和数据,以及失败时为什么清理目标卷而不是源卷。

1. 迁移入口 ​

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

Binder 入口要求 MOVE_PACKAGE 权限,取得 calling UID/user 后把请求交给新的 MovePackageHelper。helper 不直接复制 APK,而是创建 MoveInfo 并构造 InstallingSession,最终走 InstallPackageHelper 的替换安装路径。

java
movePackageHelper.movePackageInternal(
        packageName, volumeUuid, moveId, callingUid, user);

2. 前置拒绝 ​

movePackageInternal 先取得对调用者可见且已安装的 package state,再检查:system app 不允许移动;目标卷必须是合法 private/mounted writable volume;代码路径必须是现代 cluster directory;目标不能与当前 volume 相同;device admin 不能从非外部卷移动;已有 frozen package 返回 MOVE_FAILED_OPERATION_PENDING。

这些失败发生在创建 freezer 之前,避免为明显不合法的请求杀进程。

3. 用户与加密 ​

移动完整应用时,helper 枚举所有 installed users;文件加密设备要求每个相关 user 的 CE storage 已解锁,否则返回 MOVE_FAILED_LOCKED_USER。这是因为迁移同时涉及 app data,不能只移动 APK 而让 CE inode 处于半迁移状态。

目标是 physical external volume 时只移动 code,moveCompleteApp=false;目标是 internal/private volume 时 code 与 data 都移动,moveCompleteApp=true。这两个模式决定容量计算、user storage prepare 和失败 cleanup 的范围。

4. 容量测量 ​

源码符号:getPackageSizeInfoLI。

helper 在 mInstallLock 下调用 Installer getAppSize,对每个 installed user 累加 code/data,并从 data 中扣除 cache。system 分区原生包的 code size 按 0 计入,因为迁移只处理可移动 data/code。

java
mPm.mInstaller.getAppSize(
        packageStateInternal.getVolumeUuid(), packageNames,
        userId, 0, packageStateInternal.getAppId(),
        packageStateInternal.getPccId(), ceDataInodes,
        codePaths, stats);
stats.dataSize -= stats.cacheSize;

目标卷可用空间不足时返回 MOVE_FAILED_INTERNAL_ERROR;源码在此阶段已经关闭 freezer,不会进入 session。

5. 存储准备 ​

prepareUserStorageForMove 将源/目标 volume UUID 和所有相关 UserInfo 交给 StorageManagerInternal.prepareUserStorageForMove。如果准备抛 RuntimeException,helper 转换为 MOVE_FAILED_INTERNAL_ERROR 并关闭 freezer。

该步骤先于实际 InstallingSession,确保目标卷上的 CE/DE 目录和 quota/inode 环境已经存在;它不是 Installer.getAppSize 的隐含副作用。

6. 冻结与进度 ​

所有前置检查通过后,helper 在 mPm.mLock 下创建 PackageFreezer(USER_ALL),再通知 move created。冻结覆盖所有用户,避免迁移期间任一用户启动旧路径。

完整 app 移动会启动进度线程,每秒观察目标卷可用空间变化,将进度从 10 推进到 90;安装回调收到结果后 countDown latch 并关闭 freezer。

7. 迁移信息 ​

MoveInfo 携带 from/to UUID、packageName、appId、seInfo、targetSdk、fromCodePath 和 PCC id。InstallingSession 使用 OriginInfo.fromExistingFile(codeFile) 和 INSTALL_REPLACE_EXISTING,把原 code path 当作待复制的安装源。

因此移动不是独立的“文件复制 API”,而是安装替换管线的一种输入。签名、ABI、shared library 和 package state 仍由安装流程验证;移动成功后新 volumeUuid 由 commit 阶段写入 PackageSetting。

8. 成功与失败清理 ​

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

成功移动后,doPostInstallCleanUp 调用 cleanUpForMoveInstall(fromUuid, packageName, fromCodePath),删除源卷 app data 和 code path;失败则传入 toUuid,清理目标卷半成品,保留源卷可运行状态。

java
if (request.getReturnCode() == INSTALL_SUCCEEDED) {
    cleanUpForMoveInstall(moveInfo.mFromUuid,
            moveInfo.mPackageName, moveInfo.mFromCodePath);
} else {
    cleanUpForMoveInstall(moveInfo.mToUuid,
            moveInfo.mPackageName, moveInfo.mFromCodePath);
}

cleanUpForMoveInstall 清理 DE/CE 数据时保留 ART profiles,原因是 profile 不随 app data 移动;删除唯一副本会损害后续 dexopt。

9. 回调语义 ​

MoveCallbacks 通过 Handler 向 IPackageMoveObserver 发送 created/status changed。安装回调把 PackageInstaller public status 映射为 move status:成功为 MOVE_SUCCEEDED,存储失败为 MOVE_FAILED_INSUFFICIENT_STORAGE,其他错误为 internal error。

回调成功只表示安装替换事务完成;源卷 cleanup、stats logging 和 freezer close 在回调附近执行,但仍是独立动作。诊断时要同时看 move status、PackageSetting volumeUuid 和源/目标目录。

10. 失败定位 ​

现象检查点解释
MOVE_FAILED_DOESNT_EXISTvisibility、installed users、pkg object包不可见、未安装或没有可执行 AndroidPackage
MOVE_FAILED_SYSTEM_PACKAGEpackageState.isSystem()system app 不支持普通迁移
MOVE_FAILED_LOCKED_USERCE unlocked 状态完整 app 移动要求所有 installed user 解锁
MOVE_FAILED_OPERATION_PENDINGfrozen packages已有安装/清理操作持有 freezer
失败后目标卷残留cleanUpForMoveInstall(toUuid, ...)、Installer 异常失败 cleanup 可能部分完成
成功后源卷仍有数据from volume cleanupcode/data 删除在 post-install cleanup 阶段
移动后 ART 需重新优化PackageState volumeUuid、profiles 保留profile 保留不等于 artifact 可直接复用

11. 源码练习 ​

  1. 从 movePackageInternal 列出 system/admin/frozen/volume/path/locked-user 六个前置失败点及其返回码。
  2. 对比 internal/private 与 physical external 目标卷,判断 moveCompleteApp、容量计算和 CE 要求的差异。
  3. 追踪 MoveInfo 如何进入 InstallingSession,说明移动为什么复用 INSTALL_REPLACE_EXISTING 而不是独立复制逻辑。
  4. 构造安装回调失败输入,定位为什么清理 to volume;再构造成功输入,说明何时删除 from volume、何时关闭 freezer。