多用户包管理
本文面向已经了解 PackageSetting 基本结构的读者,重点回答一个问题:同一份 APK 如何在多个 Android 用户之间共享代码、分离状态,并在用户创建和删除时由 PMS 完成哪些动作。应用级状态字段的逐项定义留给 PackageSetting 的 per-user 状态;本文建立它与 UserManagerService、Settings、installd 和查询快照之间的调用链。
多用户包管理有三个容易混淆的层次:全局包记录(包名、代码路径、签名、appId)、每用户包状态(installed、enabled、stopped 等)和每用户数据目录(DE/CE)。APK 通常由所有用户共享,但这不等于所有用户都安装了它;PackageSetting 可以保留全局代码,同时为每个 userId 保存不同状态。
1. 三层模型
| 层次 | 代表对象 | 典型字段/资源 | 生命周期 |
|---|---|---|---|
| 全局包 | PackageSetting | packageName、appId、code path、签名 | APK 安装/删除 |
| 用户状态 | PackageUserStateImpl | installed、enabled、stopped、installReason | 用户创建/卸载/设置变更 |
| 用户数据 | installd 管理的 DE/CE 目录 | data inode、应用文件、code cache | 用户创建/解锁/删除 |
同一包的 user 0 状态为 installed、user 10 状态为 uninstalled 时,代码仍可能存在于 /data/app,但 user 10 不会出现在“已安装包”查询结果,也不会加入该用户的存储映射。状态与数据是按用户隔离的,代码路径却通常是全局的。
2. UID 映射
源码文件:frameworks/base/core/java/android/os/UserHandle.java,符号:PER_USER_RANGE、getUserId、getUid
public static final int PER_USER_RANGE = 100000;
public static @UserIdInt int getUserId(int uid) {
if (MU_ENABLED) {
return uid / PER_USER_RANGE;
} else {
return UserHandle.USER_SYSTEM;
}
}
public static int getUid(@UserIdInt int userId,
@AppIdInt int appId) {
if (MU_ENABLED && appId >= 0) {
return userId * PER_USER_RANGE
+ (appId % PER_USER_RANGE);
} else {
return appId;
}
}PMS 保存的是 appId 和 userId,进程启动或目录创建时再组合成 Linux UID。user 10 的同一 appId 会得到不同 UID,因此“同一个包名”不能推断“同一个进程身份”。
3. 用户创建入口
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java,符号:createNewUser
void createNewUser(int userId,
@Nullable Set<String> userTypeInstallablePackages,
String[] disallowedPackages) {
try (PackageManagerTracedLock installLock =
mInstallLock.acquireLock()) {
mSettings.createNewUserLI(this, mInstaller, userId,
userTypeInstallablePackages,
disallowedPackages);
}
synchronized (mLock) {
scheduleWritePackageRestrictions(userId);
scheduleWritePackageListLocked(userId);
mAppsFilter.onUserCreated(
snapshotComputer(), userId);
}
}PMS 先在 install lock 下让 Settings 计算每个包的初始状态并准备数据创建批次,释放锁后再写 package restrictions、package list 和通知 AppsFilter。这段顺序避免了长时间持有 PMS 主锁执行 installd I/O,同时保证新用户的包状态先进入内存模型。
4. 初始安装策略
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java,符号:createNewUserLI
final boolean skipPackageAllowList =
userTypeInstallablePackages == null;
final long currentTimeMillis =
System.currentTimeMillis();
synchronized (mLock) {
for (int i = 0; i < mPackages.size(); i++) {
PackageSetting ps = mPackages.valueAt(i);
if (ps.getPkg() == null) {
ps.setInstalled(false, userHandle);
writeKernelMappingLPr(ps);
continue;
}
boolean shouldMaybeInstall = ps.isSystem()
&& !ArrayUtils.contains(disallowedPackages,
ps.getPackageName())
&& !ps.getPkgState().isHiddenUntilInstalled();
boolean shouldReallyInstall = shouldMaybeInstall
&& (skipPackageAllowList
|| userTypeInstallablePackages.contains(
ps.getPackageName()));
ps.setInstalled(shouldReallyInstall, userHandle);
if (shouldReallyInstall) {
ps.setFirstInstallTime(currentTimeMillis,
userHandle);
}
}
}新用户默认只初始安装满足条件的 system package;disallowedPackages 优先于 user type allowlist。ps.getPkg()==null 的记录也会显式写入 installed=false,避免无代码包错误地进入新用户映射。
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java,符号:stopped/uninstall reason 分支
boolean shouldBeStopped =
service.mShouldStopSystemPackagesByDefault
&& ps.isSystem()
&& !ps.isApex()
&& !service.mInitialNonStoppedSystemPackages
.contains(ps.getPackageName());
ps.setStopped(shouldBeStopped, userHandle);
int uninstallReason = (shouldMaybeInstall
&& !shouldReallyInstall)
? UNINSTALL_REASON_USER_TYPE
: UNINSTALL_REASON_UNKNOWN;
ps.setUninstallReason(uninstallReason, userHandle);installed=false 并不覆盖所有原因。若系统包因用户类型 allowlist 没被选中,PMS 将 per-user uninstallReason 记为 USER_TYPE;系统还可以按配置把一部分非 APEX system package 标为 stopped。后续查询、启动和卸载逻辑会消费这些状态。
5. DE 数据准备
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java,符号:createNewUserLI 的 CreateAppDataArgs
if (shouldReallyInstall) {
if (ps.getAppId() < 0) {
// APEX 等无效 appId 不创建应用数据目录。
continue;
}
final String seInfo = ps.getSeInfo();
final boolean usesSdk = !ps.getPkg()
.getUsesSdkLibraries().isEmpty();
final CreateAppDataArgs args =
Installer.buildCreateAppDataArgs(
ps.getVolumeUuid(), ps.getPackageName(),
userHandle, StorageManager.FLAG_STORAGE_DE,
ps.getAppId(), seInfo,
ps.getPkg().getTargetSdkVersion(),
usesSdk, pccId);
batch.createAppData(args);
}用户创建阶段只创建 DE 数据,因为 CE 存储在用户解锁后才可用。Settings 在 PMS 锁内累积参数,之后统一 batch.execute(installer);真正的目录、inode、SELinux 上下文处理由 installd 完成。
6. 持久化边界
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java,符号:getUserPackagesStateFile、writePackageRestrictions
private ResilientAtomicFile getUserPackagesStateFile(
int userId) {
File mainFile = new File(
getUserSystemDirectory(userId),
"package-restrictions.xml");
File temporaryBackup = new File(
getUserSystemDirectory(userId),
"package-restrictions-backup.xml");
File reserveCopy = new File(
getUserSystemDirectory(userId),
"package-restrictions.xml.reservecopy");
return new ResilientAtomicFile(
mainFile, temporaryBackup, reserveCopy);
}每个 user 有独立的 package-restrictions.xml。它保存 per-user 安装、启用、暂停、组件覆盖、首装时间等状态;全局 APK 路径和签名仍在共享的 package settings 中。ResilientAtomicFile 通过备份和 reserve copy 保证写入中断后仍有恢复来源。
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java,符号:resolveUserIds
@NonNull
int[] resolveUserIds(@CanBeALL @UserIdInt int userId) {
return (userId == USER_ALL)
? mUserManager.getUserIds()
: new int[]{userId};
}USER_ALL 不是一个真实用户状态,它只在 API 入口展开为当前 UserManagerService 返回的 userId 数组。清数据、创建目录和权限操作如果支持 USER_ALL,都必须先经过这个展开,不能把 -1 写进 PackageUserState。
7. 快照与查询
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java,符号:readUserState、modifyUserState、removeUser
@VisibleForTesting
PackageUserStateImpl modifyUserState(int userId) {
PackageUserStateImpl state = mUserStates.get(userId);
if (state == null) {
state = new PackageUserStateImpl(this);
mUserStates.put(userId, state);
}
return state;
}
public PackageUserStateInternal readUserState(int userId) {
PackageUserStateImpl state = mUserStates.get(userId);
return state != null ? state : PackageUserStateInternal.DEFAULT;
}
void removeUser(int userId) {
mUserStates.delete(userId);
onChanged();
}读路径对缺失 user state 返回不可变默认值;写路径才创建新的 PackageUserStateImpl。因此“没有显式记录”与 installed=true 或其他默认语义由 DEFAULT 决定,不能简单等同于用户不存在。
源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java,符号:snapshot、状态 setters
@Override
public PackageUserStateImpl snapshot() {
return mSnapshot.snapshot();
}
public PackageUserStateImpl setInstalled(boolean value) {
setBoolean(Booleans.INSTALLED, value);
onChanged();
return this;
}
public PackageUserStateImpl setEnabledState(int value) {
mEnabledState = value;
onChanged();
return this;
}PMS 的查询通常通过 Computer/filtered snapshot 读取 user state;写入通过 modifyUserState 并触发 watchable change。快照把并发读与安装锁内的可变对象分开,查询结果不会直接持有正在修改的状态对象。
8. 用户删除
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java,符号:cleanUpUser
void cleanUpUser(UserManagerService userManager,
@UserIdInt int userId) {
synchronized (mLock) {
synchronized (mDirtyUsers) {
mDirtyUsers.remove(userId);
}
mUserNeedsBadging.delete(userId);
mDeletePackageHelper.removeUnusedPackagesLPw(
userManager, userId);
mSettings.removeUserLPw(userId);
mPendingBroadcasts.remove(userId);
mAppsFilter.onUserDeleted(
snapshotComputer(), userId);
mPermissionManager.onUserRemoved(userId);
mInstallerService.onUserRemoved(userId);
}
mInstantAppRegistry.onUserRemoved(userId);
mPackageMonitorCallbackHelper.onUserRemoved(userId);
}用户删除不是只删目录。PMS 还要清理 per-user package state、未使用包、广播队列、AppsFilter、权限、PackageInstaller 和 instant app registry。Settings.removeUserLPw 负责从每个 PackageSetting 中删除 user state,并删除该用户的 package restrictions 文件。
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java,符号:removeUserLPw
void removeUserLPw(int userId) {
for (Entry<String, PackageSetting> entry
: mPackages.entrySet()) {
entry.getValue().removeUser(userId);
}
mPreferredActivities.remove(userId);
synchronized (mPackageRestrictionsLock) {
getUserPackagesStateFile(userId).delete();
mPendingAsyncPackageRestrictionsWrites.delete(userId);
}
removeCrossProfileIntentFiltersLPw(userId);
mRuntimePermissionsPersistence.onUserRemoved(userId);
mDomainVerificationManager.clearUser(userId);
writePackageListLPr();
writeKernelRemoveUserLPr(userId);
}最后的 kernel remove-user 文件通知存储映射层该 user 已删除;否则即使 Java 状态清掉,底层仍可能保留旧的 package-to-uid 映射。跨 profile intent、运行时权限和 domain verification 也必须按 user 清理。
9. 权限与跨用户入口
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java,符号:典型 Binder 方法中的 enforceCrossUserPermission
final int callingUid = Binder.getCallingUid();
snapshot.enforceCrossUserPermission(
callingUid, userId,
true /* requireFullPermission */,
false /* checkShell */,
"clear application data");任何带 userId 的公开 PMS 操作都必须先判断调用者能否访问目标用户。userId 参数本身不是授权凭证;调用者是当前 user、系统、root、shell 还是拥有跨用户权限,决定了能否读取或修改另一个用户的状态。跨用户检查通常在 snapshot 上执行,以便权限判断与包状态读取使用一致视图。
10. 失败与恢复
| 阶段 | 失败 | 结果 |
|---|---|---|
| 创建用户 | allowlist/denylist 排除包 | installed=false,记录 USER_TYPE uninstall reason |
| 创建数据 | installd 批次失败 | 记录 warning,后续 reconcile 可重试 |
| 持久化 | restrictions 写入中断 | atomic backup/reserve copy 保留恢复来源 |
| 查询 | user state 缺失 | 返回 PackageUserStateInternal.DEFAULT |
| 用户删除 | 包状态清理后仍有底层映射 | writeKernelRemoveUserLPr 通知存储映射层 |
| 跨用户调用 | 权限不足 | enforceCrossUserPermission 抛出安全异常 |
用户创建与删除都不是单事务。内存状态、XML、installd、AppsFilter、权限和 kernel 映射由不同 owner 管理,所以源码会把失败记录、重新 reconcile 和幂等删除分散在多个组件中。分析异常时应先定位是“per-user 状态没有写入”“DE 目录没有创建”还是“跨用户权限拒绝”,不要统称为安装失败。
11. 阅读练习
顺着源码复述下面两条路径,可以检验是否真正理解了多用户边界:
- 用户创建:
UserManagerService回调 →PackageManagerService.createNewUser→Settings.createNewUserLI→PackageUserState.setInstalled→Installer.Batch创建 DE → 写 restrictions →AppsFilter.onUserCreated。 - 用户删除:
cleanUpUser→Settings.removeUserLPw→ 每个PackageSetting.removeUser→ 删除 restrictions、权限、跨 profile 状态 → 写 kernel remove-user 通知。
然后回答三个判断题:同一 APK 是否可以只为 user 10 标记未安装?USER_ALL 是否能直接存入 user state?用户删除是否只需要删除 /data/user/<id>?正确答案分别是“可以”“不能,必须展开成真实 userId”“不够,还要清理 PMS、权限、过滤器和 kernel 映射”。
