系统应用更新
本文面向已经读过 安装前检查 和 安装后注册 的读者。这里不重新讲普通 APK 的解析、签名校验和 dexopt,而是回答一个更容易混淆的问题:/system/app 中的只读 APK 被更新后,Package Manager 为什么仍能在 OTA 或卸载更新时找回它?读完后,读者应能沿着 Android 17 的源码定位覆盖包、原始包、用户状态和恢复动作各自的所有者,并解释一个更新包为什么会在 /data/app 生效。
系统应用更新不是把 /system 文件替换掉。PMS 把“原始系统包”和“当前运行包”拆成两份状态:原始包的路径、签名和版本保存在 Settings.mDisabledSysPackages,更新包作为普通可扫描包放在 /data/app,并在当前包表中成为消费者可见的对象。这个设计让只读分区保持不变,同时允许安装器更新系统应用。
1. 覆盖模型
1.1 两份包状态
系统包通常先从只读分区扫描,例如 /system/app/Settings/Settings.apk。更新安装把新 APK 放到 /data/app,但不会删除原文件。PMS 同时维护以下关系:
| 对象 | 典型位置 | 作用 | 谁读取 |
|---|---|---|---|
| 原始系统包 | /system、/product 等 | 提供恢复路径、预装版本和系统签名基线 | Settings、启动扫描、版本校验 |
| 更新包 | /data/app | 当前要运行的代码和资源 | mPackages、组件解析器、权限管理器 |
| disabled 记录 | mDisabledSysPackages | 保存原始包的 PackageSetting 快照 | 删除更新、OTA 清理 |
| 用户状态 | PackageSetting 的 per-user state | 记录每个用户是否安装、启用 | updateSettingsInternalLI、查询接口 |
图中 mPackages 是运行时消费者看到的当前包表;mDisabledSysPackages 不是第二个可运行包表,而是恢复原始系统包所需的保存区。因此“禁用”描述的是包设置角色,不等于把 APK 从磁盘删除。
1.2 更新成立的边界
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
preparePackage 在解析包、判断替换对象后,才把已有设置标记为系统包更新。系统包有两个明确的安装位置限制:
if (systemApp) {
if (onExternal) {
// 系统应用更新不能落在外部存储
throw new PrepareFailure(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
"Cannot install updates to system apps on sdcard");
} else if (instantApp) {
// instant app 不能成为系统包的覆盖层
throw new PrepareFailure(INSTALL_FAILED_SESSION_INVALID,
"Cannot update a system app with an instant app");
}
}systemApp 来自已有 PackageSetting 的 isSystem(),而不是来自调用者传入的名称。这里的失败发生在 commit 之前,所以不会修改 mPackages,也不会产生“先禁用、再恢复”的中间状态。版本降级另由 verifyReplacingVersionCode() 处理,不能把上述位置限制误读成“所有系统包更新都禁止降级”。
2. Prepare 分支
2.1 替换对象
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
preparePackage 的替换判断先建立本次请求和已有设置的联系:
boolean replace = false;
boolean systemApp = false;
PackageSetting ps = null;
if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {
// renamed package 先归一化到真实包名
final String renamedPackage = mPm.mSettings.getRenamedPackageName(pkgName);
if (renamedPackage != null) {
pkgName = renamedPackage;
}
synchronized (mPm.mLock) {
ps = mPm.mSettings.getPackageLPr(pkgName);
if (ps != null) {
replace = true;
systemApp = ps.isSystem();
}
}
}这里的 owner 是 Settings:它持有包名到 PackageSetting 的映射;InstallRequest 只携带本次安装上下文。systemApp 由旧设置推导出来,避免把一个同名但实际不是系统包的 APK 错当成系统更新。后续签名、shared library 和版本校验都使用这个替换关系。
2.2 预装版本基线
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
verifyReplacingVersionCode() 会比较长版本号、base revision code 以及同名 split 的 revision code。对系统包,禁止降级时还会取保存的预装设置:
final PackageSetting dataOwnerPs = mPm.mSettings.getPackageLPr(packageName);
final PackageSetting disabledSystemPs =
mPm.mSettings.getDisabledSystemPkgLPr(dataOwnerPs);
if (disabledSystemPs != null && !mPm.mIsDebuggableBuild
&& PackageManagerServiceUtils.checkDowngrade(dataOwnerPs, parsedPackage)) {
throw new PrepareFailure(INSTALL_FAILED_VERSION_DOWNGRADE,
"Downgrade of system package is not allowed");
}实际源码还会按安装标志、构建是否 debuggable 和 APK-in-APEX 等条件分支;上段展示的是关键关系:更新包的版本不能只和当前 /data 包比较,还可能需要和 disabledSystemPs 保存的系统镜像版本比较。这样 OTA 后旧 data 覆盖包不会通过一个宽松的 downgrade 标志绕过系统版本基线。
3. Commit 分支
3.1 旧运行对象
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
完成 prepare、scan、reconcile 后,commitPackagesLocked() 才改变运行时包表。系统包更新的分支如下:
if (installRequest.isInstallReplace()) {
final AndroidPackage oldPackage = mPm.mPackages.get(packageName);
installRequest.setScannedPackageSettingFirstInstallTimeFromReplaced(
deletedPkgSetting, allUsers);
final long currentTime = System.currentTimeMillis();
installRequest.setScannedPackageSettingLastUpdateTime(currentTime);
installRequest.setScannedPackageSettingFirstInstallTime(currentTime);
installRequest.getRemovedInfo().mBroadcastAllowList =
mPm.mAppsFilter.getVisibilityAllowList(
mPm.snapshotComputer(),
installRequest.getScannedPackageSetting(),
allUsers, mPm.mSettings.getPackagesLocked());
if (installRequest.isInstallSystem() && oldPackage != null) {
// 先从运行时注册表移除旧对象,避免新旧对象同时被解析
mRemovePackageHelper.removePackage(oldPackage, true);
if (!disableSystemPackageLPw(oldPackage)) {
// 已经是更新系统包:清理更旧的 data APK
installRequest.getRemovedInfo().mArgs =
new CleanUpArgs(packageName, oldPackage.getPath());
} else {
installRequest.getRemovedInfo().mArgs = null;
}
} else {
mDeletePackageHelper.executeDeletePackage(
reconciledPkg.mDeletePackageAction, packageName,
true, allUsers, false, installRequest.isKeepArtProfile());
}
}removePackage(oldPackage, true) 只处理当前运行对象及其注册关系;对只读分区中的原始 APK 没有“物理删除”语义。紧接着 disableSystemPackageLPw 把原始 PackageSetting 放入保存区。若保存区原本已有记录,说明这是“更新包再次更新”,此时 mArgs 指向旧的 data APK,供后续清理;首次覆盖则保留原始路径,不设置清理参数。
3.2 当前包注册
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
旧对象处理完成后,提交新的扫描结果:
final AndroidPackage pkg = commitReconciledScanResultLocked(
reconciledPkg, allUsers);
updateSettingsLI(pkg, allUsers, installRequest);commitReconciledScanResultLocked 会把新的 PackageSetting 和 AndroidPackage 写回 Settings 与 mPackages;随后 commitPackageSettings 将组件、可见性、包属性和权限通知给各自消费者:
mPm.mSettings.insertPackageSettingLPw(pkgSetting, pkg);
mPm.mPackages.put(pkg.getPackageName(), pkg);
mPm.mComponentResolver.addAllComponents(pkg, chatty,
mPm.mSetupWizardPackage, snapshot);
mPm.mAppsFilter.addPackage(pkgSetting, mPm.mSettings.getPackagesLocked());
mPm.addAllPackageProperties(pkg);
mPm.mPermissionManager.onPackageAdded(pkgSetting,
(scanFlags & SCAN_AS_INSTANT_APP) != 0, oldPkg);生效时机是 commit 完成后:Intent 查询读 mComponentResolver,包可见性读 AppsFilter,权限管理器读自己的包状态,而 Binder 查询通过 PMS 的快照看到新的 mPackages。因此“覆盖生效”不是单纯把文件复制到 /data/app,而是文件、包设置和各消费者注册共同完成的提交。
4. disabled 状态
4.1 保存原始设置
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
boolean disableSystemPackageLPw(String name, boolean replaced) {
final PackageSetting p = mPackages.get(name);
if (p == null) {
Log.w(PackageManagerService.TAG,
"Package " + name + " is not an installed package");
return false;
}
final PackageSetting dp = mDisabledSysPackages.get(name);
if (dp == null && p.getPkg() != null && p.isSystem()
&& !p.isUpdatedSystemApp()) {
final PackageSetting disabled;
if (replaced) {
// 更新提交不能改写当前系统设置,因此保存一份副本
disabled = new PackageSetting(p);
} else {
disabled = p;
}
p.getPkgState().setUpdatedSystemApp(true);
mDisabledSysPackages.put(name, disabled);
final SharedUserSetting sharedUserSetting =
getSharedUserSettingLPr(disabled);
if (sharedUserSetting != null) {
sharedUserSetting.mDisabledPackages.add(disabled);
}
return true;
}
return false;
}这个函数在 mPm.mLock 下运行。p 是当前表中的系统包设置,disabled 是恢复所需的副本;replaced=true 时复制尤其重要,因为随后新 data 包会复用包名并写入当前表。返回 false 并不表示安装失败,它表示该包已经是 updated system app,调用方需要把较旧的 data APK 交给清理逻辑。
4.2 重新启用原包
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java
PackageSetting enableSystemPackageLPw(String name) {
final PackageSetting p = mDisabledSysPackages.get(name);
if (p == null) {
Log.w(PackageManagerService.TAG,
"Package " + name + " is not disabled");
return null;
}
final SharedUserSetting sharedUserSetting = getSharedUserSettingLPr(p);
if (sharedUserSetting != null) {
sharedUserSetting.mDisabledPackages.remove(p);
}
p.getPkgState().setUpdatedSystemApp(false);
final AndroidPackageInternal pkg = p.getPkg();
final PackageSetting ret = addPackageLPw(name, p.getRealName(), p.getPath(),
p.getAppId(), p.getFlags(), p.getPrivateFlags(),
mDomainVerificationManager.generateNewId(),
pkg != null && pkg.isSdkLibrary(), p.hasSharedUser());
// 复制 ABI、版本、库依赖和安装来源等持久属性
mDisabledSysPackages.remove(name);
return ret;
}启用动作先移除 disabled 关系,再把原始设置重新放回 mPackages。它本身不负责扫描 APK;调用方必须随后调用 installPackageFromSystemLIF,否则只有设置恢复而没有组件和代码对象。这个分离解释了恢复流程中为什么同时需要 mLock 和 mInstallLock。
5. 用户状态
5.1 更新时继承状态
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
final int[] installedForUsers = ps.queryInstalledUsers(allUsers, true);
if (ps.isSystem()) {
// 系统包升级默认意味着用户要运行新代码
if (!installRequest.isApplicationEnabledSettingPersistent()) {
for (int userId : installedForUsers) {
if (requestedUserId == UserHandle.USER_ALL
|| requestedUserId == userId) {
ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT,
userId, installerPackageName);
}
}
}
for (int userId : allUsers) {
ps.setInstalled(ArrayUtils.contains(installedForUsers, userId), userId);
ps.resetOverrideComponentLabelIcon(userId);
}
}PackageSetting 是 per-user 状态的 owner。更新包不会简单地把所有用户设为 installed=true,而是先读取旧包的安装集合,再把集合复制到新包;只有调用者指定的用户会在后续分支中被设为已安装。这样可以保留“某用户卸载、另一用户仍安装”的差异。若安装器要求持久化 enabled 状态,则不执行默认启用。
5.2 写入时机
用户状态和包表的写入发生在 updateSettingsInternalLI 的锁保护区;随后 mSettings.writeLPr() 或临时设置写入把状态落盘。权限状态、内核 UID 映射和 overlay paths 也有各自写入点,不能用“APK 已经复制完成”作为持久化完成的判断。
6. 删除更新
6.1 恢复顺序
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
删除更新包时,restoreDisabledSystemPackageLIF 负责把保存的系统包重新变成可扫描对象:
@GuardedBy("mPm.mInstallLock")
public void restoreDisabledSystemPackageLIF(DeletePackageAction action,
int[] allUserHandles, boolean writeSettings) throws SystemDeleteException {
final PackageSetting deletedPs = action.mDeletingPs;
final PackageRemovedInfo outInfo = action.mRemovedInfo;
final PackageSetting disabledPs = action.mDisabledPs;
synchronized (mPm.mLock) {
// 扫描检查要求系统包先处于 enabled 状态
mPm.mSettings.enableSystemPackageLPw(
disabledPs.getPkg().getPackageName());
PackageManagerServiceUtils.removeNativeBinariesLI(deletedPs);
}
try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
installPackageFromSystemLIF(disabledPs.getPathString(),
allUserHandles,
outInfo == null ? null : outInfo.mOrigUsers,
writeSettings);
}
}先拿 mLock 恢复设置,再拿 mInstallLock 解析和注册 APK,顺序保证扫描期间不会看到一个仍标记为 disabled 的原始包。升级包的 native libraries 在恢复前清掉,避免旧 data 产物继续被加载。若恢复后存在原用户集合,函数还会把删除前保存的 overlay paths 写回用户状态。
6.2 重新扫描系统 APK
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
private void installPackageFromSystemLIF(String codePathString,
int[] allUserHandles, int[] origUserHandles, boolean writeSettings)
throws PackageManagerException {
final File codePath = new File(codePathString);
final int parseFlags = mPm.getDefParseFlags()
| ParsingPackageUtils.PARSE_MUST_BE_APK
| ParsingPackageUtils.PARSE_IS_SYSTEM_DIR;
final int scanFlags = mPm.getSystemPackageScanFlags(codePath);
final AndroidPackage pkg = initPackageTracedLI(codePath, parseFlags, scanFlags);
synchronized (mPm.mLock) {
final PackageSetting pkgSetting =
mPm.mSettings.getPackageLPr(pkg.getPackageName());
try {
mSharedLibraries.updateSharedLibraries(pkg, pkgSetting, null, null,
Collections.unmodifiableMap(mPm.mPackages));
} catch (PackageManagerException e) {
Slog.e(TAG, "updateAllSharedLibrariesLPw failed: " + e.getMessage());
}
}
setPackageInstalledForSystemPackage(pkg, allUserHandles,
origUserHandles, writeSettings);
mAppDataHelper.prepareAppDataAfterInstallLIF(pkg);
}恢复不是把旧 PackageSetting 原样塞回表,而是按系统目录的 parse/scan flags 重新初始化包,重新计算 shared library 路径,并准备 app data。setPackageInstalledForSystemPackage 会把 origUserHandles 映射回 installed 状态,最后由权限管理器处理安装权限。调用链的消费者因此重新获得组件、库、权限和数据目录,而不仅是一个文件路径。
7. OTA 清理
7.1 启动时分类
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
启动扫描结束后,prepareSystemPackageCleanUp 逐个检查系统包设置:
if (!ps.isSystem()) {
continue;
}
final AndroidPackage scannedPkg = mPm.mPackages.get(packageName);
final PackageSetting disabledPs =
mPm.mSettings.getDisabledSystemPkgLPr(packageName);
if (scannedPkg != null) {
if (disabledPs != null) {
// OTA 同时扫描了系统包和 data 更新包:移除当前系统对象,等待更好的 data 版本
mRemovePackageHelper.removePackage(scannedPkg, true);
expectingBetter.put(packageName, ps.getPath());
}
continue;
}
if (disabledPs == null) {
// 原系统 APK 已经不存在,用户数据无从恢复
mRemovePackageHelper.removePackageData(ps, userIds);
} else if (disabledPs.getPath() == null
|| !disabledPs.getPath().exists()
|| disabledPs.getPkg() == null) {
possiblyDeletedUpdatedSystemApps.add(packageName);
} else {
expectingBetter.put(packageName, disabledPs.getPath());
}这里有三个结果:expectingBetter 表示仍应尝试恢复或等待 data 版本;possiblyDeletedUpdatedSystemApps 表示 disabled 记录指向的原始路径已不存在;没有 disabled 记录的系统包则直接清理数据。APEX 包有单独分支,不应套用 APK 覆盖模型。
7.2 OTA 删除后的收尾
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
mPm.mSettings.removeDisabledSystemPackageLPw(packageName);
if (pkg == null) {
// data 更新和系统原包都不存在,后续协调阶段删除全部数据
msg = "Updated system package " + packageName
+ " no longer exists; removing its data";
} else {
// 找到 data 包,但它不再享有系统包特权
mRemovePackageHelper.removePackage(pkg, true);
final PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
if (ps != null) {
ps.getPkgState().setUpdatedSystemApp(false);
}
initPackageTracedLI(new File(pkg.getPath()), 0, scanFlags);
}
final PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
if (ps != null && mPm.mPackages.get(packageName) == null) {
mRemovePackageHelper.removePackageData(ps, userIds);
}先移除 disabled 记录是刻意的:后续重新扫描必须在“非 disabled”上下文中进行,否则扫描器会继续把它当成覆盖系统包。若 data 包还能扫描,就以普通 data 包身份重新注册并撤销系统特权;若扫描失败且设置仍存在但 mPackages 没有对象,最后一步才清理其包数据。
8. 压缩 stub
系统镜像可能只放一个压缩 stub。它不是普通更新,但复用了同一套“保存原系统包、移除运行对象、从 data 路径重新安装”的基础设施。
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
final File scanFile = decompressPackage(stubPkg.getPackageName(), stubPkg.getPath());
if (scanFile == null) {
throw PackageManagerException.ofInternalError(
"Unable to decompress stub at " + stubPkg.getPath(),
PackageManagerException.INTERNAL_ERROR_DECOMPRESS_STUB);
}
synchronized (mPm.mLock) {
mPm.mSettings.disableSystemPackageLPw(stubPkg.getPackageName(), true);
}
mRemovePackageHelper.removePackage(stubPkg, true);
try {
return initPackageTracedLI(scanFile, parseFlags, scanFlags);
} catch (PackageManagerException e) {
// 解压后的 data 代码路径不能残留
mRemovePackageHelper.removeCodePath(scanFile);
throw e;
}失败路径的关键是 removeCodePath(scanFile):stub 解压成功不等于扫描成功,临时 data 代码必须和设置状态一起回收。恢复 stub 时还会先 enableSystemPackageLPw,扫描完成后再按用户把 stub 设为 disabled,因为 stub 本身不可运行。
9. 状态路径
状态转移的 owner 是 Settings 中的 PackageSetting 与 mDisabledSysPackages;mPackages 是当前运行状态的消费者索引。Updated -> Restoring 由删除流程触发,Preloaded -> OtaWaiting 由启动扫描清理触发,两者都必须经过扫描和注册才能真正对外生效。
时序图强调两个提交屏障:旧对象先从消费者索引移除,新的组件注册随后发生;disableSystemPackageLPw 只保存恢复信息,不直接让 Resolver 使用原始 APK。
10. 验证路径
10.1 覆盖包定位
在设备上更新一个可更新的系统应用后,使用:
adb shell pm path <package.name>
adb shell dumpsys package <package.name>输入是已更新的系统包名。pm path 的关键断言是当前 base APK 位于 /data/app,而不是 /system/app;dumpsys package 应同时显示系统包标志、更新包路径和 per-user installed/enabled 状态。这个检查证明“当前消费者使用 data 路径”,不能单独证明 OTA 恢复一定成功。
10.2 删除更新恢复
对同一包执行恢复出厂版本的卸载操作(设备策略允许时):
adb shell pm uninstall --user 0 <package.name>
adb shell pm path <package.name>关键断言是用户 0 的状态变为未安装或回到系统包状态,路径重新指向只读分区;同时其他用户的状态应保持其原值。若路径仍在 /data/app,应继续检查 mDisabledSysPackages 对应记录、删除动作的 DeletePackageAction 和 restoreDisabledSystemPackageLIF 日志。该验证覆盖用户状态传播和恢复扫描,不证明 native library 清理是否完整。
10.3 失败边界
把系统包更新目标放到外部存储,或把 session 标记为 instant app,预期分别得到 INSTALL_FAILED_INVALID_INSTALL_LOCATION 和 INSTALL_FAILED_SESSION_INVALID。这两个输入在 prepare 阶段失败,因而不应看到新的 mPackages 注册;若已经出现新组件,说明失败发生在更晚阶段,需要转查 commit 清理路径。
11. 源码路线
建议按以下顺序阅读,能保持“输入 → 保存区 → 当前消费者 → 恢复”的主线:
InstallPackageHelper.preparePackage:确认替换对象、系统包标志和位置限制。InstallPackageHelper.verifyReplacingVersionCode:确认 data 版本与预装版本的比较条件。InstallPackageHelper.commitPackagesLocked:观察旧对象移除、disabled 保存和清理参数。Settings.disableSystemPackageLPw/enableSystemPackageLPw:理解两份PackageSetting的生命周期。InstallPackageHelper.updateSettingsInternalLI:跟踪 per-user enabled/installed 状态。InstallPackageHelper.restoreDisabledSystemPackageLIF:跟踪删除更新后的恢复顺序。prepareSystemPackageCleanUp/cleanupDisabledPackageSettings:跟踪 OTA 缺失、降级和数据清理分支。
12. 设计收束
系统应用更新的核心不在“覆盖一个文件”,而在于同时维护三条不变量:只读分区的原始 APK 始终可恢复;mPackages 任一时刻只暴露一个当前运行对象;每个用户的安装状态在更新、删除和 OTA 扫描之间可传播。mDisabledSysPackages 保存原始设置,InstallPackageHelper 负责时序,Settings 负责状态所有权,Resolver、PermissionManager 和 app-data 管理器负责消费提交结果。沿着这四个角色阅读,遇到“更新后仍运行旧版本”“卸载后没有恢复”或“OTA 后包被清理”等问题时,就能把现象对应到具体阶段,而不是只检查 /data/app 是否存在文件。
