banner
约 5,700 字
19 分钟

HyperOS 的 Greezer 与 AOSP Freezer

摘要

一次针对 HyperOS 后台冻结机制的完整排查。从开发者选项中的 Cached Apps Freezer 开关出发,追踪到 PowerMillet、Greezer、Aurogon、SmartPower,以及最终落在 Linux cgroup v2 cgroup.freeze 上的真实冻结链路。

为什么 HyperOS 的“暂停执行已缓存的应用”总会变回关闭:从 AOSP Freezer 到 Greezer 的一次完整追踪

本文记录一次针对 HyperOS 3.x / Android 16 的实际排障。目标不是证明所有 HyperOS 设备都采用完全相同的实现,而是把一个具体样本上的行为从设置层、Framework、厂商服务一路追到 Linux cgroup v2 的真实冻结状态。

为降低个人信息泄露风险,本文不公开设备序列号、终端用户名、精确设备型号、具体第三方应用包名、PID/UID 列表和细粒度使用时间线。保留属性名、类名、函数名、系统路径和必要命令,因为它们是复现技术结论所必需的。

事情起因很简单。

HyperOS 的开发者选项里有一个 Android 原生选项:“暂停执行已缓存的应用”。在 AOSP 语境下,它对应 Cached Apps Freezer,用来冻结满足条件的 cached process,减少后台 CPU 执行和部分资源消耗。

问题是:把它手动切到“已开启”,确认、重启,回来以后它又变成“已停用”。

一开始这很像一个普通的设置持久化问题。实际追下去,却发现它牵出了一整套 Xiaomi 自己的后台冻结体系:PowerMillet、Greezer、Millet monitor、Aurogon、SmartPower,以及最终落在 Linux cgroup v2 cgroup.freeze 上的真实冻结操作。

最后得到的结论可以先放在这里:

这台样本设备并不是“不支持进程冻结”。相反,它一直在冻结后台进程。只是 HyperOS 正常模式下选择由 Xiaomi 的 Greezer/PowerMillet 负责,而不是让 AOSP Cached Apps Freezer 成为主冻结器。

更关键的是,这不是偶然冲突,而是 Framework 里明确写出来的互斥逻辑。


表面现象其实包含两个问题

最开始看到的是:

bash
adb shell settings get global cached_apps_freezer

重启后得到:

纯文本
disabled

手工改成:

bash
adb shell settings put global cached_apps_freezer enabled

立即读取可以得到:

纯文本
enabled

所以 SettingsProvider 并不是完全不让写。

与此同时,还可以把 AOSP 的 DeviceConfig 开关设置为 true:

bash
adb shell device_config put activity_manager_native_boot use_freezer true

但运行态仍然是:

纯文本
ActivityManager: Freezer disabled
use_freezer=false
Apps frozen: 0

而且这个现象甚至不需要等重启:在系统已经运行的情况下,把 cached_apps_freezer 切为 enabled,Framework observer 会立即响应,但结果依然是 Freezer disabled

这实际上说明有两个不同的问题:

  • 为什么运行中已经写成 enabled,AOSP freezer 仍然不启用?

  • 为什么重启以后,cached_apps_freezer 还会被主动写回 disabled

后来证明,这两个问题分别对应两段不同的 HyperOS 代码。


先回到 AOSP:正常情况下 freezer 是怎么决定是否启用的

现代 Android 的 Cached Apps Freezer 主要由 CachedAppOptimizer 管理。

简化以后,AOSP 的判断大致是:

Java
if ("disabled".equals(configOverride)) {
    useFreezer = false;
} else if ("enabled".equals(configOverride)
        || DeviceConfig.getBoolean(
                "activity_manager_native_boot",
                "use_freezer",
                true)) {
    useFreezer = mFreezer.isFreezerSupported();
} else {
    useFreezer = false;
}

也就是说:

  • Global Settings 显式 disabled:关闭;

  • 显式 enabled:尝试启用;

  • 没有显式覆盖时,DeviceConfig use_freezer=true:尝试启用;

  • 真正启用前,还要通过 Freezer.isFreezerSupported() 的能力检测。

Android 16 上的 Freezer.java 已经非常薄,Java 层主要把调用转给 native:

Java
public int freezeBinder(int pid, boolean freeze, int timeoutMs) {
    return nativeFreezeBinder(pid, freeze, timeoutMs);
}

public int getBinderFreezeInfo(int pid) {
    return nativeGetBinderFreezeInfo(pid);
}

public boolean isFreezerSupported() {
    return nativeIsFreezerSupported();
}

native 能力检测会检查诸如:

  • FreezerState task profile 是否能解析到正确属性路径;

  • cgroup.freeze 是否能打开和读取;

  • Binder freezer info 是否能正常获取;

  • Frozen / Unfrozen profile 是否有效。

如果这些步骤失败,native 侧通常会留下很有辨识度的日志,例如:

纯文本
No attribute for FreezerState
Cannot open freezer path "...": ...
Failed to read freezer state: ...
Unexpected value in cgroup.freeze: ...
Unable to read freezer info: ...
Missing freezer profiles

这成为后面判断“能力检测究竟有没有被执行”的重要依据。


第一个弯路:看到某个 cgroup.freeze 不存在,不代表内核没有 freezer

排查早期很容易犯一个错误:照着常见示例去找类似路径:

纯文本
/sys/fs/cgroup/uid_0/cgroup.freeze

文件不存在。

如果在这里停下,很容易得出“这个 ROM 没启用 cgroup freezer”的结论。

但继续枚举 /sys/fs/cgroup 后发现,真正的布局根本不是这样。

系统进程使用类似:

纯文本
/sys/fs/cgroup/system/uid_<uid>/pid_<pid>/cgroup.freeze

应用进程使用类似:

纯文本
/sys/fs/cgroup/apps/uid_<uid>/pid_<pid>/cgroup.freeze

进一步检查 ROM 自己的 cgroups.json,可以看到 cgroup v2 根目录就是:

纯文本
/sys/fs/cgroup

并且显式声明了 freezer controller。

task_profiles.json 里也存在:

纯文本
FreezerState -> cgroup.freeze

同时 FrozenUnfrozen profile 都存在。

所以这一步真正得到的结论不是“freezer 不存在”,而是:

早期只是套用了错误的 cgroup 路径假设。HyperOS 使用的是自己的 system/apps 分层。

这是这次排查里很值得保留的一个经验:Android OEM 完全可以重新组织 cgroup 层级。判断某个进程的真实 cgroup,最可靠的方法不是猜路径,而是先读:

bash
cat /proc/<pid>/cgroup

再根据 0::/... 拼接到 /sys/fs/cgroup


Binder 和 native 也不像是缺失

继续检查目标系统的 Framework 和 native 二进制。

services.jar 中的 Freezer.java 是新的 native delegator 版本,而不是旧式 Java 自己解析 freezer 路径的实现。

libandroid_servers.so 中可以找到前面那些 AOSP isFreezerSupported() 失败分支字符串。

libbinder.so 的动态符号里也可以看到:

纯文本
IPCThreadState::freeze(...)
IPCThreadState::getProcessFreezeInfo(...)

这当然还不能单独证明 kernel ioctl 运行时一定成功,但至少可以排除一个简单假设:不是用户态 libbinder 连 Binder freezer API 都没有。

到这里,最异常的地方反而变成了日志。

每次切换 cached_apps_freezer,ActivityManager 都会马上打印:

纯文本
Freezer disabled

但即使把 Freezer 相关日志调高,也没有看到任何 native 能力检测失败信息。

如果 nativeIsFreezerSupported() 真被调用并返回 false,理论上应该更容易看到它究竟失败在哪一步。

于是怀疑开始转向另一个方向:

有没有可能 HyperOS 在进入 native 能力检测之前,就已经在 Java 层把 useFreezer 判成 false?

后来反编译 services.jar,直接找到了答案。


真正的硬门控:persist.sys.powmillet.enable

目标 HyperOS 的 CachedAppOptimizer.updateUseFreezer() 并不是原封不动的 AOSP 逻辑。

关键部分是:

Java
String configOverride = Settings.Global.getString(
        mAm.mContext.getContentResolver(),
        "cached_apps_freezer");

boolean mPowerMilletEnable = SystemProperties.getBoolean(
        "persist.sys.powmillet.enable",
        false);

if ("disabled".equals(configOverride) || mPowerMilletEnable) {
    useFreezer = false;
} else if ("enabled".equals(configOverride)
        || DeviceConfig.getBoolean(
                "activity_manager_native_boot",
                KEY_USE_FREEZER,
                true)) {
    useFreezer = mFreezer.isFreezerSupported();
} else {
    useFreezer = false;
}

这一段已经足够解释运行时现象。

只要:

纯文本
persist.sys.powmillet.enable=true

就会直接进入:

Java
useFreezer = false;

甚至不会调用:

Java
mFreezer.isFreezerSupported();

设备实测正是:

bash
adb shell getprop persist.sys.powmillet.enable

输出:

纯文本
true

普通 shell 尝试修改:

bash
adb shell setprop persist.sys.powmillet.enable false

会被 property service 拒绝,属性保持 true

这就把前面的所有现象串起来了:

纯文本
cached_apps_freezer = enabled


CachedAppOptimizer 收到变化


persist.sys.powmillet.enable = true


直接 useFreezer = false


不会进入 nativeIsFreezerSupported()


ActivityManager: Freezer disabled

所以之前看不到 native freezer 失败日志,不是“日志被吞了”,而是那段代码根本没运行

到这一步,运行中强制打开 AOSP freezer 为什么无效,已经有了直接答案。

但还有第二个问题:为什么重启以后设置值本身还会被写回 disabled

答案在 miui-services.jar


重启后自动变回 disabled:AurogonImmobulusMode 在主动维护互斥状态

/system_ext/framework/miui-services.jar 里,可以找到 Greezer 的 AurogonImmobulusMode

它的 updateCtsStatus() 几乎把 Xiaomi 的设计意图直接写在代码里:

Java
private void updateCtsStatus() {
    String freezeEnable = Settings.Global.getString(
            mContext.getContentResolver(),
            "cached_apps_freezer");

    if (mCtsModeOn) {
        SystemProperties.set(
                "persist.sys.powmillet.enable",
                "false");
        GreezeManagerDebugConfig.mPowerMilletEnable = false;

        if ("disabled".equals(freezeEnable)) {
            Settings.Global.putString(
                    mContext.getContentResolver(),
                    "cached_apps_freezer",
                    "enabled");
        }

        mGreezeService.thawAll("tsMode");
        mGreezeService.setFreezeBinderEnableCloud(false);

    } else if ("enabled".equals(freezeEnable)
            || DeviceConfig.getBoolean(
                    "activity_manager_native_boot",
                    "use_freezer",
                    true)) {

        Settings.Global.putString(
                mContext.getContentResolver(),
                "cached_apps_freezer",
                "disabled");
    }
}

正常用户模式下,mCtsModeOn=false

此时只要:

  • cached_apps_freezer 当前是 enabled,或者

  • DeviceConfig 的 use_freezer=true

它就会主动:

纯文本
cached_apps_freezer = disabled

initCtsStatus() 又会在 PowerMillet 启用时调用这套状态同步逻辑。

至此,最开始的“重启后为什么又变成已停用”也完全解释了。

两段代码分工非常清楚:

  • CachedAppOptimizer 的 PowerMillet 门控负责:即使你把设置改成 enabled,也不真正启用 AOSP freezer

  • AurogonImmobulusMode 负责:在正常模式下,把设置值本身重新维护成 disabled

这不是 UI bug,也不是某个 SettingsProvider 保存失败,而是 ROM 有意识地保持两套冻结方案互斥。


CTS 模式反而会切回 AOSP freezer

更有意思的是,AurogonImmobulusMode 还实现了反向逻辑。

当进入它定义的 CTS 状态:

纯文本
mCtsModeOn = true

系统会:

纯文本
persist.sys.powmillet.enable = false

必要时 cached_apps_freezer = enabled

Greezer thawAll()

关闭 Greezer 的 Binder-freeze 配置

这说明 Xiaomi 并不是“删掉了 AOSP freezer”。

更准确地说,它保留了 AOSP 路径,但在正常用户模式下选择 PowerMillet/Greezer;进入兼容性测试状态时,再切向更接近 AOSP 的行为。

反编译中还可以看到,某些被识别为 CTS/GTS 的包会触发:

Java
mCtsModeOn = true;
updateCtsStatus();

因此不建议把“伪装 CTS 包”当成日常切换方案。尤其 persist.sys.powmillet.enable 是持久属性,而普通 ADB shell 又没有权限自由写回,拿这种测试机制当用户开关没有必要。

既然目标是接受并使用 Greezer,最合理的做法反而是不要触发这条兼容路径。


那么 Greezer 到底是什么

继续追 miui-services.jar,可以看到一套相当完整的厂商后台冻结框架。

系统 init 中存在三个 Millet monitor service:

纯文本
millet_sig
millet_binder
millet_pkg

它们实际上都由同一个程序启动:

纯文本
/system_ext/bin/millet_monitor

只是传入不同参数:

纯文本
SIG
BINDER
PKG

运行时它们处于专用 millet SELinux domain。

GreezeManagerService 会注册这些 monitor,并用位图记录状态。三类通道都注册完成后:

纯文本
monitor = 7
MilletEnable = true

Greezer 才正式进入完整工作状态。

这里很重要的一点是:这些 monitor 并不是最终执行 freeze 的组件。

它们更像是事件入口,给 Greezer 提供各种“为什么需要解冻”“什么时候可以重新冻结”的反馈。

实际历史里能看到类似行为:

纯文本
FZ
THAW reason: PACKET
FZ
THAW reason: alarm
FZ
THAW reason: provider / service / foreground transition
...

也就是说,Greezer 不是一个简单的:

纯文本
进程进 cached → 过几秒 → freeze

而更像一个事件感知的后台状态机:

纯文本
后台应用

策略认为可冻结

FZ

网络包 / Alarm / Binder / Provider / Service / 前后台变化

THAW

处理事件

重新满足冻结策略

FZ

这也是为什么只盯着 Android 开发者选项里的 AOSP freezer 开关,会误判 HyperOS 实际的后台冻结状态。


Greezer 不是“只记账”:它真的写了 cgroup.freeze

这次调查最关键的内核证据来自 FreezeUtils

代码初始化时默认:

Java
FREEZER_ROOT_PATH = "/sys/fs/cgroup";

如果发现:

纯文本
/sys/fs/cgroup/apps/

存在,就会切成:

Java
FREEZER_ROOT_PATH += "/apps";

也就是:

纯文本
/sys/fs/cgroup/apps

Greezer 还支持 UID 维度或 PID 维度的冻结。

样本设备上的:

纯文本
persist.sys.greeze.cgroupuid

没有显式设置,代码默认值是 false

因此当前实际走的是逐 PID 冻结,而不是整个 UID 一次冻结。

实际路径形态是:

纯文本
/sys/fs/cgroup/apps/
  uid_<uid>/
    pid_<pid>/
      cgroup.freeze
      cgroup.events

冻结就是写:

纯文本
cgroup.freeze = 1

解冻写:

纯文本
cgroup.freeze = 0

为了排除“dumpsys greezer 只是 Java 层自己维护了 Frozen 列表”的可能性,又做了一次运行态交叉验证。

先从:

bash
adb shell dumpsys greezer

取得一个匿名化的 frozen PID,然后读取:

bash
cat /proc/<PID>/cgroup

得到类似:

纯文本
0::/apps/uid_UID-X/pid_PID-X

再看对应内核节点:

纯文本
/sys/fs/cgroup/apps/uid_UID-X/pid_PID-X/cgroup.freeze

实际值是:

纯文本
1

同时:

纯文本
cgroup.events

显示:

纯文本
populated 1
frozen 1

这一步非常关键,因为它把三层状态连起来了:

纯文本
Greezer Java bookkeeping

/proc/<pid>/cgroup

Linux cgroup v2 cgroup.freeze / cgroup.events

所以可以确定:

这台设备上的 Greezer 确实通过 Linux cgroup v2 freezer 停止目标进程执行。

它不是一个 UI 包装,也不是单纯记录“我认为这个应用被冻结了”。


Greezer 冻结成功后还维护 SmartPower 和自己的状态

freezeProcess() 并不是写完 cgroup.freeze 就结束。

成功冻结后,它还会维护一系列上层状态,大体包括:

纯文本
FreezeUtils.freezePid(...)

SmartPowerServiceInternal.addFrozenPid(...)

加入 Greezer 自己的 mFrozenPids

记录 fromWho / reason / freeze time

必要时安排 timeout thaw

解冻路径则基本对称:

纯文本
cgroup.freeze = 0

SmartPower removeFrozenPid

记录 thaw reason / thaw time

从 mFrozenPids 删除

进入 history

因此从架构上看,至少有三层:

纯文本
策略层
Aurogon / PowerStrategy / SmartPower

Greezer Framework 状态
mFrozenPids / history / monitor

内核执行层
cgroup v2 cgroup.freeze

这也解释了为什么 dumpsys greezer 能输出非常丰富的冻结历史和原因,而不只是一个当前 PID 集合。


Binder freeze 路径存在,但这个样本当前没有启用

Android 原生 Cached Apps Freezer 不只是冻结 CPU 执行,还会协调 Binder freezer。

Greezer 也保留了一条类似路径。

它的逻辑大体是:

Java
freezeBinder(uid, pid, true, timeout);
freezeProcess(...);

解冻时则反向执行。

freezeBinder() 前面有两个 gate:

Java
if (!mFreezeBinderEnable || !mKernelSupportNew) {
    return;
}

样本设备的 dumpsys greezer 显示:

纯文本
freezebinder_enable:true
supportEnable=false

所以目前实际情况是:

纯文本
mFreezeBinderEnable = true
mKernelSupportNew   = false

最终 freezeBinder() 直接返回。

也就是说,这台设备当前的真实冻结链更接近:

纯文本
Greezer 策略

Binder freeze 尝试

因 mKernelSupportNew=false 跳过

逐 PID cgroup v2 freeze

这里要特别避免一个过度推断:

supportEnable=false 不等于 “Android Binder driver 不支持 BINDER_FREEZE”。

它只能说明 Xiaomi 自己的 mKernelSupportNew 能力握手没有成功置位。

因为目标系统的 Framework 里,一旦 gate 打开,ProcessUtils.freezeBinder() 最终调用的还是:

Java
ActivityManagerService.getFreezer().freezeBinder(...)

也就是回到 Android 自己的 Freezer.freezeBinder() / native Binder freezer 路径。

Xiaomi 在这里增加的主要不是另一套 Framework Binder API,而是:

  • 是否使用 Binder freeze 的策略;

  • 自己的能力握手机制;

  • 和 Millet monitor / Greezer 状态机的配合。


那个神秘的 MAGIC_FREEZE_BINDER = 111

mKernelSupportNew 默认是 false。

它会在 reportNet() 收到一个特殊 UID 时被置为 true:

Java
if (MAGIC_FREEZE_BINDER == uid) {
    mKernelSupportNew = true;
    return;
}

而:

纯文本
MAGIC_FREEZE_BINDER = 111

当三个 Millet monitor 都注册完成后,Framework 会调用:

Java
nativeCall(
    MAGIC_FREEZE_BINDER,
    ADD_CORE_UID,
    true
);

其中:

纯文本
MAGIC_FREEZE_BINDER = 111
ADD_CORE_UID        = 100

从现有证据看,这很像一种 Xiaomi 自己的 userspace ↔ native/kernel 能力握手:

纯文本
Millet monitor 全就绪

nativeCall(111, 100, true)

Xiaomi native / kernel subsystem

若支持对应新能力,回报特殊事件

reportNet(uid=111)

mKernelSupportNew = true

当前设备停在:

纯文本
supportEnable=false

所以这条握手没有完成。

调查到这里就停止了,没有继续去定位 nativeCall() 最终落在哪个 .so、是否通过某个 netlink 通道与内核交互,也没有继续解释为什么这个样本没有得到 111 回报。

原因很简单:这些已经不影响原始问题的结论,也不影响 Greezer 当前正常通过 cgroup v2 工作。


为什么之前看到的 mcd / octvm SELinux 日志不是根因

早期日志里还出现过一条很诱人的线索:某个厂商进程尝试创建旧式:

纯文本
/sys/fs/cgroup/freezer

并被 SELinux 拒绝。

如果只看这一条日志,很容易把它和“freezer 启用失败”联系起来。

但后来它被排除为主因,理由包括:

  • 日志主要出现在启动阶段,与手工切换 cached_apps_freezer 的 live re-check 时刻对不上;

  • 当前 ROM 的真实 cgroup v2 cgroup.freeze 已被验证正常;

  • Greezer 的实际后端已经在 /sys/fs/cgroup/apps/.../cgroup.freeze 成功冻结进程;

  • 最关键的是,CachedAppOptimizer 在 PowerMillet=true 时根本不执行 native freezer support check。

也就是说,即使修掉那条旧路径的 AVC,也不会改变:

纯文本
persist.sys.powmillet.enable=true

useFreezer=false

这一 Framework 决策。

这是另一个值得记住的排障原则:SELinux denial 很显眼,但显眼不等于因果。必须看时间线和真实调用路径是否对得上。


Greezer 的控制面:可以看,但普通 ADB shell 不能随便操作

系统注册了一个 Binder service:

纯文本
greezer: [miui.greeze.IGreezeManager]

所以普通 shell 可以:

bash
adb shell dumpsys greezer

看到类似:

纯文本
Settings:
  enable=true
  monitor=true
  ...
Frozen processes:[...]
Greezer History:
  ...

反编译还发现 GreezeManagerService 有一套很强的 ShellCommand,包括类似:

纯文本
fzpid
fzuid
thpid
thuid
thaw
lsfz
query
history
binderproxy
...

但这里有个明确的权限限制。

checkPermission() 的逻辑大致是:

Java
int uid = Binder.getCallingUid();

if (!UserHandle.isApp(uid) && uid != 2000) {
    return;
}

throw new SecurityException(...);

ADB shell 的 UID 正是 2000

因此设计上就是:

纯文本
普通应用 UID    → 拒绝
shell UID 2000 → 拒绝
system/root 等  → 允许

这解释了一个看起来有点矛盾的现象:

  • dumpsys greezer 可以读;

  • 但不能指望普通 adb shell cmd greezer ... 获得完整 freeze/thaw 控制权。

对于日常排查,这其实是合理的安全边界。


最终架构可以这样理解

把所有已经验证过的部分放在一起,这台样本设备上的后台冻结大致可以画成:

纯文本
HyperOS 后台管理策略
       Aurogon / PowerStrategy / SmartPower


                 GreezeManagerService

          ┌──────────────┼──────────────┐
          │              │              │
          ▼              ▼              ▼
     millet_sig     millet_binder    millet_pkg
          │              │              │
          └──────────事件反馈────────────┘


                 freeze / thaw 决策

              ┌──────────┴──────────┐
              │                     │
              ▼                     ▼
      Binder freeze 辅助层      cgroup v2 执行层
      当前 gate 未通过          当前实际生效
              │                     │
              ▼                     ▼
      AMS Freezer API       /sys/fs/cgroup/apps/
                            uid_x/pid_y/cgroup.freeze

与此同时,AOSP Cached Apps Freezer 仍然留在 Framework 里,但正常模式下被另一个更高优先级的条件压住:

纯文本
persist.sys.powmillet.enable=true


CachedAppOptimizer.updateUseFreezer()


        useFreezer=false

而 Greezer 的 CTS 状态维护又进一步确保:

纯文本
正常模式:cached_apps_freezer → disabled
CTS 模式:PowerMillet → false,必要时 cached_apps_freezer → enabled

所以最准确的描述不是“小米删掉了 AOSP freezer”,而是:

HyperOS 保留了 AOSP freezer,但正常用户模式选择由 PowerMillet/Greezer 接管后台冻结,并显式阻止两套主冻结策略同时工作。


这次排查最终排除了哪些假设

为了避免后来重新掉进同样的坑,可以把主要假设整理成下面这样。

假设

最终判断

关键依据

Linux 内核没有 cgroup freezer

排除

cgroup v2 freezer controller、cgroup.freezefrozen=1 都真实存在

只是找错了 cgroup 路径

确认

ROM 使用 /system/apps 分层,而不是早期猜测路径

Frozen / Unfrozen task profile 缺失

排除

task_profiles.json 中存在对应 profile

用户态 libbinder 没有 freezer API

排除

freeze / getProcessFreezeInfo API 存在

AOSP nativeIsFreezerSupported() 自己失败

不是主因

PowerMillet=true 时根本不会调用它

Settings.Global 根本写不进去

排除

运行中可以成功写成 enabled,observer 也会响应

只是重启时某处覆盖了设置

只解释一半

确实有写回代码,但运行中 PowerMillet 也会直接阻止启用

mcd/octvm 的 SELinux AVC 是根因

排除为主因

时序不匹配,真实 cgroup v2 后端工作正常

HyperOS 注入了一个自定义 Freezer 子类

排除

AMS 默认仍返回标准 new Freezer(),关键改动在门控

Greezer 只是厂商 UI/Java 账本

排除

Frozen PID 对应 cgroup.freeze=1cgroup.events frozen=1

Greezer 当前启用了 Binder freeze

排除

freezebinder_enable=truesupportEnable=false

supportEnable=false 就代表 Binder driver 不支持 freeze

未证实

Xiaomi 自己的握手失败不能等价于标准 Binder ioctl 不存在


如果只想确认自己的 HyperOS 是否也是类似情况

以后再遇到同类问题,不需要重走这么长的路。

可以先看三个状态:

bash
adb shell settings get global cached_apps_freezer
adb shell getprop persist.sys.powmillet.enable
adb shell dumpsys activity | grep -E 'use_freezer=|Apps frozen:'

如果看到类似:

纯文本
cached_apps_freezer = disabled
persist.sys.powmillet.enable = true
use_freezer = false

就很值得直接检查目标 ROM 的:

纯文本
services.jar

搜索:

纯文本
persist.sys.powmillet.enable

如果 CachedAppOptimizer.updateUseFreezer() 里存在和本文类似的门控,那么不必再优先怀疑 cgroup 或 Binder。

然后检查:

bash
adb shell service list | grep -i greez
adb shell dumpsys -l | grep -i greez
adb shell dumpsys greezer | head -40

如果 Greezer 是活跃的,再从 Frozen 列表中选一个测试 PID,做内核交叉验证:

bash
adb shell '
p=<PID>
cg=$(awk -F: '\''$1=="0"{print $3}'\'' /proc/$p/cgroup)
echo "$cg"
cat "/sys/fs/cgroup${cg}/cgroup.freeze"
cat "/sys/fs/cgroup${cg}/cgroup.events"
'

如果得到:

纯文本
cgroup.freeze = 1
frozen 1

那就已经足以证明实际冻结发生在 kernel cgroup v2 层。


对日常使用的结论

如果接受 HyperOS 自己的 Greezer,那么最合理的状态其实就是当前默认状态:

纯文本
persist.sys.powmillet.enable = true
cached_apps_freezer = disabled
Greezer enable = true
Millet monitor = ready

这时开发者选项里的“暂停执行已缓存的应用”显示“已停用”,并不代表后台进程不会被冻结。

它只代表:

AOSP Cached Apps Freezer 不是当前 ROM 的主冻结策略。

没有必要继续反复把它强制切成 enabled,因为 PowerMillet 的 Framework 门控仍会让最终 mUseFreezer=false

同样不建议为了“让 AOSP freezer 看起来开启”去停掉 millet_sigmillet_bindermillet_pkg。这些 monitor 是 Greezer 事件反馈体系的一部分,停掉它们只会破坏厂商自己的状态机,却不会自动移除 persist.sys.powmillet.enable 这层门控。

也不建议把 CTS 兼容模式当成普通用户开关。它会修改持久 PowerMillet 状态、执行 thawAll() 并调整 Greezer 的 Binder freeze 配置,其目标明显更偏向兼容性测试环境,而不是日常电源管理切换。


还有哪些问题没有继续追

到停止调查时,仍然有几个值得继续研究、但已经不影响主结论的问题:

  • nativeCall(MAGIC_FREEZE_BINDER=111, ...) 最终落在哪个 native library;

  • 它是否通过 Millet 的 netlink 或其他厂商内核接口完成能力握手;

  • 为什么当前样本没有得到 reportNet(uid=111) 回报,从而一直 supportEnable=false

  • GreezeBinderProxyManager 在 Binder freeze gate 没打开时承担了多少事务代理职责;

  • Millet 的 BINDER monitor 与标准 Binder freezer 之间精确如何分工;

  • 不同 SoC / HyperOS 分支是否会让 mKernelSupportNew=true

  • Greezer 和 AOSP Cached Apps Freezer 在真实功耗、推送延迟、后台兼容性上的量化差异。

这些问题都很有意思,但已经属于下一层研究。

原始问题本身已经闭环:

纯文本
为什么开发者选项会变回“已停用”?

因为 HyperOS 正常模式下主动让 PowerMillet/Greezer 接管后台冻结,
并通过 Framework 门控 + CTS 状态维护,让 AOSP Cached Apps Freezer 保持关闭。

而最值得这次排查时间的地方,是最后确认了另一件事:

纯文本
“原生 freezer 关闭” ≠ “系统没有 freezer”

在这个样本上,真正工作的冻结链路是:

纯文本
Millet 事件监控

GreezeManagerService / Aurogon / SmartPower

逐 PID freeze / thaw 策略

Linux cgroup v2 cgroup.freeze

开发者选项只是露出了 AOSP 那一层开关,而 HyperOS 真正运行的后台冻结系统,在它下面还有一整套厂商实现。

END