Skip to content

安装加载进度

追踪 Android 17 增量安装加载进度的状态写入、回调注册、查询和失败清理边界。

基于android-17.0.0_r1
AndroidPMSIncrementalInstallPackageSettingPackageInstaller

安装加载进度 ​

本文面向已经读过 增量安装、PackageStateMutator 事务提交 和 安装失败清理 的读者。本文不讲 Incremental FS 的文件传输协议,而是追踪 PMS 如何保存“包是否仍在加载、加载到多少、何时完成”,以及系统内部消费者怎样订阅和读取这些状态。

1. 两个时间点 ​

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

包级加载状态由两个字段组成:

java
private float mLoadingProgress;
private long mLoadingCompletedTime;

public boolean isLoading() {
    return PackageManagerServiceUtils.isLoading(mLoadingProgress);
}

mLoadingProgress 是 0 到 1 之间的进度值,isLoading() 通过公共工具判断是否仍未完成;mLoadingCompletedTime 是完成时间戳。它们属于 PackageSetting 全局包状态,不是每个 user 一份。

2. 单调更新 ​

源码符号:setLoadingProgress、setLoadingCompletedTime。

java
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 后的增量监听注册。

java
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. 非增量完成 ​

同一提交路径对普通文件安装做相反处理:

java
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。

java
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。

java
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. 失败定位 ​

现象检查点解释
进度倒退setLoadingProgresssetter 明确拒绝小于当前值
回调注册返回 falsevisibility、isLoading、IncrementalManager三个短路条件之一成立
查询返回 nullinstalled/user/filterCallingUid不区分不存在和不可见
普通 APK 一直 loadingcommit 是否执行 setLoadingProgress(1f)非增量包不依赖回调
增量包失败后目录残留post-install removeCodePath、Installer 日志失败清理可能部分完成
重启后进度落后Settings 写盘时机内存 watcher 与磁盘持久化异步

12. 源码练习 ​

  1. 从增量包扫描结果追到 registerLoadingProgressCallback,指出 PackageSetting.isLoading() 和 code path 判断各自保护什么。
  2. 构造进度回调先后提交 0.8、0.6、1.0,判断内存值和 watcher 触发次数。
  3. 对比 IncrementalStatesInfo 与 PackageInstaller session progress,列出两者 owner、更新来源和完成条件。
  4. 给定安装失败后 isLoading=true 仍出现在旧设置中,沿 doPostInstallCleanUp、removeCodePath 和启动扫描判断哪些状态可以暂时残留。