扫描冲突处理
本文面向已经读过 ScanRequest 与 Result、扫描签名校验 和 静态共享库 的读者。前文分别解释了请求/结果对象、签名详情和静态库版本;本文把这些输入放回冲突裁决现场,追踪 system/data 同名包、替换安装、共享 UID、Provider、静态库和 APEX 内 APK 如何被检测、选择、隐藏、删除或恢复。
Android 17 没有一个叫“处理冲突”的总函数。冲突被分散在不同阶段:assertPackageIsValid() 做重复包与 Provider 的早期检查,scanPackageForInitLI() 决定 system 版本和 data 版本谁更可信,ReconcilePackageUtils.reconcilePackages() 处理替换删除、KeySet、证书和 shared user,commitReconciledScanResultLocked() 才把选择结果写进 live state。只看某一个 if,很容易把 warning、跳过、隐藏和删除混成同一种失败。
读完后,读者应能从冲突现象反查源码入口:判断是“同一批次重复包”还是“已安装包替换”,是 system/data 版本选择还是签名不兼容,是 Provider authority 冲突还是静态库逻辑名冲突;同时指出每条分支的 owner、错误码、数据清理范围和下一次启动的恢复入口。
1. 冲突分层
1.1 冲突类型
源码文件:
frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.javaframeworks/base/services/core/java/com/android/server/pm/ReconcilePackageUtils.javaframeworks/base/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
| 冲突 | 检测位置 | 主要输入 | 结果 |
|---|---|---|---|
| 同批次重复包名 | scanInstallPackages() | scannedPackages | INSTALL_FAILED_DUPLICATE_PACKAGE |
| 已知包路径不符 | assertPackageIsValid() | SCAN_REQUIRE_KNOWN、known path | INSTALL_FAILED_PACKAGE_CHANGED |
| system/data 同名 | scanPackageForInitLI() | 签名、版本、shared user、路径 | 启用、隐藏、删除 data 或跳过 system |
| 替换无法删除旧包 | reconcilePackages() | DeletePackageHelper | INSTALL_FAILED_REPLACE_COULDNT_DELETE |
| 旧包签名不兼容 | verifySignatures() | SigningDetails、disabled setting | INSTALL_FAILED_UPDATE_INCOMPATIBLE |
| shared user 不兼容 | verifySignatures() | shared user signing details | INSTALL_FAILED_SHARED_USER_INCOMPATIBLE |
| Provider authority 冲突 | ComponentResolver | 已注册 provider | INSTALL_FAILED_CONFLICTING_PROVIDER |
| 静态库逻辑名冲突 | InstallPackageHelper | static library reverse index | INSTALL_FAILED_DUPLICATE_PACKAGE |
| 批次共享库重复 | ReconcilePackageUtils | incoming library map | internal error |
这些冲突的共同点是“包名不是唯一判断条件”。路径、scan flags、旧 PackageSetting、disabled system setting、签名 lineage、版本和 shared user 都可能改变最终决策。
1.2 状态与所有者
ParsedPackage 只是当前 APK 的候选状态;PackageSetting 是旧状态或待提交副本;mPm.mPackages 是当前可查询的 live package map;mPm.mSettings 保存持久化安装状态。冲突处理的关键是判断哪一个对象可以被修改:
- data 包替换时,删除动作由
DeletePackageHelper准备,结果由ReconciledPackage携带; - system/data 选择时,
scanPackageForInitLI()修改mPackages、disabled setting 或shouldHideSystemApp; - commit 之前,扫描副本可以被丢弃,原有 live state 仍可能保持不变;
- commit 之后,失败清理不再等价于“撤销所有内存变化”。
2. 早期检查
2.1 批次重复
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
private boolean scanInstallPackages(List<InstallRequest> requests,
Map<String, Boolean> createdAppId, Map<String, Settings.VersionInfo> versionInfos) {
final Set<String> scannedPackages = new ArraySet<>(requests.size());
for (InstallRequest request : requests) {
final ParsedPackage packageToScan = request.getParsedPackage();
if (packageToScan == null) {
request.setError(INSTALL_FAILED_SESSION_INVALID,
"Failed to obtain package to scan");
return false;
}
final String packageName = packageToScan.getPackageName();
final ScanResult scanResult = scanPackageTraced(request.getParsedPackage(),
request.getParseFlags(), request.getScanFlags(),
System.currentTimeMillis(), request.getUser(),
request.getAbiOverride(), request.getInstallSource());
request.setScanResult(scanResult);
if (!scannedPackages.add(packageName)) {
request.setError(PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE,
"Duplicate package " + packageName
+ " in multi-package install request.");
return false;
}
}
return true;
}同一 multi-package request 中,重复包名由 scannedPackages 检测。这个检查发生在每个包已经完成 scanPackageTraced() 并写入 ScanResult 之后,所以“结果对象非空”不能说明整个批次成功;第二次出现同名包会使批次失败。
2.2 已知路径
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if ((scanFlags & SCAN_REQUIRE_KNOWN) != 0) {
if (mPm.isExpectingBetter(pkg.getPackageName())) {
Slog.w(TAG, "Relax SCAN_REQUIRE_KNOWN requirement for package "
+ pkg.getPackageName());
} else {
PackageSetting known = mPm.mSettings.getPackageLPr(pkg.getPackageName());
if (known != null) {
if (!pkg.getPath().equals(known.getPathString())) {
throw new PackageManagerException(INSTALL_FAILED_PACKAGE_CHANGED,
"Application package " + pkg.getPackageName()
+ " found at " + pkg.getPath()
+ " but expected at " + known.getPathString() + "; ignoring.");
}
} else {
throw new PackageManagerException(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
"Application package " + pkg.getPackageName()
+ " not found; ignoring.");
}
}
}SCAN_REQUIRE_KNOWN 不是普通的“包必须存在”开关,它同时要求 code path 与 Settings 中的 path 一致。mExpectingBetter 是 system package OTA 恢复的例外;否则路径变化返回 INSTALL_FAILED_PACKAGE_CHANGED,未知包返回 INSTALL_FAILED_INVALID_INSTALL_LOCATION。
2.3 Provider 冲突
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
// Verify that this new package doesn't have any content providers
// that conflict with existing packages. Only do this for a new install.
if ((scanFlags & SCAN_NEW_INSTALL) != 0) {
mPm.mComponentResolver.assertProvidersNotDefined(pkg);
}Provider authority 冲突只在 SCAN_NEW_INSTALL 分支做早期检查,以免更新已安装包时因历史状态重新声明而破坏当前系统。冲突由 ComponentResolver 依据已注册 provider 判断,和包签名或 system/data 版本选择不是同一层。
3. system/data 选择
3.1 版本与路径
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
final boolean newPkgChangedPaths = pkgAlreadyExists
&& !pkgSetting.getPathString().equals(parsedPackage.getPath());
final boolean newPkgVersionGreater = pkgAlreadyExists
&& parsedPackage.getLongVersionCode() > pkgSetting.getVersionCode();
final boolean newSharedUserSetting = pkgAlreadyExists
&& (initialScanRequest.mOldSharedUserSetting
!= initialScanRequest.mSharedUserSetting);
final boolean isSystemPkgBetter = scanSystemPartition && isSystemPkgUpdated
&& newPkgChangedPaths && (newPkgVersionGreater || newSharedUserSetting);system 版本只有在“路径确实改变”且“版本更高或 shared user 改变”时才被判定为 isSystemPkgBetter。版本相等或 system 更旧并不会自动替换 data;shared user 改变是一个特殊信号,即使版本较小也可能让 system 版本胜出。
3.2 system 胜出
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if (isSystemPkgBetter) {
// The version of the application on /system is greater than the version on /data.
synchronized (mPm.mLock) {
// just remove the loaded entries from package lists
mPm.mPackages.remove(pkgSetting.getPackageName());
}
logCriticalInfo(Log.WARN,
"System package updated; name: " + pkgSetting.getPackageName()
+ "; " + pkgSetting.getVersionCode() + " --> "
+ parsedPackage.getLongVersionCode());
mRemovePackageHelper.cleanUpResources(
pkgSetting.getPackageName(), new File(pkgSetting.getPathString()));
synchronized (mPm.mLock) {
mPm.mSettings.enableSystemPackageLPw(pkgSetting.getPackageName());
}
}system 胜出时,PMS 先从 live mPackages 移除旧 data 视图,再清理旧资源,最后启用 system package setting。此时保留应用数据,删除的是旧 code/resource 状态;随后 system APK 继续走扫描和 commit。
3.3 data 胜出
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if (scanSystemPartition && isSystemPkgUpdated && !isSystemPkgBetter) {
// The /system version is less than or equal to the /data version.
if (!allowUpdatedVersionBetterThanApkInApex() || !isApkInApex) {
parsedPackage.hideAsFinal();
throw PackageManagerException.ofInternalError(
"Package " + parsedPackage.getPackageName()
+ " at " + parsedPackage.getPath()
+ " ignored: updated version better than this "
+ parsedPackage.getLongVersionCode(),
INTERNAL_ERROR_UPDATED_VERSION_BETTER_THAN_SYSTEM);
}
}system APK 版本不优于 data 版本时,通常会 hideAsFinal() 后抛内部错误,让 system scan 结果被跳过,data 版本稍后继续作为更新包使用。APK-in-APEX 有例外条件:较新的 data copy 不应仅因为 APEX 内 APK 版本较旧而让 APEX 安装失败,但 APEX 内 APK 仍会被完整扫描,以保证未来移除 data copy 后不会暴露坏包。
3.4 新 system/data
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
boolean shouldHideSystemApp = false;
if (scanSystemPartition && !isSystemPkgUpdated && pkgAlreadyExists
&& !pkgSetting.isSystem()) {
if (!parsedPackage.getSigningDetails().checkCapability(
pkgSetting.getSigningDetails(), SigningDetails.CertCapabilities.INSTALLED_DATA)
&& !pkgSetting.getSigningDetails().checkCapability(
parsedPackage.getSigningDetails(), SigningDetails.CertCapabilities.ROLLBACK)) {
logCriticalInfo(Log.WARN, "System package signature mismatch; name: "
+ pkgSetting.getPackageName());
try (PackageFreezer freezer = mPm.freezePackage(
parsedPackage.getPackageName(), UserHandle.USER_ALL,
"scanPackageInternalLI", ApplicationExitInfo.REASON_OTHER, null)) {
mDeletePackageHelper.deletePackageLIF(
parsedPackage.getPackageName(), null, true,
mPm.mUserManager.getUserIds(), 0, new PackageRemovedInfo(), false,
Process.SYSTEM_UID);
}
} else if (newPkgVersionGreater || newSharedUserSetting) {
mRemovePackageHelper.cleanUpResources(
pkgSetting.getPackageName(), new File(pkgSetting.getPathString()));
} else {
shouldHideSystemApp = true;
}
}新 system 包与非 system data 包同名时分三路:
- 签名 capability 不匹配:冻结包并删除 data 包;
- 签名兼容且 system 更新更高或 shared user 改变:清理旧 data 资源,让 system 版本接管;
- 签名兼容但 system 不更好:设置
shouldHideSystemApp=true,让 data 版本稍后重新扫描。
这里的“隐藏”不是删除 system APK,而是暂时不把它作为 live package;addForInitLI() 结束后会调用 disableSystemPackageLPw() 保存隐藏状态。
4. reconcile 冲突
4.1 替换删除
源码文件:frameworks/base/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
final DeletePackageAction deletePackageAction;
// we only want to try to delete for non system apps
if (installRequest.isInstallReplace() && !installRequest.isInstallSystem()) {
final boolean killApp = (installRequest.getScanFlags() & SCAN_DONT_KILL_APP) == 0;
final int deleteFlags = PackageManager.DELETE_KEEP_DATA
| (killApp ? 0 : PackageManager.DELETE_DONT_KILL_APP);
deletePackageAction = DeletePackageHelper.mayDeletePackageLocked(
installRequest.getRemovedInfo(),
installRequest.getOriginalPackageSetting(),
installRequest.getDisabledPackageSetting(),
deleteFlags, null /* all users */, Process.SYSTEM_UID);
if (deletePackageAction == null) {
throw new ReconcileFailure(
PackageManager.INSTALL_FAILED_REPLACE_COULDNT_DELETE,
"May not delete " + installPackageName + " to replace");
}
} else {
deletePackageAction = null;
}普通替换只有在非 system install 时准备删除动作。DELETE_KEEP_DATA 保留用户数据;SCAN_DONT_KILL_APP 决定是否追加不杀进程标志。无法安全删除旧包时,reconcile 直接失败,不会继续签名和 commit。
4.2 KeySet 与证书
源码文件:frameworks/base/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
if (ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, sharedUserSetting, scanFlags)) {
if (!ksms.checkUpgradeKeySetLocked(signatureCheckPs, parsedPackage)) {
if (!isSystemPackage) {
throw new ReconcileFailure(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"Package " + parsedPackage.getPackageName()
+ " upgrade keys do not match the previously installed version");
} else {
String msg = "System package " + parsedPackage.getPackageName()
+ " signature changed; retaining data.";
PackageManagerService.reportSettingsProblem(Log.WARN, msg);
}
}
} else {
final boolean compatMatch = PackageManagerServiceUtils.verifySignatures(
signatureCheckPs, sharedUserSetting, disabledPkgSetting,
signingDetails, compareCompat, compareRecover, isRollback);
}KeySet 检查优先于普通证书比较。非 system 包 KeySet 不匹配直接失败;system 包记录 warning 并保留数据。没有 upgrade keyset 时才进入 verifySignatures(),在那里处理 installed-data、rollback、兼容升级和恢复签名。
4.3 shared user
verifySignatures() 在包签名通过后还会检查 shared user 的签名集合和 lineage。若新包不能加入 shared user,返回 INSTALL_FAILED_SHARED_USER_INCOMPATIBLE;即使 capability 允许,也必须通过 hasCommonAncestor(),否则 lineage 分叉同样失败。冲突不只属于包自身,而是属于“包 + shared UID 组”的共同状态。
4.4 批次共享库
源码文件:frameworks/base/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
for (InstallRequest installRequest : installRequests) {
installRequest.onReconcileStarted();
combinedPackages.put(installRequest.getScannedPackageSetting().getPackageName(),
installRequest.getParsedPackage());
final List<SharedLibraryInfo> allowedSharedLibInfos =
sharedLibraries.getAllowedSharedLibInfos(installRequest);
if (allowedSharedLibInfos != null) {
for (SharedLibraryInfo info : allowedSharedLibInfos) {
if (!SharedLibraryUtils.addSharedLibraryToPackageVersionMap(
incomingSharedLibraries, info)) {
throw ReconcileFailure.ofInternalError(
"Shared Library " + info.getName()
+ " is being installed twice in this set!",
PackageManagerException.INTERNAL_ERROR_SHARED_LIB_INSTALLED_TWICE);
}
}
}
}同一安装批次的 static library name/version 不能重复声明。这个冲突在 reconcile 第一遍就失败,不会等到全局 mSharedLibraries commit 后才发现;incomingSharedLibraries 是批次级临时索引。
5. 恢复路径
5.1 expecting better
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
@GuardedBy("mPm.mLock")
public void checkExistingBetterPackages(ArrayMap<String, File> expectingBetterPackages,
List<String> stubSystemApps, int systemScanFlags, int systemParseFlags) {
for (int i = 0; i < expectingBetterPackages.size(); i++) {
final String packageName = expectingBetterPackages.keyAt(i);
if (mPm.mPackages.containsKey(packageName)) {
continue;
}
final File scanFile = expectingBetterPackages.valueAt(i);
logCriticalInfo(Log.WARN, "Expected better " + packageName
+ " but never showed up; reverting to system");
final Pair<Integer, Integer> flags =
mPm.getSystemPackageRescanFlagsAndReparseFlags(
scanFile, systemScanFlags, systemParseFlags);
if (flags.first == 0) {
Slog.e(TAG, "Ignoring unexpected fallback path " + scanFile);
continue;
}
mPm.mSettings.enableSystemPackageLPw(packageName);
try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
initPackageTracedLI(scanFile, flags.second, flags.first);
} catch (PackageManagerException e) {
Slog.e(TAG, "Failed to parse original system package: " + e.getMessage());
}
}
}如果 system 包被认为“应该有更好的 data 版本”但 data 版本最终没有出现,checkExistingBetterPackages() 会根据原始 code path 反向恢复 scan/reparse flags,重新启用并扫描 system APK。未知 fallback path 被忽略,原始 system APK 解析失败只记录错误,不会伪造成功状态。
5.2 disabled setting
system/data 冲突处理中,disabled system package 保存 system 原始版本的签名、路径和 metadata;data 更新版本可能成为 live package。下次扫描 system APK 时,PMS 通过 disabled setting 判断是否仍是 updated system app,并在必要时把原始签名重新收集写回 disabled setting,保证后续 data reconcile 有可信比较对象。
5.3 APEX 例外
APEX 内 APK 的冲突不能简单套 system/data 版本规则:更新 APEX 中的 APK 可能与 system image 或 data copy 同名,allowUpdatedVersionBetterThanApkInApex() 决定较新的 data copy 是否允许压过 APEX 内版本;无论是否最终采用,源码都要求完整扫描 APK-in-APEX,以防将来 data copy 消失后暴露无效包。
6. 测试与诊断
6.1 测试输入
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
可直接覆盖冲突相关状态的测试包括:
- system package 设置
FLAG_SYSTEM后,扫描结果应反映 system 状态; updateSystemApp_applicationInfoFlagSet注入 existing/disabled setting,断言结果含FLAG_UPDATED_SYSTEM_APP;installStaticSharedLibrary断言 synthetic package name 和 static library version;newInstallSimpleAllNominal与updateSimpleNominal对照新设置创建和更新副本;installRealPackageName验证 original package name 不会被误当成 static library synthetic name。
这些测试证明扫描阶段对象状态,不能单独证明 system/data 删除动作、Provider authority 冲突或完整 commit 恢复。
6.2 日志与错误码
adb shell logcat -s PackageManager PackageManagerService ReconcilePackageUtils
adb shell dumpsys package <package-name>
adb shell dumpsys package packages重点匹配日志:
| 日志/错误 | 说明 |
|---|---|
Duplicate package ... in multi-package install request | 同一批次重复包名 |
found at ... but expected at ... | SCAN_REQUIRE_KNOWN 路径改变 |
System package signature mismatch | 新 system 与旧 data 签名 capability 不兼容 |
System package disabled | data 版本更高,system 版本隐藏 |
System package enabled | system 版本更高或 shared user 改变 |
UPDATE_INCOMPATIBLE | KeySet/旧包签名/替换关系失败 |
SHARED_USER_INCOMPATIBLE | shared UID 签名或 lineage 冲突 |
CONFLICTING_PROVIDER | Provider authority 已被占用 |
6.3 决策复盘
遇到同包名冲突时,建议记录一张最小状态表:
| 字段 | system/data 对照 |
|---|---|
| code path | /system/...、/data/app/... 或 /apex/... |
| version code | 新旧版本比较 |
SigningDetails | current signer、lineage、capability |
isSystem/isUpdatedSystemApp | PackageSetting 和 disabled setting |
| shared user | old/new SharedUserSetting |
| scan flags | SCAN_AS_SYSTEM、SCAN_NEW_INSTALL、SCAN_AS_APEX |
| live state | mPm.mPackages 是否存在 |
| cleanup | data code path、资源、AppId 是否已清理 |
只有把这些字段同时对照,才能区分“system 被隐藏等待 data 重扫”和“data 已被删除、system 接管”这两种完全不同的结果。
7. 源码路线
定位扫描冲突时,可以沿以下顺序阅读:
scanInstallPackages():确认批次重复包和扫描结果写入时机。assertPackageIsValid():确认已知路径、Provider、静态库包名和基本 validity。scanPackageForInitLI():确认 system/data 版本、路径、签名、shared user 和 APEX 选择。ReconcilePackageUtils.reconcilePackages():确认替换删除、KeySet、证书和 shared user 的二阶段判断。PackageManagerServiceUtils.verifySignatures():确认 capability、compat/recover、rollback 和 disabled setting。SharedLibrariesImpl:确认静态库版本和批次共享库冲突。commitReconciledScanResultLocked():确认隐藏/删除/更新后的设置何时进入 live state。checkExistingBetterPackages():确认“更好版本没有出现”时的 system 恢复。
一个实用练习是:设备上已有 /data/app/foo 版本 5,OTA 新增 /system/app/foo 版本 4,签名 lineage 兼容,且 system 版本没有 shared user 改变。请指出 system scan 会设置哪个布尔状态、是否删除 data、何时重新扫描 data,以及如果 data 目录随后消失,哪个恢复方法会重新启用 system 包。
8. 设计收束
Android 17 的扫描冲突处理可以归纳为一条状态裁决链:
- 早期检查负责阻止同批次重复包、未知路径、Provider authority 和静态库逻辑名冲突;
- system/data 选择同时依赖签名 capability、版本、路径变化和 shared user,结果可能是删除 data、启用 system、隐藏 system 或跳过当前 system 输入;
- 普通替换先准备可删除动作,再做 KeySet/证书/shared user reconcile,失败不会继续 commit;
ReconciledPackage和PackageSetting副本承载待提交状态,mPm.mPackages只有在 commit 后才代表最终可查询对象;- disabled system setting、
mExpectingBetter和checkExistingBetterPackages()为 OTA/更新异常提供恢复路径; - APEX 内 APK、static library 和 Provider 各自有额外边界,不能用“包名冲突 = 删除旧包”概括。
后续专题会进入增量扫描,继续分析首次启动、普通启动、fingerprint 变化和 OTA 之后,PMS 如何决定哪些冲突状态需要重扫、复用或清理。
