压缩系统 Stub 包
本文面向已经读过 系统包扫描、系统应用更新安装 和 卸载策略裁决 的读者。本文不把 stub 当作普通 APK 压缩,而是沿 Android 17 真实代码解释:system 分区为什么只放一个 manifest 外壳,完整 APK 何时解压到 /data,解压/扫描失败怎样恢复,以及用户禁用和更新回退如何改变最终状态。
1. Stub 的两个对象
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java。
压缩系统应用由两份代码形态组成:system 分区中的 tiny stub 和压缩的完整 payload。stub 通常只够让 PMS 解析 manifest;可执行的完整包必须被解压到 data 分区并重新扫描。
stub 不是最终运行包;mPackages 里暂时存在的 stub 也不意味着应用已经可启动。启动扫描必须在所有普通 system/data 包处理完后,最后展开或禁用 stub。
2. 启动时机
源码文件:frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.java;相关符号:updateStubSystemAppsList、installSystemStubPackages。
扫描期间,updateStubSystemAppsList 把 pkg.isStub() 的包名加入列表。InitAppsHelper 在系统和 data 扫描、旧 updated system app 修复、expecting-better 处理完成后,最后调用 stub 安装。
// Uncompress and install any stubbed system applications.
// This must be done last to ensure all stubs are replaced or disabled.
installSystemStubPackages(stubSystemApps, systemScanFlags);“最后”是状态顺序约束:如果在冲突修复前展开,PMS 可能把尚未决定最终归属的 stub 当作最终 system package;如果 data 版本优先级、disabled system package 或 renamed package 还未处理,解压结果可能被后续扫描覆盖。
3. 启动展开
源码符号:InstallPackageHelper.installSystemStubPackages。
for (int i = systemStubPackageNames.size() - 1; i >= 0; --i) {
String packageName = systemStubPackageNames.get(i);
if (mPm.mSettings.isDisabledSystemPackageLPr(packageName)) {
systemStubPackageNames.remove(i);
continue;
}
PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
if (ps != null && ps.getEnabled(UserHandle.USER_SYSTEM)
== COMPONENT_ENABLED_STATE_DISABLED_USER) {
systemStubPackageNames.remove(i);
continue;
}
try {
installStubPackageLI(pkg, 0, scanFlags);
ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT,
UserHandle.USER_SYSTEM, "android");
systemStubPackageNames.remove(i);
} catch (PackageManagerException e) {
Slog.e(TAG, "Failed to parse uncompressed system package", e);
}
}已经被 system setting 禁用的包和被用户禁用的 stub 不会自动展开。成功展开后,system user 的 enabled state 恢复为 DEFAULT;仍留在列表中的失败项随后被设为 COMPONENT_ENABLED_STATE_DISABLED。
4. 解压与重扫
源码符号:installStubPackageLI、decompressPackage。
File scanFile = decompressPackage(stubPkg.getPackageName(),
stubPkg.getPath());
if (scanFile == null) {
throw PackageManagerException.ofInternalError(
"Unable to decompress stub at " + stubPkg.getPath(),
INTERNAL_ERROR_DECOMPRESS_STUB);
}
synchronized (mPm.mLock) {
mPm.mSettings.disableSystemPackageLPw(
stubPkg.getPackageName(), true /* replaced */);
}
mRemovePackageHelper.removePackage(stubPkg, true /* chatty */);
return initPackageTracedLI(scanFile, parseFlags, scanFlags);decompressPackage 先确认压缩文件存在,再在 /data/app 生成目标 code path,调用 decompressFiles 和 native binary extraction。系统尚未 ready 时压缩块释放会延迟到 system ready;系统 ready 后则立即调用 F2FS compressed block release。
解压成功不等于安装成功。完整包仍要 parse、证书、ABI、shared library 和 scan 注册;initPackageTracedLI 失败时,源码显式调用 removeCodePath(scanFile) 删除临时 data 目录,避免留下未注册的 APK。
5. 失败回滚
源码符号:enableCompressedPackage。
try (PackageFreezer freezer = mPm.freezePackage(
stubPkg.getPackageName(), UserHandle.USER_ALL,
"setEnabledSetting",
ApplicationExitInfo.REASON_PACKAGE_UPDATED, null)) {
pkg = installStubPackageLI(stubPkg, parseFlags, 0);
mAppDataHelper.prepareAppDataAfterInstallLIF(pkg);
synchronized (mPm.mLock) {
mSharedLibraries.updateSharedLibraries(...);
mPm.mPermissionManager.onPackageInstalled(pkg, ...);
mPm.writeSettingsLPrTEMP();
}
}如果展开失败,恢复路径再次冻结包,在锁内 enableSystemPackageLPw,然后用 system 路径重新安装 stub。恢复成功后仍会把 stub 设为 disabled,因为 stub 本身不可运行;恢复失败则记录 wtf,表示 system package 基础设施出现严重问题。
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java;符号:enableCompressedPackage、installStubPackageLI。
6. 用户状态
stub 展开需要结合 user enabled state。system user 的 DISABLED_USER 会跳过启动展开;普通安装/启用路径调用 enableCompressedPackage 时则冻结 USER_ALL,解压完整包后再把用户状态恢复到可用路径。
这不是简单的 package 全局 enabled flag:PackageUserState 可能让一个用户启用、另一个用户仍未安装。卸载 updated system app 后,DeletePackageHelper 保存每个 user 的 enabled/installed 状态;恢复 system stub 后再按原状态决定是否重新展开 compressed package。
7. 手动启用
用户在 Settings 中重新启用一个此前因解压失败而 disabled 的 stub 时,PMS 会调用 enableCompressedPackage。这条路径与首次启动展开的不同点是:它必须冻结现有包、准备 app data、更新 shared libraries 和 PermissionManager,再清理 code cache;任何一步失败都会恢复 stub 并再次禁用。
mAppDataHelper.clearAppDataLIF(pkg, UserHandle.USER_ALL,
FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
| Installer.FLAG_CLEAR_CODE_CACHE_ONLY);清理的是 cache/code-cache,不是用户私有数据;目的是让新解压代码重新建立干净的运行时产物。
8. 更新与删除
删除 updated system app 时,PMS 先重新启用 disabled system package 以满足扫描器“不允许已禁用 package 作为待扫描 system package”的不变量,再安装 system stub。最后 disableStubPackage 根据原用户状态设置 stub enabled/disabled;如果至少一个用户原先使用完整包,才重新调用 enableCompressedPackage。
因此更新回退后的最终状态可能是:system 分区 stub 已注册但 disabled,或 data 分区完整包重新安装并对部分用户可用。不能只看 /system 文件是否存在判断应用是否恢复。
9. 失败定位
| 现象 | 首先检查 | 解释 |
|---|---|---|
| 启动后 stub 被禁用 | installStubPackageLI 异常和 systemStubPackageNames | 完整包解压或扫描失败,stub 保留但不可运行 |
| data 目录残留 | removeCodePath(scanFile) | 只在扫描失败分支清理临时代码;需检查具体异常 |
| 用户启用后仍失败 | enableCompressedPackage 回滚路径 | shared library、权限、app data 或扫描可能失败 |
| 更新回退后未重新展开 | priorUserStates、disableStubPackage | 只有原先 installed/enabled 用户才触发展开 |
| 压缩块未释放 | systemReady 时机、F2FS release | 启动早期会延迟释放,不代表解压未完成 |
| system stub 被当作可运行包 | 启动阶段顺序 | stub 展开必须在 scan/cleanup 收尾阶段最后执行 |
10. 源码练习
- 从
installSystemStubPackages追踪一个成功 stub:列出 skip、decompress、disable system、remove old package、scan 和 enabled state 恢复顺序。 - 构造完整 APK 扫描失败输入,指出
removeCodePath、system stub 恢复和最终 disabled state 分别发生在哪里。 - 对比首次启动展开和 Settings 手动启用,说明为什么后者需要 PackageFreezer、app data prepare 和 shared library 更新。
- 给定两个 user 只有 user 0 仍安装完整包,追踪 updated system app 删除后的 stub 是否重新展开,以及哪些字段来自
priorUserStates。
