APK 拷贝到 data-app
本文承接 InstallingSession 会话、安装前检查 和 InstallPackageHelper 入口。本文只讲 APK 的物理路径生命周期:来源文件如何进入临时 code path,何时提取 native library,什么时候切换到最终目录,以及异常时谁负责删除。
Android 17 的文件流不是“写入 /data/app 就结束”。普通文件安装至少有两个路径阶段:InstallingSession.copyApkForFileInstall() 产生临时目录,InstallPackageHelper.doRenameLI() 在 scan/reconcile 成功后生成最终目录。已 staged 输入会跳过第一次复制;增量安装在第二阶段用 hard-link 语义接入最终路径;move install 则由 installer 直接移动完整应用。
1. 路径生命周期
1.1 三种输入
1.2 owner 与时机
| 阶段 | owner | 路径状态 |
|---|---|---|
| origin/stage | PackageInstallerSession/OriginInfo | 客户端写入或外部来源 |
| 临时复制 | InstallingSession | request.codeFile 指向临时目录 |
| native 提取 | NativeLibraryHelper | 临时目录下的 lib/ |
| 扫描协调 | InstallPackageHelper | 目录仍是临时路径 |
| 最终切换 | doRenameLI | 目录进入最终 code path |
| 路径回写 | ParsedPackage + PackageSetting | 所有 code/native path 更新 |
2. 临时目录
2.1 stage 分配调用
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallingSession.java
if (mOriginInfo.mStaged) {
if (DEBUG_INSTALL) {
Slog.d(TAG, mOriginInfo.mFile + " already staged; skipping copy");
}
request.setCodeFile(mOriginInfo.mFile);
return PackageManager.INSTALL_SUCCEEDED;
}
final boolean isEphemeral =
(mInstallFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
request.setCodeFile(mPm.mInstallerService.allocateStageDirLegacy(
mVolumeUuid, isEphemeral));已 staged 的 session 已经拥有目标临时目录,直接把它设为 request code file;普通来源才申请新目录。Instant App 只作为分配 stage 目录的参数,不改变后续复制接口。
2.2 Stage 分配
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerService.java
public File allocateStageDirLegacy(String volumeUuid, boolean isEphemeral)
throws IOException {
final File stageDir = buildSessionDir(volumeUuid, isEphemeral);
prepareStageDir(stageDir);
return stageDir;
}Android 17 的具体目录命名和权限由 buildSessionDir()/prepareStageDir() 统一处理;InstallingSession 不拼接 /data/app,只接收服务返回的 File。这个边界避免了不同存储卷和 Instant App 目录策略散落在复制代码中。
3. APK 内容复制
3.1 文件复制入口
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallingSession.java
private int copyApkForFileInstall(InstallRequest request) {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "copyApk");
try {
if (mOriginInfo.mStaged) {
if (DEBUG_INSTALL) {
Slog.d(TAG, mOriginInfo.mFile + " already staged; skipping copy");
}
request.setCodeFile(mOriginInfo.mFile);
return PackageManager.INSTALL_SUCCEEDED;
}
int ret;
try {
final boolean isEphemeral =
(mInstallFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
request.setCodeFile(mPm.mInstallerService.allocateStageDirLegacy(
mVolumeUuid, isEphemeral));
} catch (IOException e) {
final String errorMessage = "Failed to create copy file";
Slog.w(TAG, errorMessage + ": " + e);
ret = PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
request.setError(ret, errorMessage);
return ret;
}
ret = PackageManagerServiceUtils.copyPackage(
mOriginInfo.mFile.getAbsolutePath(), request.getCodeFile());
if (ret != PackageManager.INSTALL_SUCCEEDED) {
request.setError(ret, "Failed to copy package");
return ret;
}复制阶段先决定“是否已经 staged”,再分配临时目录,最后复制整个 package。任一步失败都会设置 request 错误并返回;此时还没有执行 InstallPackageHelper.doRenameLI()。
3.2 Base/split 顺序
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
public static int copyPackage(String packagePath, File targetDir) {
if (packagePath == null) {
return PackageManager.INSTALL_FAILED_INVALID_URI;
}
try {
final File packageFile = new File(packagePath);
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite(
input.reset(), packageFile, /*flags=*/ 0);
if (result.isError()) {
Slog.w(TAG, "Failed to parse package at " + packagePath);
return result.getErrorCode();
}
final PackageLite pkg = result.getResult();
copyFile(pkg.getBaseApkPath(), targetDir, "base.apk");
if (!ArrayUtils.isEmpty(pkg.getSplitNames())) {
for (int i = 0; i < pkg.getSplitNames().length; i++) {
copyFile(pkg.getSplitApkPaths()[i], targetDir,
"split_" + pkg.getSplitNames()[i] + ".apk");
}
}
return PackageManager.INSTALL_SUCCEEDED;
} catch (IOException | ErrnoException e) {
Slog.w(TAG, "Failed to copy package at " + packagePath + ": " + e);
return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
}
}源文件可以是单 APK,也可以是 cluster 目录。copyPackage() 重新做一次轻量解析,以 base-first 顺序写入 base.apk 和 split_<name>.apk;目标目录因此在复制完成后已经是可供后续完整解析的 package 目录。
3.3 单文件复制
private static void copyFile(String sourcePath, File targetDir, String targetName)
throws ErrnoException, IOException {
if (!FileUtils.isValidExtFilename(targetName)) {
throw new IllegalArgumentException("Invalid filename: " + targetName);
}
final File targetFile = new File(targetDir, targetName);
final FileDescriptor targetFd = Os.open(targetFile.getAbsolutePath(),
O_RDWR | O_CREAT, DEFAULT_FILE_ACCESS_MODE);
Os.chmod(targetFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE);
FileInputStream source = null;
try {
source = new FileInputStream(sourcePath);
FileUtils.copy(source.getFD(), targetFd);
} finally {
IoUtils.closeQuietly(source);
}
}目标文件名经过 isValidExtFilename() 检查,文件以固定访问模式创建,再由 FileUtils.copy() 从源 FD 写入。源输入和目标 FD 的关闭责任在 finally/底层工具中完成。
4. Native library
4.1 提取时机
final boolean isIncremental = isIncrementalPath(
request.getCodeFile().getAbsolutePath());
final File libraryRoot = new File(request.getCodeFile(), LIB_DIR_NAME);
NativeLibraryHelper.Handle handle = null;
try {
handle = NativeLibraryHelper.Handle.create(request.getCodeFile());
ret = NativeLibraryHelper.copyNativeBinariesWithOverride(
handle, libraryRoot, request.getAbiOverride(), isIncremental);
if (ret != PackageManager.INSTALL_SUCCEEDED) {
request.setError(ret, "Failed to copy native libraries");
}
} catch (IOException e) {
final String errorMessage = "Copying native libraries failed";
Slog.e(TAG, errorMessage, e);
request.setError(PackageManagerException.ofInternalError(
errorMessage, PackageManagerException.INTERNAL_ERROR_NATIVE_LIBRARY_COPY));
} finally {
IoUtils.closeQuietly(handle);
}native library 提取发生在临时 code path 中,ABI 覆盖来自 request。isIncremental 会让 NativeLibraryHelper 采用增量路径处理;Handle 无论成功失败都必须关闭。
4.2 失败含义
native library 复制返回普通安装错误码时,request 记录该码;I/O 异常则包装为内部错误 INTERNAL_ERROR_NATIVE_LIBRARY_COPY。这两个错误都阻止后续 processInstallRequests(true, ...),临时目录由失败清理删除。
5. Move install
5.1 installer 移动
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallingSession.java
private int copyApkForMoveInstall(InstallRequest request) {
if (DEBUG_INSTALL) {
Slog.d(TAG, "Moving " + mMoveInfo.mPackageName + " from "
+ mMoveInfo.mFromUuid + " to " + mMoveInfo.mToUuid);
}
try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
mPm.mInstaller.moveCompleteApp(mMoveInfo.mFromUuid, mMoveInfo.mToUuid,
mMoveInfo.mPackageName, mMoveInfo.mAppId, mMoveInfo.mSeInfo,
mMoveInfo.mTargetSdkVersion, mMoveInfo.mFromCodePath, mMoveInfo.mPccId);
} catch (Installer.InstallerException e) {
request.setError(PackageManagerException.ofInternalError(
"Failed to move app", PackageManagerException.INTERNAL_ERROR_MOVE));
return PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
}
final String toPathName = new File(mMoveInfo.mFromCodePath).getName();
request.setCodeFile(new File(
Environment.getDataAppDirectory(mMoveInfo.mToUuid), toPathName));
return PackageManager.INSTALL_SUCCEEDED;
}move install 不调用 copyPackage(),而是让 installer 在 mInstallLock 下移动完整应用。成功后 request code file 指向目标卷中沿用的目录名;失败返回 internal error,后续清理按 move 分支处理。
6. 复制结果交接
6.1 结果交接
private void processPendingInstall(InstallRequest installRequest) {
if (mRet == PackageManager.INSTALL_SUCCEEDED) {
mRet = copyApk(installRequest);
}
if (mRet == PackageManager.INSTALL_SUCCEEDED) {
F2fsUtils.releaseCompressedBlocks(
mPm.mContext.getContentResolver(),
new File(installRequest.getCodePath()));
}
installRequest.setReturnCode(mRet);
if (mParentInstallingSession != null) {
mParentInstallingSession.tryProcessInstallRequest(installRequest);
} else {
mPm.mHandler.post(() -> processInstallRequests(
mRet == PackageManager.INSTALL_SUCCEEDED,
Collections.singletonList(installRequest)));
}
}只有前置位置/版本检查成功才调用 copyApk();复制成功后才释放 F2FS compressed blocks。单包直接 post,multi-package 回传 parent。此时 code file 仍是临时路径,尚未切换到最终目录。
6.2 Rename 时机
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if (reconciledPackages == null) {
return;
}
if (renameAndUpdatePaths(requests)) {
// Rename before dexopt because ART encodes the path in odex/vdex files.
pendingForDexopt = true;
final Runnable actionsAfterDexopt = () -> doPostDexopt(
reconciledPackages, requests, createdAppId, moveInfo, acquireTime);
prepPerformDexoptIfNeeded(reconciledPackages, actionsAfterDexopt);
}rename 放在 reconcile 之后、dexopt 之前。源码注释给出原因:ART 会把路径编码进 odex/vdex,必须先确定最终路径再编译。
7. 最终 code path
7.1 doRenameLI
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
@GuardedBy("mPm.mInstallLock")
private void doRenameLI(InstallRequest request,
ParsedPackage parsedPackage) throws PrepareFailure {
final int status = request.getReturnCode();
if (request.isInstallMove()) {
if (status != PackageManager.INSTALL_SUCCEEDED) {
mRemovePackageHelper.cleanUpForMoveInstall(request.getMoveToUuid(),
request.getMovePackageName(), request.getMoveFromCodePath());
throw new PrepareFailure(status, request.getReturnMsg());
}
return;
}
if (status != PackageManager.INSTALL_SUCCEEDED) {
mRemovePackageHelper.removeCodePath(request.getCodeFile());
throw new PrepareFailure(status, request.getReturnMsg());
}
final File targetDir = resolveTargetDir(request.getInstallFlags(), request.getCodeFile());
final File beforeCodeFile = request.getCodeFile();
final File afterCodeFile = PackageManagerServiceUtils.getNextCodePath(
targetDir, parsedPackage.getPackageName());move install 成功时不再 rename;普通文件安装失败则删除临时 code path。成功的普通安装才计算最终目录,目录名由 package name 和系统 code-path 策略生成。
7.2 Rename/link
final boolean onIncremental = mPm.mIncrementalManager != null
&& isIncrementalPath(beforeCodeFile.getAbsolutePath());
try {
makeDirRecursive(afterCodeFile.getParentFile(), 0771);
if (onIncremental) {
// Just link files here. The stage dir is removed when the session completes.
mPm.mIncrementalManager.linkCodePath(beforeCodeFile, afterCodeFile);
} else {
Os.rename(beforeCodeFile.getAbsolutePath(), afterCodeFile.getAbsolutePath());
}
} catch (IOException | ErrnoException e) {
Slog.w(TAG, "Failed to rename", e);
throw new PrepareFailure(PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE,
"Failed to rename");
}普通路径用 Os.rename() 把整个临时目录切到最终目录;增量路径则建立 code path link,stage 目录在 session 完成时再删除。两者都在 mInstallLock 保护下执行。
7.3 SELinux 标签
if (!onIncremental && !SELinux.restoreconRecursive(afterCodeFile)) {
Slog.w(TAG, "Failed to restorecon");
throw new PrepareFailure(PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE,
"Failed to restorecon");
}
request.setCodeFile(afterCodeFile);非增量最终目录需要递归恢复 SELinux context;增量路径跳过这一步,由增量文件系统维护其安全属性。restorecon 失败不会继续 dexopt,而是返回 media unavailable。
8. 路径回写
8.1 Package 路径
String afterCanonicalPath = afterCodeFile.getCanonicalPath();
String beforeCanonicalPath = beforeCodeFile.getCanonicalPath();
parsedPackage.setPath(afterCanonicalPath);
parsedPackage.setNativeLibraryDir(
parsedPackage.getNativeLibraryDir()
.replace(beforeCanonicalPath, afterCanonicalPath));
parsedPackage.setNativeLibraryRootDir(
parsedPackage.getNativeLibraryRootDir()
.replace(beforeCanonicalPath, afterCanonicalPath));
String secondaryNativeLibraryDir = parsedPackage.getSecondaryNativeLibraryDir();
if (secondaryNativeLibraryDir != null) {
parsedPackage.setSecondaryNativeLibraryDir(
secondaryNativeLibraryDir.replace(beforeCanonicalPath, afterCanonicalPath));
}目录移动后,解析对象中的 package path、native library dir、native root 和 secondary native dir 都要同步替换。只更新 request code file 会留下“文件已移动、对象仍指向旧目录”的状态不一致。
8.2 Base/split 路径
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
parsedPackage.setBaseApkPath(FileUtils.rewriteAfterRename(
beforeCodeFile, afterCodeFile, parsedPackage.getBaseApkPath()));
parsedPackage.setSplitCodePaths(FileUtils.rewriteAfterRename(
beforeCodeFile, afterCodeFile, parsedPackage.getSplitCodePaths()));
request.updateAllCodePaths(AndroidPackageUtils.getAllCodePaths(parsedPackage));rewriteAfterRename() 同时更新 base 和 split APK 路径;InstallRequest 再根据新的 ParsedPackage 重建全部 code paths,供后续 dexopt、Settings 和通知使用。
8.3 Setting 回写
PackageSetting scannedPackageSetting = request.getScannedPackageSetting();
scannedPackageSetting.setPath(new File(parsedPackage.getPath()));
scannedPackageSetting.setLegacyNativeLibraryPath(
parsedPackage.getNativeLibraryRootDir());rename 成功后,扫描阶段创建的 PackageSetting 也要写入最终路径。这个回写发生在 renameAndUpdatePaths() 中,早于 dexopt 和 commit 的最终状态提交。
9. 失败与清理
9.1 普通文件失败
private void doPostInstallCleanUp(InstallRequest request, MoveInfo moveInfo) {
if (moveInfo != null) {
// move install cleanup handled by source/target volume
} else if (request.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) {
mRemovePackageHelper.removeCodePath(request.getCodeFile());
}
}普通安装在复制、scan、reconcile、rename 或 dexopt 失败后,最终都依据 request code file 删除对应 code path。若目录已经 rename 到最终位置,request 已在 rename 过程中更新;若尚未 rename,则删除临时目录。
9.2 Rename 失败
doRenameLI() 先执行目录 rename/link,再执行 restorecon 和路径回写。rename 成功但 restorecon 或 canonical path 获取失败时,request 会收到错误;后续 doPostInstallCleanUp() 仍尝试按当前 code file 清理。读源码时要注意:这不是一个文件系统事务,恢复依赖后续清理逻辑。
9.3 资源关闭
native library Handle 在 copyApkForFileInstall() 的 finally 关闭;AssetManager、PackageParser 和 session stage 的关闭则由各自 owner 管理。文件复制失败不会跳过这些 finally,因此 I/O 失败路径同样需要验证资源释放。
10. 时序与诊断
10.1 完整时序
10.2 诊断顺序
- 先检查
OriginInfo.mStaged、mMoveInfo和InstallRequest.codeFile,确定走复用、复制还是 move。 - 对普通复制检查
copyPackage()是否成功生成base.apk和split_*.apk。 - 若 native library 失败,检查 ABI override、incremental path 和 Handle 关闭日志。
- 若 scan/reconcile 失败,确认目录仍是临时路径,先查 request return code。
- 若 dexopt 找不到文件,检查
doRenameLI()是否在 dexopt 前完成 rename 和 ParsedPackage 路径回写。 - 若安装后路径错误,同时检查 request code file、ParsedPackage path、base/split paths 和 PackageSetting path。
- 失败清理时区分临时目录、最终目录和 move install 的源/目标卷。
11. 测试与断言
11.1 输入矩阵
| 输入 | 关键断言 | 证明范围 |
|---|---|---|
| staged session | 不调用 copyPackage,codeFile 复用 origin | staged 快路 |
| 单 APK | 目标目录出现 base.apk | monolithic 复制 |
| base + split | split 文件名和顺序正确 | cluster 复制 |
| 无效源路径 | 返回 invalid URI/APK | 输入错误 |
| native ABI 不匹配 | native copy 返回对应错误 | ABI 分支 |
| move install | 调用 moveCompleteApp,codeFile 指向目标卷 | move 分支 |
| 普通 rename | Os.rename 后 ParsedPackage 全路径更新 | 最终切换 |
| incremental rename | linkCodePath,不调用 restorecon | 增量分支 |
| restorecon 失败 | MEDIA_UNAVAILABLE,后续不 dexopt | 安全标签 |
| rename 失败 | INSUFFICIENT_STORAGE,临时路径可清理 | I/O 失败 |
| split code path | base/split 路径都替换为最终路径 | 路径回写 |
测试不能只看最终 APK 是否存在;必须断言 request、ParsedPackage、PackageSetting 三个 owner 是否都指向同一最终路径。
12. 源码路线
建议按以下顺序阅读:
InstallingSession.processPendingInstall():复制触发与结果交接。copyApkForFileInstall():stage 分配、APK 复制和 native 提取。PackageManagerServiceUtils.copyPackage():base/split 文件命名和复制。copyApkForMoveInstall():installer 移动路径。InstallPackageHelper.installPackagesTraced():rename 位于 reconcile 与 dexopt 之间。renameAndUpdatePaths()/doRenameLI():最终 code path、增量 link 和 restorecon。FileUtils.rewriteAfterRename()与request.updateAllCodePaths():路径回写。doPostInstallCleanUp():失败时删除临时或最终 code path。
13. 设计收束
APK 文件生命周期可以压缩为:
origin/stage
-> 临时 code path
-> base.apk + split_*.apk + lib/
-> scan/reconcile 成功
-> rename 或 incremental link
-> ParsedPackage/PackageSetting 全路径回写
-> dexopt 与 commit其中 InstallingSession 负责把内容变成可扫描的临时目录,InstallPackageHelper 负责在状态足够稳定后切换最终路径。rename 不是装饰性重命名:它决定 ART 后续写入的路径、SELinux context、PackageSetting 和所有 split/native 路径的一致性。任何失败都必须回到正确的 owner 清理,不能只删除源 APK 或只修改内存字段。
