组件启用状态变更
本文面向已经读过 PackageStateMutator 事务提交、PackageSetting 数据结构 和 ComponentResolver 组件解析器 的读者。它只讨论 setComponentEnabledSetting/setComponentEnabledSettings 如何修改 per-user enabled state,以及状态变化怎样影响解析和广播;不重复讲 pm 命令语法,也不把 package enabled state 和 stopped state 混为一谈。
1. 入口与对象
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java。
Binder 层有单项和批量两个入口,最终都汇入私有的 setEnabledSettings:
public void setComponentEnabledSetting(ComponentName componentName,
int newState, int flags, int userId, String callingPackage) {
if (!mUserManager.exists(userId)) return;
if (callingPackage == null) {
callingPackage = Integer.toString(Binder.getCallingUid());
}
setEnabledSettings(List.of(new ComponentEnabledSetting(
componentName, newState, flags)), userId, callingPackage);
}输入对象 ComponentEnabledSetting 可以代表整个 package,也可以代表一个具体 component。目标状态最终写入 PackageSetting 的用户状态:package 级别写 mEnabledState,component 级别写 enabled/disabled component 集合。
2. 输入校验
源码符号:setEnabledSettings。
首先只接受五种状态:DEFAULT、ENABLED、DISABLED、DISABLED_USER 和 DISABLED_UNTIL_USED。批量请求还禁止重复 package、重复 component,以及同一 package 内 DONT_KILL_APP 标志冲突。
if (!(newState == COMPONENT_ENABLED_STATE_DEFAULT
|| newState == COMPONENT_ENABLED_STATE_ENABLED
|| newState == COMPONENT_ENABLED_STATE_DISABLED
|| newState == COMPONENT_ENABLED_STATE_DISABLED_USER
|| newState == COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED)) {
throw new IllegalArgumentException(
"Invalid new component state: " + newState);
}重复和 flag 冲突在任何状态写入前抛出异常,因此批量列表不会出现“前几个已经改了,后几个才失败”的部分提交。
3. 调用者与目标
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:setEnabledSettings。
入口先用 enforceCrossUserPermission 校验 user,再在 mLock 内建立 snapshot 检查每个目标包。调用者是目标 app 自身时可以修改自己的组件;修改其他包则必须持有 CHANGE_COMPONENT_ENABLED_STATE。
boolean isCallerTargetApp = ArrayUtils.contains(
snapshot.getPackagesForUid(callingUid), packageName);
if (!isCallerTargetApp && !allowedByPermission) {
throw new SecurityException("Attempt to change component state");
}
if (pkgSetting == null || snapshot.shouldFilterApplicationIncludingUninstalled(
pkgSetting, callingUid, userId)) {
throw new IllegalArgumentException("Unknown package/component");
}可见性检查发生在 protected package 检查之前。对调用者不可见的包会得到 unknown,而不会暴露它是否存在、是否是 device owner 或是否受保护。
4. 受保护包
非目标 app 修改 package state 时,PMS 调用 mProtectedPackages.isPackageStateProtected(userId, packageName)。命中 device/profile owner、DPC、provisioning 或 supervision 保护时抛 SecurityException。启用 protectSystemRequiredPackages() 后,required system package 的 package 级禁用还要额外检查 ALLOW_CONTROL_SYSTEM_REQUIRED_PACKAGES。
组件级请求另有一个系统生成的 app details activity 限制:没有 CHANGE_COMPONENT_ENABLED_STATE 时不能禁用 APP_DETAILS_ACTIVITY_CLASS_NAME。这和 protected package 是两条独立规则。
5. 组件存在性
每个 component 请求都会通过 AndroidPackageUtils.hasComponentClassName 验证 class name。targetSdk >= Jelly Bean 的包,类不存在会抛 IllegalArgumentException;旧 targetSdk 则只记录 warning,并把对应 updateAllowed 设为 false,让批量请求继续处理其他合法项。
这种兼容分支意味着“方法返回成功”不一定表示列表中的每个旧 target component 都改变了状态。调试批量请求时要同时看异常、warning 和 updateAllowed。
6. 状态写入
源码符号:setEnabledSettingInternalLocked。
if (!setting.isComponent()) {
pkgSetting.setEnabled(newState, userId, callingPackage);
success = true;
} else {
switch (newState) {
case COMPONENT_ENABLED_STATE_ENABLED:
success = pkgSetting.enableComponentLPw(className, userId);
break;
case COMPONENT_ENABLED_STATE_DISABLED:
success = pkgSetting.disableComponentLPw(className, userId);
break;
case COMPONENT_ENABLED_STATE_DEFAULT:
success = pkgSetting.restoreComponentLPw(className, userId);
break;
}
}package 级 DISABLED_UNTIL_USED 等状态由 setEnabled 保存;component 级只接受 enabled/disabled/default,DISABLED_USER 不会进入 component switch。写入后调用 updateSequenceNumberLP,让 resolver 和查询 snapshot 知道该 package/user 的状态发生变化。
7. 进程与广播
每项设置的 flags 会影响两个独立行为:是否杀进程,以及 PACKAGE_CHANGED 广播何时发送。没有 DONT_KILL_APP 的项加入 sendNowBroadcasts,退出锁后立即发送;带 DONT_KILL_APP 的项进入 mPendingBroadcasts,由 handler 延迟批量发送。
批量请求中同一 package 的多个 component 若混用 DONT_KILL_APP 会在前置校验阶段拒绝,避免一部分组件立即广播、另一部分组件延迟广播。
8. 持久化时机
如果任何 setting 带 SYNCHRONOUS,PMS 在 mLock 内调用 flushPackageRestrictionsAsUserInternalLocked;否则调用 scheduleWritePackageRestrictions(userId)。同步 flag 只控制 restrictions 文件写盘,不改变组件状态的内存提交顺序。
退出锁后,PMS 生成新的 snapshot 给 mBroadcastHelper.sendPackageChangedBroadcast;metrics 则使用写入前的旧 snapshot 计算 old state。这是有意的观察点:指标要记录“从什么状态变到什么状态”,广播消费者要看到新状态。
9. 特殊副作用
package 级禁用 SUSPEND_APPS 权限持有者时,PMS 会解除该包作为 suspender 造成的挂起,并移除 distracting package restrictions,避免禁用一个管理包后其他应用永久 suspended。
启用 system stub package 时,PMS 还可能先解压压缩 APK 并替换 system 分区版本;解压失败只把该项 updateAllowed 置为 false,不会写入 enabled state。清除 app data 时,如果 manifest 的 resetEnabledSettingsOnAppDataCleared 为 true,PMS 会恢复 Activity、Receiver、Service、Provider 的 component overrides 到 default。
10. 查询消费者
组件解析器读取 PackageUserState 的 enabled state,再结合 manifest component 的默认 enabled 属性决定是否产生 ResolveInfo。COMPONENT_ENABLED_STATE_DEFAULT 并不等于强制启用,而是回退到 manifest;MATCH_DISABLED_COMPONENTS 等查询 flag 可以改变结果过滤,但不会修改持久状态。
因此排查“组件已 enable 但仍解析不到”时,应分开检查:写入是否成功、目标 user 是否正确、resolver 使用的 flags、组件自身 manifest enabled,以及 pending broadcast 是否已经发送。广播延迟不会改变 resolver 已提交的内存状态,但会影响依赖 PACKAGE_CHANGED 刷新的外部缓存。
11. 失败定位
| 现象 | 检查源码 | 解释 |
|---|---|---|
| 抛 Invalid new component state | setEnabledSettings 状态白名单 | 输入值不是五种公开状态之一 |
| Unknown component | visibility、user、class name 校验 | 可能不可见、未安装或类名不存在 |
| protected package SecurityException | ProtectedPackages 与 required system 检查 | system/privileged 不自动绕过保护 |
| 批量请求部分无效 | updateAllowed、旧 targetSdk 分支 | warning 路径会跳过单项而不抛异常 |
| 进程未重启 | DONT_KILL_APP flags | 该 flag 只延迟广播并保留进程 |
| 状态已变但外部缓存未刷新 | pending broadcast handler | 内存提交、restrictions 写盘、广播是三个时点 |
| 清数据后组件恢复默认 | resetComponentEnabledSettingsIfNeededLPw | manifest flag 明确要求清理 enabled overrides |
12. 源码练习
- 从单项 Binder 入口追到
setEnabledSettingInternalLocked,标出 user 权限、可见性、protected、组件存在性和真正 setter 的顺序。 - 构造同一 package 两个 component 一个带
DONT_KILL_APP、一个不带的批量请求,指出异常发生在写入前的哪一步。 - 对比 package 级
DEFAULT和 component 级DEFAULT,说明它们如何分别回退到 package/manifest 的默认 enabled 语义。 - 给定 resolver 已看到新状态但第三方缓存仍旧,沿
sendNowBroadcasts、mPendingBroadcasts和 package restrictions 写盘三个时间点定位原因。
