多用户安装
本文承接 UMS 与 PMS 和 per-user 包状态,聚焦安装动作如何落到不同用户。这里不重复 APK 解析、签名和 dexopt 主流程,而是回答三个具体问题:完整安装如何选择 newUsers,已存在 APK 如何只为一个用户启用,以及每个用户何时获得数据目录和 PACKAGE_* 广播。
“安装一个 APK”在多用户系统中有两个层次:代码安装是全局的,用户启用是 per-user 的。完整安装可能替换全局 code path,再为一组用户准备数据;installExistingPackageAsUser 不复制 APK,只修改目标用户的 PackageUserState。
1. 两条安装路径
| 路径 | APK 是否变化 | per-user 状态 | 典型入口 |
|---|---|---|---|
| 完整安装/更新 | 新增或替换 code path | 根据 request 选择多个 user | PackageInstallerSession → InstallPackageHelper |
| 启用已有包 | 不变化 | 仅目标 user 设 installed | installExistingPackageAsUser |
2. 用户选择
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:getNewUsers
private int[] getNewUsers(
InstallRequest installRequest, int[] allUsers)
throws PackageManagerException {
final int userId = installRequest.getUserId();
if (userId != UserHandle.USER_ALL
&& userId != UserHandle.USER_CURRENT
&& !ArrayUtils.contains(allUsers, userId)) {
throw new PackageManagerException(
PackageManagerException.INTERNAL_ERROR_MISSING_USER,
"User " + userId
+ " doesn't exist or has been removed");
}
IntArray newUserIds = new IntArray();
if (userId != UserHandle.USER_ALL) {
newUserIds.add(userId);
} else {
int[] installedForUsers =
installRequest.getOriginUsers();
for (int currentUserId : allUsers) {
boolean installedForCurrentUser =
ArrayUtils.contains(installedForUsers,
currentUserId);
boolean restrictedByPolicy =
mPm.isUserRestricted(currentUserId,
UserManager.DISALLOW_INSTALL_APPS)
|| mPm.isUserRestricted(currentUserId,
UserManager.DISALLOW_DEBUGGING_FEATURES);
if (installedForCurrentUser
|| !restrictedByPolicy) {
newUserIds.add(currentUserId);
}
}
}
return newUserIds.toArray();
}USER_ALL 并不意味着无条件给所有用户安装。已有用户如果已经安装该包会保留在集合中;尚未安装的用户只有在没有相关限制时才加入。反过来,指定 userId 的请求只影响那个用户,并且不存在或已删除的 user 会在提交阶段失败。
3. 提交后的数据准备
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:prepPerformDexoptIfNeeded
final int[] allUsers = mPm.mUserManager.getUserIds();
final int[] newUsers = getNewUsers(
request, allUsers);
mAppDataHelper.prepareAppDataPostCommitLIF(
ps, 0 /* previousAppId */, newUsers);
if (request.isClearCodeCache()) {
mAppDataHelper.clearAppDataLIF(
ps.getPkg(), UserHandle.USER_ALL,
FLAG_STORAGE_DE | FLAG_STORAGE_CE
| FLAG_STORAGE_EXTERNAL
| Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
}完整安装在全局包提交之后,才按照 newUsers 准备每个用户的数据。previousAppId=0 明确关闭这条路径的数据迁移;用户级 DE/CE 是否实际创建,还由 AppDataHelper 根据用户运行/解锁状态决定。
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:commitPackagesLocked
AndroidPackage pkg = commitReconciledScanResultLocked(
reconciledPkg, allUsers);
updateSettingsLI(pkg, allUsers, installRequest);
PackageSetting ps = mPm.mSettings
.getPackageLPr(packageName);
if (ps != null) {
installRequest.setNewUsers(
ps.queryInstalledUsers(allUsers, true));
}
if (installRequest.getReturnCode()
== PackageManager.INSTALL_SUCCEEDED) {
mPm.updateSequenceNumberLP(
ps, installRequest.getNewUsers());
}提交后重新查询 queryInstalledUsers 得到最终 new users,而不是完全相信请求初始参数。归档安装还可能在最后把包标为 archived/not installed,因此“安装成功”与“哪些用户最终 installed=true”要以提交后的 PackageSetting 为准。
4. 启用已有 APK
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:installExistingPackageAsUser
public Pair<Integer, IntentSender>
installExistingPackageAsUser(
@Nullable String packageName,
@UserIdInt int userId,
@InstallFlags int installFlags,
@InstallReason int installReason,
@Nullable List<String> allowlistedPermissions,
@Nullable IntentSender intentSender) {
final int callingUid = Binder.getCallingUid();
if (mContext.checkCallingOrSelfPermission(
android.Manifest.permission.INSTALL_PACKAGES)
!= PackageManager.PERMISSION_GRANTED
&& mContext.checkCallingOrSelfPermission(
android.Manifest.permission.INSTALL_EXISTING_PACKAGES)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"Neither caller has install permission");
}
Computer preLockSnapshot = mPm.snapshotComputer();
preLockSnapshot.enforceCrossUserPermission(
callingUid, userId, true, true,
"installExistingPackage for user " + userId);
if (mPm.isUserRestricted(userId,
UserManager.DISALLOW_INSTALL_APPS)) {
return Pair.create(
PackageManager.INSTALL_FAILED_USER_RESTRICTED,
intentSender);
}这条入口需要安装权限、跨用户权限和目标用户未被 DISALLOW_INSTALL_APPS 限制。它的安全边界比普通应用启动更严格,因为它会改变目标用户的 installed 状态。
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:状态修改分支
synchronized (mPm.mLock) {
PackageSetting ps = mPm.mSettings
.getPackageLPr(packageName);
if (ps == null || ps.getPkg() == null) {
return Pair.create(
PackageManager.INSTALL_FAILED_INVALID_URI,
intentSender);
}
if (!ps.getInstalled(userId)) {
ps.setInstalled(true, userId);
ps.setHidden(false, userId);
ps.setInstallReason(installReason, userId);
ps.setUninstallReason(
PackageManager.UNINSTALL_REASON_UNKNOWN,
userId);
ps.setFirstInstallTime(
System.currentTimeMillis(), userId);
mPm.mSettings.writePackageRestrictionsLPr(userId);
mPm.mSettings.writeKernelMappingLPr(ps);
installed = true;
}
}启用已有包的核心只有 per-user 状态变更:不会重新解析 APK、重新分配 appId 或重建全局 package record。若目标用户已经 installed,普通调用不会重复安装;INSTALL_FULL_APP 只在 Instant App 转 full app 等特殊分支继续处理。
5. Instant 与归档
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:Instant/full 分支
final boolean instantApp =
(installFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
final boolean fullApp =
(installFlags & PackageManager.INSTALL_FULL_APP) != 0;
if (instantApp && (ps.isSystem()
|| ps.isUpdatedSystemApp()
|| isPackageDeviceAdmin
|| isProtectedPackage)) {
return Pair.create(
PackageManager.INSTALL_FAILED_INVALID_URI,
intentSender);
}
ScanPackageUtils.setInstantAppForUser(
mPm.mInjector, ps, userId,
instantApp, fullApp);Instant/full 不是全局 APK 的两个版本,而是 per-user 的安装属性。系统包、设备管理员或受保护包不能通过这个入口转成 Instant App;状态修改完成后,后续可见性、权限和广播路径都会按目标用户的 instant 标志判断。
6. 数据目录时机
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:启用已有包的数据准备
if (installed && pkgSetting.getPkg() != null) {
mPm.mPermissionManager.onPackageInstalled(
pkgSetting.getPkg(), Process.INVALID_UID,
permissionParamsBuilder.build(), userId);
try (PackageManagerTracedLock lock =
mPm.mInstallLock.acquireLock()) {
mAppDataHelper.prepareAppDataPostCommitLIF(
pkgSetting, 0, new int[]{userId});
}
}数据准备发生在 per-user state 提交之后。AppDataHelper 根据用户状态和 storage flags 决定 DE/CE 创建时机:用户未解锁时不能假定 CE key 可用,用户尚未运行时也可能只记录待准备状态。调用者不能把 setInstalled(true) 当作“应用目录已经全部存在”。
7. 单用户广播
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:启用已有包的广播
boolean isArchived = pkgSetting.getPkg() == null;
mBroadcastHelper.sendPackageAddedForUser(
mPm.snapshotComputer(), packageName,
pkgSetting, userId, isArchived,
DataLoaderType.NONE,
mPm.mAppPredictionServicePackage);
synchronized (mPm.mLock) {
mPm.updateSequenceNumberLP(
pkgSetting, new int[]{userId});
}installExistingPackageAsUser 只给目标 user 调用 sendPackageAddedForUser,随后更新该 user 的 sequence number。广播发送发生在数据准备后,接收者看到的包状态应已经是 installed;但这不意味着所有用户都收到广播。
源码文件:frameworks/base/services/core/java/com/android/server/pm/BroadcastHelper.java,符号:sendPackageAddedForUser
void sendPackageAddedForUser(
@NonNull Computer snapshot,
String packageName, PackageSetting ps,
int userId, boolean isArchived,
int dataLoaderType,
String appPredictionServicePackage) {
sendPackageBroadcastAndNotify(
Intent.ACTION_PACKAGE_ADDED, packageName,
ps, userId, isArchived, dataLoaderType,
appPredictionServicePackage);
}真正的接收者过滤仍由 sendPackageBroadcastAndNotify、AppsFilter 和用户广播机制完成。包已 installed 不等于任意接收者都能看到它;包可见性和 receiver 权限仍然生效。
8. 更新用户
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:commitPackagesLocked
if (installRequest.isInstallReplace()) {
PackageStateInternal oldState =
mPm.snapshotComputer()
.getPackageStateInternal(packageName);
installRequest
.setScannedPackageSettingFirstInstallTimeFromReplaced(
oldState, allUsers);
installRequest.getRemovedInfo().mBroadcastAllowList =
mPm.mAppsFilter.getVisibilityAllowList(
mPm.snapshotComputer(),
installRequest.getScannedPackageSetting(),
allUsers,
mPm.mSettings.getPackagesLocked());
}
AndroidPackage pkg = commitReconciledScanResultLocked(
reconciledPkg, allUsers);
updateSettingsLI(pkg, allUsers, installRequest);更新安装会记录原包的 per-user 首次安装时间,并构造广播 allowlist。它不能简单地把所有 user 都当成首次安装:已 installed 用户走替换语义,未 installed 用户才可能在提交后进入 new users。
9. 广播差异
广播 action 由安装结果和用户原状态共同决定:
| 情况 | 代码路径 | 用户级结果 |
|---|---|---|
| 新 APK 首次安装到 user | new user in InstallRequest | PACKAGE_ADDED |
| 已安装 user 的 APK 更新 | replace + broadcast allowlist | PACKAGE_REPLACED 等更新广播 |
| 已有 APK 启用到 user | installExistingPackageAsUser | 目标 user PACKAGE_ADDED |
| APK 全局替换但 user 未安装 | 不加入该 user 的 installed 集合 | 不应伪造该 user 的普通 update 广播 |
源码文件:frameworks/base/services/core/java/com/android/server/pm/BroadcastHelper.java,符号:广播分派相关方法
sendPackageBroadcastAndNotify(
Intent.ACTION_PACKAGE_ADDED,
packageName, extras, targetUserId,
finishedReceiver);
sendPackageBroadcastAndNotify(
Intent.ACTION_PACKAGE_REPLACED,
packageName, extras, targetUserId,
finishedReceiver);广播不是全局“一次发送”:目标 user、包可见性 allowlist、receiver 权限和 action 都参与最终分发。排查“某用户没有收到更新广播”时,先检查该 user 是否 installed、allowlist 是否包含 receiver,再看 receiver 自己的 manifest 过滤器。
10. 权限与恢复
installExistingPackageAsUser 在修改 installed 状态后调用 PermissionManagerServiceInternal.onPackageInstalled,并通过 restoreAndPostInstall 处理备份恢复、角色更新和可选 IntentSender 回调。它不会因为 APK 已经存在就跳过用户级权限初始化。
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:restoreAndPostInstall
InstallRequest request = new InstallRequest(
userId, PackageManager.INSTALL_SUCCEEDED,
pkgSetting.getPkg(), new int[]{userId},
() -> mPm.restorePermissionsAndUpdateRolesForNewUserInstall(
packageName, userId),
pkgSetting.getAppId(), callingUid,
pkgSetting.isSystem());
restoreAndPostInstall(request);用户级启用的“安装完成”不止是广播:权限恢复和角色更新在 post-install 回调中执行。调用方拿到成功码时,部分异步恢复工作可能仍在进行。
11. 失败路径
| 失败点 | 返回/行为 |
|---|---|
无 INSTALL_PACKAGES / INSTALL_EXISTING_PACKAGES | SecurityException |
| 跨用户权限不足 | SecurityException |
DISALLOW_INSTALL_APPS | INSTALL_FAILED_USER_RESTRICTED |
| 包不存在或无解析包 | INSTALL_FAILED_INVALID_URI |
| Instant App 受系统/设备管理员保护 | INSTALL_FAILED_INVALID_URI |
| 指定用户不存在 | 完整安装阶段 INTERNAL_ERROR_MISSING_USER |
| 数据目录准备失败 | 安装请求错误,后续安装流程停止 |
安装的全局阶段成功,不代表每个 user 的数据准备、权限恢复和广播都成功。反之,installExistingPackageAsUser 不会因为没有 APK 拷贝而绕过跨用户权限和 user restriction。
12. 阅读检查
读者可以复述两条主线:
- 完整安装:
InstallRequest.userId→getNewUsers→ 全局 commit →queryInstalledUsers→prepareAppDataPostCommitLIF→ per-user 广播。 - 已有包启用:权限/跨用户检查 →
PackageSetting.setInstalled(true, userId)→ restrictions/kernel mapping → PermissionManager → app data →sendPackageAddedForUser→ restore/roles。
然后判断:安装 APK 到 user 0 会自动把 installed=true 写给所有用户吗?更新已安装用户和启用已有包使用完全相同的广播路径吗?数据目录在 setInstalled(true) 的同一行就一定全部创建了吗?正确答案分别是“不一定”“不完全相同”“不一定”。
