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 已完整写入 | 普通文件读取 |
| streaming | DataLoader 准备的 staging image | IMAGE_READY 后才能 sealed | 提交后不再依赖按需缺页 |
| incremental | IncFS storage | image 可供解析,缺页可延后 | 读取缺页时由 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
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
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
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
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
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 不可恢复失败
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
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
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
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
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
安装完成后执行:
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. 源码路线
PackageManagerShellCommand.runIncrementalInstall:确认命令只选择 DataLoader 参数。PackageInstallerSession.addFile/prepareDataLoaderLocked:跟踪 metadata、文件清单和状态回调。IncrementalFileStorages.initialize:理解 storage、文件和进度回调的创建顺序。PackageInstallerSession的IDataLoaderStatusListener:区分 ready、retryable unavailable 和 unrecoverable。InstallPackageHelper的linkCodePath分支:确认增量路径如何进入/data/app。commitPackageSettings和registerLoadingProgressCallback:跟踪消费者何时看到 loading 状态。executePostCommitStepsLIF:确认 native binary extraction 是提交后的屏障。
Incremental 安装的本质是把“文件存在”拆成“路径已建立、元数据已登记、缺页可获取、native 可加载”四个阶段。PackageInstallerSession 管理 DataLoader 状态,IncFS storage 管理页面和生命周期,PMS 管理包提交与消费者注册;任何一层失败,都应沿这条边界定位。
