安装加载进度
本文面向已经读过 增量安装、PackageStateMutator 事务提交 和 安装失败清理 的读者。本文不讲 Incremental FS 的文件传输协议,而是追踪 PMS 如何保存“包是否仍在加载、加载到多少、何时完成”,以及系统内部消费者怎样订阅和读取这些状态。
1. 两个时间点
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java。
包级加载状态由两个字段组成:
private float mLoadingProgress;
private long mLoadingCompletedTime;
public boolean isLoading() {
return PackageManagerServiceUtils.isLoading(mLoadingProgress);
}mLoadingProgress 是 0 到 1 之间的进度值,isLoading() 通过公共工具判断是否仍未完成;mLoadingCompletedTime 是完成时间戳。它们属于 PackageSetting 全局包状态,不是每个 user 一份。
2. 单调更新
源码符号:setLoadingProgress、setLoadingCompletedTime。
public PackageSetting setLoadingProgress(float progress) {
// To prevent race conditions, we don't allow progress to ever go down
if (mLoadingProgress < progress) {
mLoadingProgress = progress;
onChanged();
}
return this;
}
public PackageSetting setLoadingCompletedTime(long loadingCompletedTime) {
mLoadingCompletedTime = loadingCompletedTime;
onChanged();
return this;
}进度只允许增加,防止多个 Incremental 回调线程把状态倒退。完成时间没有同样的单调保护,调用者需要保证它只在完成事件写入。两个 setter 都调用 onChanged(),会使 PackageSetting watcher 和 snapshot cache 感知变化。
3. 增量包回调
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java;符号:commit 后的增量监听注册。
if (mIncrementalManager != null
&& IncrementalManager.isIncrementalPath(parsedPackage.getPath())) {
if (scanResult.mPkgSetting != null
&& scanResult.mPkgSetting.isLoading()) {
mIncrementalManager.registerLoadingProgressCallback(
parsedPackage.getPath(),
new IncrementalProgressListener(
parsedPackage.getPackageName(), mPm));
}
}只有 code path 是 incremental path 且 package 仍处于 loading 才注册回调。已完成包不再订阅进度,避免回调线程继续触碰稳定状态。
4. 非增量完成
同一提交路径对普通文件安装做相反处理:
if (!IncrementalManager.isIncrementalPath(pkgSetting.getPathString())) {
pkgSetting.setLoadingProgress(1f);
}普通 APK 不依赖 IncrementalManager 的分块回调,因此在包状态提交后直接标记完成。安装成功和 loading 完成在这里汇合;安装失败不会执行这段成功分支,而由 post-install cleanup 删除 code path。
5. 系统内部查询
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:PackageManagerInternal.getIncrementalStatesInfo。
public IncrementalStatesInfo getIncrementalStatesInfo(
@NonNull String packageName, int filterCallingUid, int userId) {
final Computer snapshot = snapshotComputer();
final PackageStateInternal ps =
snapshot.getPackageStateForInstalledAndFiltered(
packageName, filterCallingUid, userId);
if (ps == null) {
return null;
}
return new IncrementalStatesInfo(ps.isLoading(),
ps.getLoadingProgress(), ps.getLoadingCompletedTime());
}查询同时受 installed、user 和 visibility 过滤。filterCallingUid 是可信的原始调用者 UID,允许系统内部代码在清除 Binder identity 后仍按原调用者过滤;返回 null 不区分“包不存在”和“调用者不可见”。
6. 回调注册
源码符号:registerInstalledLoadingProgressCallback。
PackageStateInternal ps = snapshot
.getPackageStateForInstalledAndFiltered(
packageName, Binder.getCallingUid(), userId);
if (ps == null) {
return false;
}
if (!ps.isLoading()) {
Slog.w(TAG, "Package is fully loaded.");
return false;
}
if (mIncrementalManager == null) {
Slog.w(TAG, "Incremental is not enabled");
return false;
}
return mIncrementalManager.registerLoadingProgressCallback(
ps.getPathString(), callback.getBinder());注册失败有三个明确原因:包不可见/未安装、包已经完成加载、设备没有 IncrementalManager。PMS 不会为了注册回调而重新创建 loading state。
7. 查询与回调的竞态
查询和回调注册都先创建 Computer snapshot,但回调实际注册发生在 IncrementalManager。包可能在 snapshot 创建后完成加载,导致注册返回 false;这是正常竞态,调用者应回退到一次性读取 getIncrementalStatesInfo。
反过来,查询返回 isLoading=true 也不保证下一次回调仍会到达:安装失败、包删除或 code path 清理都可能让监听失效。loading state 是当前包状态的观察接口,不是传输协议的可靠进度日志。
8. 持久化
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java。
PackageSetting 的 loadingProgress 和 loadingCompletedTime 写入 package settings;启动恢复后,PMS 仍可通过 isLoading() 判断一个增量包是否需要重新注册回调。普通包完成时进度为 1,通常不会继续处于 loading。
持久化写入和 IncrementalManager 回调不是同一个时刻。回调更新先改变内存并触发 watcher,Settings 写盘由 PMS 的 settings 调度决定;异常掉电可能使磁盘进度落后于内存。
9. 失败清理
安装失败时,InstallPackageHelper.doPostInstallCleanUp 对 move install 调用 cleanUpForMoveInstall,普通安装调用 removeCodePath(request.getCodeFile())。这会移除 Incremental/Installer code path,但不会把一个失败包强行写成“加载完成”。
如果扫描阶段已经注册 Incremental progress callback,删除 code path 后回调源已经不存在;下次启动通过 package state 和 path reconcile 决定是否重新扫描或删除残留 setting。
10. 查询消费者
IncrementalStatesInfo 主要供系统内部的安装、加载 UI 和 Incremental 相关服务读取。PMS 不把 loading progress 自动转换成 PackageInstaller session 的公开安装进度;session 的 progress、文件写入和 package loading 是不同层的状态。
因此“PackageInstaller 显示 100%”与“PackageSetting.isLoading() 为 false”不能互相替代,必须分别查看 session 和 package state。
11. 失败定位
| 现象 | 检查点 | 解释 |
|---|---|---|
| 进度倒退 | setLoadingProgress | setter 明确拒绝小于当前值 |
| 回调注册返回 false | visibility、isLoading、IncrementalManager | 三个短路条件之一成立 |
| 查询返回 null | installed/user/filterCallingUid | 不区分不存在和不可见 |
| 普通 APK 一直 loading | commit 是否执行 setLoadingProgress(1f) | 非增量包不依赖回调 |
| 增量包失败后目录残留 | post-install removeCodePath、Installer 日志 | 失败清理可能部分完成 |
| 重启后进度落后 | Settings 写盘时机 | 内存 watcher 与磁盘持久化异步 |
12. 源码练习
- 从增量包扫描结果追到
registerLoadingProgressCallback,指出PackageSetting.isLoading()和 code path 判断各自保护什么。 - 构造进度回调先后提交
0.8、0.6、1.0,判断内存值和 watcher 触发次数。 - 对比
IncrementalStatesInfo与 PackageInstaller session progress,列出两者 owner、更新来源和完成条件。 - 给定安装失败后
isLoading=true仍出现在旧设置中,沿doPostInstallCleanUp、removeCodePath和启动扫描判断哪些状态可以暂时残留。
