Skip to content

多用户包管理

追踪用户创建、包的 per-user 状态、DE 数据准备、持久化和用户删除。

基于android-17.0.0_r1
AndroidPMSMultiUserPackageManager

多用户包管理 ​

本文面向已经了解 PackageSetting 基本结构的读者,重点回答一个问题:同一份 APK 如何在多个 Android 用户之间共享代码、分离状态,并在用户创建和删除时由 PMS 完成哪些动作。应用级状态字段的逐项定义留给 PackageSetting 的 per-user 状态;本文建立它与 UserManagerService、Settings、installd 和查询快照之间的调用链。

多用户包管理有三个容易混淆的层次:全局包记录(包名、代码路径、签名、appId)、每用户包状态(installed、enabled、stopped 等)和每用户数据目录(DE/CE)。APK 通常由所有用户共享,但这不等于所有用户都安装了它;PackageSetting 可以保留全局代码,同时为每个 userId 保存不同状态。

1. 三层模型 ​

层次代表对象典型字段/资源生命周期
全局包PackageSettingpackageName、appId、code path、签名APK 安装/删除
用户状态PackageUserStateImplinstalled、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

java
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

java
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

java
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 分支

java
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

java
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

java
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

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

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

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

java
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

java
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

java
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. 阅读练习 ​

顺着源码复述下面两条路径,可以检验是否真正理解了多用户边界:

  1. 用户创建:UserManagerService 回调 → PackageManagerService.createNewUser → Settings.createNewUserLI → PackageUserState.setInstalled → Installer.Batch 创建 DE → 写 restrictions → AppsFilter.onUserCreated。
  2. 用户删除:cleanUpUser → Settings.removeUserLPw → 每个 PackageSetting.removeUser → 删除 restrictions、权限、跨 profile 状态 → 写 kernel remove-user 通知。

然后回答三个判断题:同一 APK 是否可以只为 user 10 标记未安装?USER_ALL 是否能直接存入 user state?用户删除是否只需要删除 /data/user/<id>?正确答案分别是“可以”“不能,必须展开成真实 userId”“不够,还要清理 PMS、权限、过滤器和 kernel 映射”。