Skip to content

Incremental 安装

解释 Incremental File System、DataLoader 和按需加载如何接入安装会话。

基于android-17.0.0_r1
AndroidPackageManagerServiceIncremental FSDataLoader源码阅读

Incremental 安装 ​

本文承接 Session 安装 和 ADB 安装流程。普通 session 要先把 APK 完整写入 staging 目录;Incremental 安装则先创建一个由 Incremental File System(IncFS)提供的文件视图,再由 DataLoader 在缺页时补齐数据。本文聚焦 Android 17 中这条接入链:shell 命令如何选择 DataLoader,session 如何创建 image,PMS 如何把增量路径链接到 /data/app,以及安装后为何仍要等待 native binary 提取和加载进度。它不展开 Linux IncFS 驱动内部算法,也不把 streaming 安装误写成“安装后按需加载”。

1. 两种加载时机 ​

1.1 加载模式 ​

DataLoaderType 在 session 参数中区分数据来源:streaming 仍要求在提交前准备好可安装 image;incremental 使用 IncFS 允许文件在安装完成后继续加载。两者都经过 PackageInstallerSession,差别在于 image 的生命周期和数据是否允许未完成。

模式文件视图提交条件安装后行为
普通真实 staging 目录APK 已完整写入普通文件读取
streamingDataLoader 准备的 staging imageIMAGE_READY 后才能 sealed提交后不再依赖按需缺页
incrementalIncFS storageimage 可供解析,缺页可延后读取缺页时由 DataLoader 提供

IncFS 对 PMS 的抽象是“一个可扫描的路径”。PMS 不需要为每个 APK 读取请求重写 PackageParser;真正的异步数据提供者是 DataLoader 与 IncFS storage,PMS 只负责创建、提交、监控和清理它们。

2. 命令入口 ​

2.1 选择 incremental ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java

java
private int runIncrementalInstall() throws RemoteException {
    final InstallParams params = makeInstallParams(UNSUPPORTED_INSTALL_CMD_OPTS);
    if (params.sessionParams.dataLoaderParams == null) {
        params.sessionParams.setDataLoaderParams(
                PackageManagerShellCommandDataLoader.getIncrementalDataLoaderParams(this));
    }
    return doRunInstall(params);
}

命令本身只设置 DataLoaderParams,真正的 session 创建、文件添加和 commit 仍由 doRunInstall 完成。install、install-streaming 和 install-incremental 的共同入口让后续 PMS 管道保持一致;区别在 sessionParams.dataLoaderParams 是否为空以及其 type。

2.2 文件元数据 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java

java
public void addFile(int location, String name, long lengthBytes,
        byte[] metadata, byte[] signature) {
    if (!isDataLoaderInstallation()) {
        throw new IllegalStateException(
                "Cannot add files to non-data loader installation session.");
    }
    if (metadata == null) {
        throw new IllegalArgumentException(
                "DataLoader installation requires valid metadata: " + name);
    }
    synchronized (mLock) {
        assertCallerIsOwnerOrRoot();
        assertPreparedAndNotSealedLocked("addFile");
        if (!mFiles.add(new FileEntry(mFiles.size(),
                new InstallationFile(location, name, lengthBytes,
                        metadata, signature)))) {
            throw new IllegalArgumentException("File already added: " + name);
        }
    }
}

DataLoader session 不使用 openWrite 写入完整内容,而是登记文件名、长度、metadata 和签名。mFiles 是 session owner;sealed 之后不能再改变文件清单。Incremental 的文件完整性和按页加载信息由 metadata/signature 交给 IncFS/DataLoader,而不是由 PMS 自己缓存整份 APK。

3. 创建 IncFS ​

3.1 初始化 storage ​

源码文件:frameworks/base/core/java/android/os/incremental/IncrementalFileStorages.java

java
public static IncrementalFileStorages initialize(Context context, File stageDir,
        File inheritedDir, DataLoaderParams dataLoaderParams,
        IDataLoaderStatusListener statusListener,
        List<InstallationFileParcel> addedFiles,
        PerUidReadTimeouts[] perUidReadTimeouts,
        IPackageLoadingProgressCallback progressCallback) throws IOException {
    final IncrementalManager incrementalManager =
            (IncrementalManager) context.getSystemService(
                    Context.INCREMENTAL_SERVICE);
    if (incrementalManager == null) {
        throw new IOException("Failed to obtain incrementalManager.");
    }

    final IncrementalFileStorages result = new IncrementalFileStorages(
            stageDir, inheritedDir, incrementalManager, dataLoaderParams);
    for (InstallationFileParcel file : addedFiles) {
        if (file.location != LOCATION_DATA_APP) {
            throw new IOException("Unknown file location: " + file.location);
        }
        result.addApkFile(file);
    }
    if (progressCallback != null) {
        incrementalManager.registerLoadingProgressCallback(
                stageDir.getAbsolutePath(), progressCallback);
    }
    result.startLoading(dataLoaderParams, statusListener, null, null,
            perUidReadTimeouts);
    return result;
}

初始化顺序很具体:取得 IncrementalManager,创建 storage,把每个 data-app 文件注册为 IncFS 文件,再注册进度回调,最后启动 DataLoader。任一步失败都会抛出 IOException,调用方需要清理已创建的 storage。inheritedDir 允许继承已有增量 storage,但它必须仍是 incremental path。

3.2 创建文件与继承 ​

源码文件:frameworks/base/core/java/android/os/incremental/IncrementalFileStorages.java

java
private void addApkFile(InstallationFileParcel apk) throws IOException {
    final File targetFile = new File(mStageDir, apk.name);
    if (!targetFile.exists()) {
        mDefaultStorage.makeFile(apk.name, apk.size, 0777,
                null, apk.metadata, apk.signature, null);
    }
}

public boolean makeLink(String relativePath, String fromBase, String toBase)
        throws IOException {
    if (mInheritedStorage == null) {
        return false;
    }
    final String sourcePath = new File(fromBase, relativePath).getAbsolutePath();
    final String destPath = new File(toBase, relativePath).getAbsolutePath();
    mInheritedStorage.makeLink(sourcePath, mDefaultStorage, destPath);
    return true;
}

文件内容参数为 null 并不表示文件为空,而是表示内容由 DataLoader 后续提供。继承安装则在两个 storage 之间建立 link,避免重复传输已存在页面;system data loader 对不完整 inherited storage 有额外限制,不能把所有继承目录都视为可用。

4. DataLoader 状态 ​

4.1 prepare image ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java

java
final DataLoaderParams dataLoaderParams = params.dataLoaderParams;
final boolean manualStartAndDestroy = !isIncrementalInstallation();
final IDataLoaderStatusListener statusListener =
        new IDataLoaderStatusListener.Stub() {
    @Override
    public void onStatusChanged(int dataLoaderId, int status) {
        switch (status) {
            case IDataLoaderStatusListener.DATA_LOADER_BOUND:
                if (manualStartAndDestroy) {
                    getDataLoader(dataLoaderId).create(dataLoaderId,
                            dataLoaderParams.getData(),
                            new FileSystemControlParcel(), this);
                }
                break;
            case IDataLoaderStatusListener.DATA_LOADER_CREATED:
                if (manualStartAndDestroy) {
                    getDataLoader(dataLoaderId).start(dataLoaderId);
                }
                break;
            case IDataLoaderStatusListener.DATA_LOADER_STARTED:
                getDataLoader(dataLoaderId).prepareImage(dataLoaderId,
                        addedFiles, removedFiles);
                break;
            case IDataLoaderStatusListener.DATA_LOADER_IMAGE_READY:
                mDataLoaderFinished = true;
                dispatchSessionSealed();
                break;
        }
    }
};

manualStartAndDestroy 对 incremental 为 false,因为 IncrementalFileStorages 管理 start;streaming 等非 incremental 模式则由 session 手动 create/start/destroy。只有 DATA_LOADER_IMAGE_READY 才会把 session 推进到 sealed;IMAGE_NOT_READY 返回 INSTALL_FAILED_MEDIA_UNAVAILABLE,UNAVAILABLE 则允许调用者稍后再次 commit,而不是立即失败并删除 session。

4.2 不可恢复失败 ​

java
case IDataLoaderStatusListener.DATA_LOADER_UNAVAILABLE:
    // DataLoader 暂时不可用,保留 session 让调用方重试
    sendPendingStreaming(mContext, getRemoteStatusReceiver(), sessionId,
            "DataLoader unavailable");
    break;
case IDataLoaderStatusListener.DATA_LOADER_UNRECOVERABLE:
    throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE,
            "DataLoader unrecoverable");

这两个状态的消费者不同:UNAVAILABLE 发送 pending streaming 状态,session 仍存在;UNRECOVERABLE 进入失败路径。排查增量安装卡住时,首先区分这两个状态,不能把所有 DataLoader 非 ready 都当成同一种错误。

5. PMS 提交 ​

5.1 链接 code path ​

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

java
final boolean onIncremental = mPm.mIncrementalManager != null
        && isIncrementalPath(beforeCodeFile.getAbsolutePath());
if (onIncremental) {
    // IncFS stage 不能用普通 rename,改为链接 storage
    mPm.mIncrementalManager.linkCodePath(beforeCodeFile, afterCodeFile);
} else {
    Os.rename(beforeCodeFile.getAbsolutePath(), afterCodeFile.getAbsolutePath());
}
request.setCodeFile(afterCodeFile);

普通 APK 的 staging 目录通过 rename 进入 /data/app;增量路径必须调用 IncrementalManager.linkCodePath,因为真正的数据由 storage 管理。随后 request 和解析结果都改用 afterCodeFile,使 PackageSetting、Resolver 和查询快照指向最终 data-app 路径。增量路径跳过普通 restoreconRecursive,其安全标签由 IncFS/安装器路径处理。

5.2 加载进度 ​

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

java
final String codePath = ps.getPathString();
if (IncrementalManager.isIncrementalPath(codePath)
        && mIncrementalManager != null) {
    mIncrementalManager.registerLoadingProgressCallback(codePath,
            new IncrementalProgressListener(ps.getPackageName(), mPm));
}

提交后的加载进度 owner 仍是 IncFS storage,PMS 通过 IncrementalProgressListener 把进度映射到包状态和外部观察者。非 incremental 包在 commit 时直接把 loading progress 设为 1f;增量包不能这样做,否则消费者会误以为所有页面已经到达。

5.3 native 屏障 ​

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

java
final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
for (ReconciledPackage reconciledPkg : reconciledPackages) {
    final PackageSetting ps = reconciledPkg.mInstallRequest
            .getScannedPackageSetting();
    if (isIncrementalPath(ps.getPathString())) {
        final IncrementalStorage storage = mPm.mIncrementalManager
                .openStorage(ps.getPathString());
        if (storage == null) {
            throw new IllegalArgumentException(
                    "Install: null storage for incremental package "
                            + ps.getPackageName());
        }
        incrementalStorages.add(storage);
    }
}
PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental(
        incrementalStorages);

commit 后的 post-commit 阶段重新打开每个增量 storage,并等待 native binaries extraction。原因是 APK 主体可以按需加载,但 native library 不能在加载尚未完成时就交给应用进程。storage 打不开会抛出内部错误,不能静默把安装当成普通 APK。

6. 清理与生命周期 ​

6.1 session 完成 ​

源码文件:frameworks/base/core/java/android/os/incremental/IncrementalFileStorages.java

java
public void cleanUpAndMarkComplete() {
    final IncrementalStorage defaultStorage = cleanUp();
    if (defaultStorage != null) {
        defaultStorage.onInstallationComplete();
    }
}

安装 session 完成后,storage 不能继续以“临时 staging”身份存在;onInstallationComplete() 把它切换到已安装状态。失败或 abandon 则只执行 cleanUp(),释放绑定和 DataLoader,避免残留缺页回调指向已删除路径。

6.2 失败路径 ​

增量安装失败至少有三类清理:image 未准备好时销毁 DataLoader;解析/校验失败时删除增量 code path;commit 后异常时关闭 storage 并清理 native binary。因为 IncFS 文件可能看起来“已存在”但内容未完整,排查残留时应同时查看 session 状态、storage 是否仍绑定和 DataLoader 是否收到 destroy,而不能只检查目录名。

7. 调用时序 ​

8. 验证方法 ​

8.1 命令分叉 ​

在支持 IncFS 的设备上分别执行 adb install、adb install-streaming 和 adb install-incremental。记录 session 的 data loader type、pm path 和安装完成时间。关键断言是只有 incremental 路径被识别为 incremental storage,并且 commit 后仍能观察到 loading progress;这不能证明每个 APK 页面都已从远端成功加载。

8.2 缺页与不可用 ​

让 DataLoader 延迟提供某个 APK 区域,启动应用并观察缺页请求是否触发数据加载;再让 DataLoader 返回 unavailable 与 unrecoverable 两种状态。前者应保留 session 并允许重试,后者应返回 INSTALL_FAILED_MEDIA_UNAVAILABLE。这组实验覆盖状态机的两个非正常分支。

8.3 路径与 native ​

安装完成后执行:

bash
adb shell pm path <package.name>
adb shell dumpsys package <package.name> | grep -E 'loading|codePath|versionCode'

codePath 应是最终 /data/app 路径;增量安装的 loading 状态不应在首个 commit 瞬间被强制写成普通包的静态完成值。若 native library 尚未准备好就启动失败,沿 executePostCommitStepsLIF 和 waitForNativeBinariesExtractionForIncremental 检查,而不是只重试 APK 传输。

9. 源码路线 ​

  1. PackageManagerShellCommand.runIncrementalInstall:确认命令只选择 DataLoader 参数。
  2. PackageInstallerSession.addFile/prepareDataLoaderLocked:跟踪 metadata、文件清单和状态回调。
  3. IncrementalFileStorages.initialize:理解 storage、文件和进度回调的创建顺序。
  4. PackageInstallerSession 的 IDataLoaderStatusListener:区分 ready、retryable unavailable 和 unrecoverable。
  5. InstallPackageHelper 的 linkCodePath 分支:确认增量路径如何进入 /data/app。
  6. commitPackageSettings 和 registerLoadingProgressCallback:跟踪消费者何时看到 loading 状态。
  7. executePostCommitStepsLIF:确认 native binary extraction 是提交后的屏障。

Incremental 安装的本质是把“文件存在”拆成“路径已建立、元数据已登记、缺页可获取、native 可加载”四个阶段。PackageInstallerSession 管理 DataLoader 状态,IncFS storage 管理页面和生命周期,PMS 管理包提交与消费者注册;任何一层失败,都应沿这条边界定位。