应用存储迁移
本文面向已经读过 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 的替换安装路径。
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。
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,清理目标卷半成品,保留源卷可运行状态。
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_EXIST | visibility、installed users、pkg object | 包不可见、未安装或没有可执行 AndroidPackage |
MOVE_FAILED_SYSTEM_PACKAGE | packageState.isSystem() | system app 不支持普通迁移 |
MOVE_FAILED_LOCKED_USER | CE unlocked 状态 | 完整 app 移动要求所有 installed user 解锁 |
MOVE_FAILED_OPERATION_PENDING | frozen packages | 已有安装/清理操作持有 freezer |
| 失败后目标卷残留 | cleanUpForMoveInstall(toUuid, ...)、Installer 异常 | 失败 cleanup 可能部分完成 |
| 成功后源卷仍有数据 | from volume cleanup | code/data 删除在 post-install cleanup 阶段 |
| 移动后 ART 需重新优化 | PackageState volumeUuid、profiles 保留 | profile 保留不等于 artifact 可直接复用 |
11. 源码练习
- 从
movePackageInternal列出 system/admin/frozen/volume/path/locked-user 六个前置失败点及其返回码。 - 对比 internal/private 与 physical external 目标卷,判断
moveCompleteApp、容量计算和 CE 要求的差异。 - 追踪 MoveInfo 如何进入
InstallingSession,说明移动为什么复用INSTALL_REPLACE_EXISTING而不是独立复制逻辑。 - 构造安装回调失败输入,定位为什么清理 to volume;再构造成功输入,说明何时删除 from volume、何时关闭 freezer。
