Skip to content

系统应用更新

解释系统应用覆盖、原包禁用,以及卸载更新和 OTA 场景下的恢复路径。

基于android-17.0.0_r1
AndroidPackageManagerService安装系统应用源码阅读

系统应用更新 ​

本文面向已经读过 安装前检查 和 安装后注册 的读者。这里不重新讲普通 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 在解析包、判断替换对象后,才把已有设置标记为系统包更新。系统包有两个明确的安装位置限制:

java
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 的替换判断先建立本次请求和已有设置的联系:

java
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。对系统包,禁止降级时还会取保存的预装设置:

java
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() 才改变运行时包表。系统包更新的分支如下:

java
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

旧对象处理完成后,提交新的扫描结果:

java
final AndroidPackage pkg = commitReconciledScanResultLocked(
        reconciledPkg, allUsers);
updateSettingsLI(pkg, allUsers, installRequest);

commitReconciledScanResultLocked 会把新的 PackageSetting 和 AndroidPackage 写回 Settings 与 mPackages;随后 commitPackageSettings 将组件、可见性、包属性和权限通知给各自消费者:

java
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

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

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

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 负责把保存的系统包重新变成可扫描对象:

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

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 逐个检查系统包设置:

java
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

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

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 覆盖包定位 ​

在设备上更新一个可更新的系统应用后,使用:

bash
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 删除更新恢复 ​

对同一包执行恢复出厂版本的卸载操作(设备策略允许时):

bash
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. 源码路线 ​

建议按以下顺序阅读,能保持“输入 → 保存区 → 当前消费者 → 恢复”的主线:

  1. InstallPackageHelper.preparePackage:确认替换对象、系统包标志和位置限制。
  2. InstallPackageHelper.verifyReplacingVersionCode:确认 data 版本与预装版本的比较条件。
  3. InstallPackageHelper.commitPackagesLocked:观察旧对象移除、disabled 保存和清理参数。
  4. Settings.disableSystemPackageLPw / enableSystemPackageLPw:理解两份 PackageSetting 的生命周期。
  5. InstallPackageHelper.updateSettingsInternalLI:跟踪 per-user enabled/installed 状态。
  6. InstallPackageHelper.restoreDisabledSystemPackageLIF:跟踪删除更新后的恢复顺序。
  7. prepareSystemPackageCleanUp / cleanupDisabledPackageSettings:跟踪 OTA 缺失、降级和数据清理分支。

12. 设计收束 ​

系统应用更新的核心不在“覆盖一个文件”,而在于同时维护三条不变量:只读分区的原始 APK 始终可恢复;mPackages 任一时刻只暴露一个当前运行对象;每个用户的安装状态在更新、删除和 OTA 扫描之间可传播。mDisabledSysPackages 保存原始设置,InstallPackageHelper 负责时序,Settings 负责状态所有权,Resolver、PermissionManager 和 app-data 管理器负责消费提交结果。沿着这四个角色阅读,遇到“更新后仍运行旧版本”“卸载后没有恢复”或“OTA 后包被清理”等问题时,就能把现象对应到具体阶段,而不是只检查 /data/app 是否存在文件。