Skip to content

APK 拷贝到 data-app

追踪 APK 从 origin/stage 到最终 code path 的复制、native library 提取、重命名、路径回写和失败清理。

基于android-17.0.0_r1
AndroidPackageManagerServiceInstallingSessionAPKNative Library源码阅读

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/stagePackageInstallerSession/OriginInfo客户端写入或外部来源
临时复制InstallingSessionrequest.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

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

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

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

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 单文件复制 ​

java
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 提取时机 ​

java
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

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 结果交接 ​

java
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

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

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 策略生成。

java
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 标签 ​

java
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 路径 ​

java
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

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 回写 ​

java
PackageSetting scannedPackageSetting = request.getScannedPackageSetting();
scannedPackageSetting.setPath(new File(parsedPackage.getPath()));
scannedPackageSetting.setLegacyNativeLibraryPath(
        parsedPackage.getNativeLibraryRootDir());

rename 成功后,扫描阶段创建的 PackageSetting 也要写入最终路径。这个回写发生在 renameAndUpdatePaths() 中,早于 dexopt 和 commit 的最终状态提交。

9. 失败与清理 ​

9.1 普通文件失败 ​

java
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 诊断顺序 ​

  1. 先检查 OriginInfo.mStaged、mMoveInfo 和 InstallRequest.codeFile,确定走复用、复制还是 move。
  2. 对普通复制检查 copyPackage() 是否成功生成 base.apk 和 split_*.apk。
  3. 若 native library 失败,检查 ABI override、incremental path 和 Handle 关闭日志。
  4. 若 scan/reconcile 失败,确认目录仍是临时路径,先查 request return code。
  5. 若 dexopt 找不到文件,检查 doRenameLI() 是否在 dexopt 前完成 rename 和 ParsedPackage 路径回写。
  6. 若安装后路径错误,同时检查 request code file、ParsedPackage path、base/split paths 和 PackageSetting path。
  7. 失败清理时区分临时目录、最终目录和 move install 的源/目标卷。

11. 测试与断言 ​

11.1 输入矩阵 ​

输入关键断言证明范围
staged session不调用 copyPackage,codeFile 复用 originstaged 快路
单 APK目标目录出现 base.apkmonolithic 复制
base + splitsplit 文件名和顺序正确cluster 复制
无效源路径返回 invalid URI/APK输入错误
native ABI 不匹配native copy 返回对应错误ABI 分支
move install调用 moveCompleteApp,codeFile 指向目标卷move 分支
普通 renameOs.rename 后 ParsedPackage 全路径更新最终切换
incremental renamelinkCodePath,不调用 restorecon增量分支
restorecon 失败MEDIA_UNAVAILABLE,后续不 dexopt安全标签
rename 失败INSUFFICIENT_STORAGE,临时路径可清理I/O 失败
split code pathbase/split 路径都替换为最终路径路径回写

测试不能只看最终 APK 是否存在;必须断言 request、ParsedPackage、PackageSetting 三个 owner 是否都指向同一最终路径。

12. 源码路线 ​

建议按以下顺序阅读:

  1. InstallingSession.processPendingInstall():复制触发与结果交接。
  2. copyApkForFileInstall():stage 分配、APK 复制和 native 提取。
  3. PackageManagerServiceUtils.copyPackage():base/split 文件命名和复制。
  4. copyApkForMoveInstall():installer 移动路径。
  5. InstallPackageHelper.installPackagesTraced():rename 位于 reconcile 与 dexopt 之间。
  6. renameAndUpdatePaths()/doRenameLI():最终 code path、增量 link 和 restorecon。
  7. FileUtils.rewriteAfterRename() 与 request.updateAllCodePaths():路径回写。
  8. doPostInstallCleanUp():失败时删除临时或最终 code path。

13. 设计收束 ​

APK 文件生命周期可以压缩为:

text
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 或只修改内存字段。