Skip to content

per-user 包状态

解析 Android 17 PackageSetting 与 PackageUserStateImpl 的字段、按需创建、快照通知和查询消费路径。

基于android-17.0.0_r1
AndroidPMSPackageSettingMultiUser

per-user 包状态 ​

本文承接 多用户包管理,只讨论 PackageSetting 中按用户变化的状态。目标不是把字段重新列一遍,而是说明一个状态从哪里创建、如何修改、什么时候对查询生效,以及为什么 PMS 读路径可以在没有显式 user state 时返回默认值。

全局包对象保存代码路径、签名、appId 和包元数据;PackageUserStateImpl 保存某个 userId 对这个包的视图。两个层次不能混用:更新 APK 通常修改全局包,单用户卸载只修改 installed,组件禁用只修改用户状态,应用归档也只在用户维度记录。

1. 状态所有权 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java,符号:mUserStates、readUserState、modifyUserState

java
private final SparseArray<PackageUserStateImpl> mUserStates
        = new SparseArray<>();

@NonNull
public PackageUserStateInternal readUserState(int userId) {
    PackageUserStateImpl state = mUserStates.get(userId);
    return state != null
            ? state : PackageUserStateInternal.DEFAULT;
}

@VisibleForTesting
PackageUserStateImpl modifyUserState(int userId) {
    PackageUserStateImpl state = mUserStates.get(userId);
    if (state == null) {
        state = new PackageUserStateImpl(this);
        mUserStates.put(userId, state);
    }
    return state;
}

SparseArray 让 user state 稀疏存储:读一个从未显式修改过的用户不会创建对象,写入时才创建。readUserState 暴露只读接口;modifyUserState 才返回可变实现。这个分离保证查询代码无法通过类型误用修改包状态。

2. 字段布局 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java,符号:Booleans 与字段声明

java
private static class Booleans {
    private static final int INSTALLED = 1;
    private static final int STOPPED = 1 << 1;
    private static final int NOT_LAUNCHED = 1 << 2;
    private static final int HIDDEN = 1 << 3;
    private static final int INSTANT_APP = 1 << 4;
    private static final int VIRTUAL_PRELOADED = 1 << 5;
    private static final int APP_LOCK_ENABLED = 1 << 6;
}
private int mBooleans;

private long mCeDataInode;
private long mDeDataInode;
private int mEnabledState;
private int mInstallReason;
private int mUninstallReason;
private WatchedArrayMap<UserPackage, SuspendParams> mSuspendParams;
private ArchiveState mArchiveState;

Android 17 使用一个 bitset 存储多个布尔状态,另外字段保存启用状态、安装/卸载原因、数据目录 inode、挂起参数和归档状态。布尔位压缩只是内存布局优化,不改变 per-user 状态的语义。

状态域代表字段主要消费者
安装与可见性installed、hidden、instantApp、archive包查询、组件解析、归档
运行控制stopped、notLaunched、suspendParams启动、广播、设备策略
组件覆盖enabled/disabled componentsPackageUserStateUtils
数据关联CE/DE/PCC inodeAppDataHelper、installd
策略来源installReason、uninstallReason安装/用户类型诊断
用户定制overlay、aspect ratio、splash theme、app lock资源和窗口策略

3. 位图与状态变更 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java,符号:setBoolean、getBoolean

java
private void setBoolean(int flag, boolean value) {
    if (value) {
        mBooleans |= flag;
    } else {
        mBooleans &= ~flag;
    }
}

private boolean getBoolean(int flag) {
    return (mBooleans & flag) != 0;
}

对 installed、stopped、hidden 等字段的写入不会替换整个对象,只改变对应 bit。读取接口再把 bit 转换成语义方法,如 isInstalled()、isStopped()、isHidden()。

源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java,符号:setter

java
public PackageUserStateImpl setInstalled(boolean value) {
    setBoolean(Booleans.INSTALLED, value);
    onChanged();
    return this;
}

public PackageUserStateImpl setEnabledState(int value) {
    mEnabledState = value;
    onChanged();
    return this;
}

public PackageUserStateImpl setInstallReason(int value) {
    mInstallReason = value;
    AnnotationValidations.validate(
            PackageManager.InstallReason.class,
            null, mInstallReason);
    onChanged();
    return this;
}

setter 统一做三件事:更新值、校验枚举(适用时)、调用 onChanged()。调用者不应直接修改字段或集合,否则快照缓存和父级 PackageSetting 可能不知道状态已经变化。

4. Watchable 通知 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java,符号:onChanged

java
private void onChanged() {
    if (mWatchable != null) {
        mWatchable.dispatchChange(mWatchable);
    }
    dispatchChange(this);
}

mWatchable 通常指向父级 PackageSetting;状态变化先向父级传播,再通知自身观察者。PMS 的 settings、snapshot cache 或序列化调度可以通过这条链发现 user state 变更。setInstalled() 这种看似单字段写入,实际可能触发包设置层的更大范围失效。

5. 默认值与按需创建 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java,符号:构造函数

java
public PackageUserStateImpl(
        @NonNull Watchable watchable) {
    mWatchable = watchable;
    mSnapshot = makeCache();
    setBoolean(Booleans.INSTALLED, true);
}

public PackageUserStateImpl() {
    super();
    mWatchable = null;
    mSnapshot = makeCache();
    setBoolean(Booleans.INSTALLED, true);
}

新建的具体 user state 默认 installed=true,但这只适用于确实创建了对象的场景。没有对象时 PackageSetting.readUserState() 返回接口默认值;用户创建、单用户卸载、归档等路径会根据需要显式创建并修改状态。不能从“mUserStates 没有条目”直接推导出所有字段都已持久化为 true。

6. 快照缓存 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java,符号:makeCache、copy constructor、snapshot

java
private SnapshotCache<PackageUserStateImpl> makeCache() {
    return new SnapshotCache<PackageUserStateImpl>(this, this) {
        @Override
        public PackageUserStateImpl createSnapshot() {
            return new PackageUserStateImpl(
                    mWatchable, mSource);
        }
    };
}

@Override
public PackageUserStateImpl snapshot() {
    return mSnapshot.snapshot();
}

缓存第一次被读取时创建副本,后续读者复用同一个快照,直到源对象变更导致缓存失效。copy constructor 对 watched 集合调用 snapshot(),对字符串、枚举和不可变对象直接复制引用;快照自身使用 SnapshotCache.Sealed,不会再次形成可变快照链。

7. 批量写入 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java,符号:setUserState

java
void setUserState(int userId,
        long ceDataInode, long deDataInode,
        long pccCeDataInode, long pccDeDataInode,
        int enabled, boolean installed,
        boolean stopped, boolean notLaunched,
        boolean hidden, int distractionFlags,
        ArrayMap<UserPackage, SuspendParams> suspendParams,
        boolean instantApp, boolean virtualPreload,
        String lastDisableAppCaller,
        ArraySet<String> enabledComponents,
        ArraySet<String> disabledComponents,
        int installReason, int uninstallReason,
        String harmfulAppWarning, String splashScreenTheme,
        long firstInstallTime, int aspectRatio,
        ArchiveState archiveState, boolean appLockEnabled,
        int virtualGamepadUserOption,
        int personalContextMode) {
    modifyUserState(userId)
            .setSuspendParams(suspendParams)
            .setCeDataInode(ceDataInode)
            .setDeDataInode(deDataInode)
            .setPccCeDataInode(pccCeDataInode)
            .setPccDeDataInode(pccDeDataInode)
            .setEnabledState(enabled)
            .setInstalled(installed)
            .setStopped(stopped)
            .setNotLaunched(notLaunched)
            .setHidden(hidden)
            .setDistractionFlags(distractionFlags)
            .setInstallReason(installReason)
            .setUninstallReason(uninstallReason)
            .setInstantApp(instantApp)
            .setVirtualPreload(virtualPreload)
            .setArchiveState(archiveState)
            .setAppLockEnabled(appLockEnabled);
    onChanged();
}

XML 读取或包状态迁移时,PackageSetting 用一个批量方法恢复整组字段。链式 setter 会多次触发 onChanged(),最后 PackageSetting 再统一通知;这也是为什么持久化加载必须在受控的 settings 读取阶段执行,不能在普通查询线程中随意调用。

8. 查询消费者 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java,符号:典型 installed 查询

java
if (packageState.getUserStateOrDefault(userId)
        .isInstalled() == installed) {
    // package state matches the requested user view
}

PMS 查询不会直接访问 PackageUserStateImpl 的字段,而是从 PackageStateInternal 取得指定 user 的只读状态。Intent 解析、应用信息生成、安装包列表、归档和组件匹配都依赖同一 user-specific view;因此一个 installed=false 会同时影响多个 API 的可见性,而不会删除全局 PackageSetting。

源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java,符号:isSuspended、isQuarantined

java
@Override
public boolean isQuarantined() {
    if (!isSuspended()) {
        return false;
    }
    for (int i = 0; i < mSuspendParams.size(); i++) {
        SuspendParams params = mSuspendParams.valueAt(i);
        if (params.isQuarantined()) {
            return true;
        }
    }
    return false;
}

挂起不是一个单独 boolean 的简单开关:多个 suspending package 可以各自写入 SuspendParams,isSuspended() 和 isQuarantined() 再从集合推导最终视图。读者调试“应用仍 suspended”时,需要查看所有挂起来源,而不只是寻找一个 mSuspended=true 字段。

9. 持久化与恢复 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java,符号:readPackageRestrictionsLPr、writePackageRestrictions

java
private ResilientAtomicFile getUserPackagesStateFile(
        int userId) {
    File mainFile = new File(
            getUserSystemDirectory(userId),
            "package-restrictions.xml");
    File backup = new File(
            getUserSystemDirectory(userId),
            "package-restrictions-backup.xml");
    File reserve = new File(
            getUserSystemDirectory(userId),
            "package-restrictions.xml.reservecopy");
    return new ResilientAtomicFile(mainFile, backup, reserve);
}

用户状态按 userId 保存到独立 restrictions 文件,原子文件提供崩溃恢复。文件只承载 per-user 包状态和相关用户级设置;APK、签名和全局 code path 不会为每个用户复制一份。

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java,符号:removeUser

java
void removeUser(int userId) {
    mUserStates.delete(userId);
    onChanged();
}

public int[] getNotInstalledUserIds() {
    IntArray result = new IntArray(mUserStates.size());
    for (int i = 0; i < mUserStates.size(); i++) {
        if (!mUserStates.valueAt(i).isInstalled()) {
            result.add(mUserStates.keyAt(i));
        }
    }
    return result.toArray();
}

删除用户时只从 PackageSetting 的稀疏数组移除该 user;其他用户状态和全局包仍保留。getNotInstalledUserIds() 也只扫描已创建的 state,不能替代 UMS 的真实用户列表。

10. 状态组合 ​

组合解释
installed=false、数据 inode 为 0对该用户未安装,也没有可见应用数据
installed=false、inode 非 0包被卸载但保留了用户数据,后续可恢复或清理
installed=true、stopped=true已安装但被停止,启动/广播路径仍需检查 stopped
enabled=DISABLED_USER、installed=true包存在但用户级禁用,不等于卸载
suspendParams 非空至少一个来源挂起,可能还处于 quarantine
archiveState 非空用户级归档状态,不能直接等同于全局 APK 删除

这些组合解释了为什么只查看 installed 不足以判断应用是否可启动。组件解析还要结合 enabled/disabled components、suspended、hidden、用户 profile 和查询 flags。

11. 修改路径与消费者 ​

以单用户设置 enabled 为例,写路径大致是:

text
Binder API(userId)
  -> 跨用户权限检查
  -> PackageSetting.setEnabled(userId)
  -> modifyUserState(userId)
  -> PackageUserStateImpl.setEnabledState
  -> onChanged / snapshot invalidation
  -> 写 package-restrictions.xml
  -> 后续查询重新读取该 user state

这个路径的生效时机是“内存状态立即变化,XML 持久化异步或按需完成”。进程重启后,Settings.readPackageRestrictionsLPr 从文件恢复状态;若写入尚未完成,原子文件的恢复副本提供最后一致状态。

12. 失败定位 ​

现象应检查
user 0 能看到包,user 10 看不到readUserState(10).isInstalled()、用户查询 flags
禁用组件重启后恢复modifyUserState 是否调用、restrictions 是否写入
suspended 状态无法解除mSuspendParams 中是否仍有其他 suspending package
查询读到旧状态是否使用旧 Computer/snapshot,onChanged 是否传播
删除用户后包仍出现在设置Settings.removeUserLPw、AppsFilter 和 package list 是否完成

13. 阅读检查 ​

读者可以顺着源码回答:

  1. 为什么 readUserState 不创建对象,而 modifyUserState 会创建对象?
  2. 为什么 installed=false 不等于全局 APK 已删除?
  3. 为什么 suspended 状态要遍历 SuspendParams?
  4. 为什么 setter 必须调用 onChanged(),而查询只拿 PackageUserStateInternal?

如果能从 PackageSetting.readUserState 走到 PackageUserStateImpl.snapshot,再走到具体 setter、快照失效和查询消费者,就已经掌握了 Android 多用户包状态的核心数据流。