做 RK3588 Android 12 固件定制的时候,经常有客户提一个需求:壁纸怎么换,系统主题色都别跟着变。这个需求背后的元凶,就是 Android 12 默认开启的 Monet——Google 所谓的 Material You 动态取色。它会在壁纸更换后自动从图片里提取主色,然后把 SystemUI、通知栏、设置页、部分第三方应用的配色全部换一遍。对消费机来说这是卖点,对行业终端、广告机、一体机来说就是灾难。这篇文章就围绕怎么禁用 Monet 展开,先讲原理,再给调试阶段的快速关闭命令,最后给量产固件的编译期方案,覆盖 RK3588 Android 12 以及大多数 Android 12 AOSP 分支,不做脱离实际的“理论分析”,每一步都是能直接跑的命令和能落地的改动。
1. Monet 到底在哪一层起作用
1.1 从壁纸到主题色:一条完整的动态变色链路
Monet 不是一个独立应用,也不是一个普通开关。它本质上是“取色 + 调包”的组合:系统检测到壁纸变化,从壁纸 Bitmap 里提取主要颜色,把颜色交给 SystemUI 的取色引擎,再由引擎生成一套动态主题资源,最后通过 Android 的资源覆盖机制把 SystemUI、Launcher、通知栏等模块的配色全部替换掉。要禁用这个功能,不能只靠“换一张纯色壁纸”这种土办法,因为只要取色链路还活着,无论壁纸内容是什么,颜色都会被抽象成主题色,只是观感上不明显而已。
整套链路可以拆成四步:
- 壁纸变化触发:
WallpaperManager发现壁纸文件被替换,向 SystemUI 的ThemeOverlayController发送通知。 - 取色阶段:SystemUI 拿到壁纸 Bitmap,交给
ColorExtractor等取色算法,从图片里提取主色、辅色、强调色。 - 生成主题阶段:取色结果被封装成
ColorScheme,生成一组系统主题 overlay 的资源值。 - 应用阶段:OverlayManagerService 根据生成的颜色值,启用对应的运行时资源覆盖包(RRO),SystemUI 会立刻重绘。
理解了这条链路,你就明白为什么禁用 Monet 有多个下手点:可以从源头不取色,可以不让ThemeOverlayController响应壁纸变化,也可以直接在 overlay 管理层面禁用相关资源包。选择哪种方式,取决于你的目标是“调试机快速验证”还是“量产固件彻底关死”。
这里顺带提一句,做行业定制时,Android 12 默认的权限授予策略也会被客户反复问到,比如 Android 12 上 targetSdk 31 的应用默认权限预授权方式变了,很多厂商需要在开机时做批量授权。这类默认行为和 Monet 一样,都属于 AOSP 默认策略“不太适合行业终端”的典型例子,经常被放在同一张需求单上处理。Monet 相对容易被忽略,因为不仔细看根本不会发现颜色在变,但一旦客户换了壁纸,整个 UI 色系全乱,问题立刻就暴露了。
1.2 为什么不能用“杀掉 SystemUI”或“锁死壁纸”凑合
最原始的做法是监听壁纸变化,一旦发生变化就强制把壁纸替换回去。这在实际项目中往往不靠谱:一是客户可能就是想换壁纸,只是不希望界面变色;二是WallpaperManager和系统取色逻辑之间是异步的,你强制替换壁纸的时机很难卡准,经常出现“替换成功但是取色已经跑完”的情况。另一个野路子是直接杀掉 SystemUI,让系统重启 UI,这更不可取,因为 SystemUI 会自动恢复并且重新执行壁纸取色逻辑。
真正的解决办法,还是回到机制层面,把动态取色的“信号源”切断,或者把取色结果“应用不上”。这样即使客户换壁纸,系统也只会当壁纸换了,UI 配色纹丝不动。后面的方案都是围绕这两条思路展开的。
2. 先跑通再固化:调试机上关掉 Monet
2.1 第一步:找到当前生效的 Monet 相关 Overlay
拿到一台 Android 12 设备,建议先别急着改代码,用 adb 在系统层把动态取色关掉,验证这条路走不走得通。第一步是看看当前系统里到底有哪些 overlay 和 Monet 相关。
adb shell cmd overlay list | grep -i monet adb shell cmd overlay list | grep -i color输出大致长这样(不同 BSP 的包名会有差异,以实际输出为准):
[ ] com.android.internal.systemui.monet [ ] com.android.systemui.monet [ ] com.android.internal.systemui.color方括号里的状态表示 overlay 是否被启用。其中带monet字样的包就是动态取色的核心资源包,只要这些包处于启用状态,主题色就会跟随壁纸变化。cmd overlay list输出的包名是最可靠的参考,不同厂商的 Android 12 分支里,这些包名的后缀可能不完全一致,建议做项目时先跑一遍命令,把包名记录下来,后续所有操作都以这份实际列表为准。
2.2 第二步:清掉主题覆盖包并禁用 Overlay
找到相关包名后,执行下面三条命令:
adb shell settings put secure theme_customization_overlay_packages "" adb shell cmd overlay disable com.android.internal.systemui.monet adb shell cmd overlay disable com.android.systemui.monet adb shell pkill -f com.android.systemui第一条命令的作用是清空主题定制 overlay 的注册列表。Android 12 的 ThemeManager 会读取Settings.Secure里的theme_customization_overlay_packages字段,这个字段里保存着当前风格下应该启用的 overlay 包名,置为空串后,SystemUI 就不知道要加载哪些动态取色资源包,取色结果自然无法落地。
后两条命令是把刚才查到的 Monet 相关 overlay 全部禁用,这是兜底操作,防止清空字段后系统缓存里仍然残留旧状态。最后pkill -f com.android.systemui是为了让 SystemUI 彻底重启,重新读取设置。
这里有一个容易被忽略的点:如果cmd overlay disable提示overlay is a static overlay,说明这个 overlay 是静态启用的,不能在运行时直接禁用。遇到这种情况不用慌,继续执行第一条清空字段的命令,再重启 SystemUI,一般也能挡住动态取色;如果要彻底解决,就得走第 3 章的编译期方案。
2.3 第三步:验证关闭效果
执行完命令后,先看状态:
adb shell cmd overlay list | grep -i monet adb shell settings get secure theme_customization_overlay_packages正常的预期是:monet 相关 overlay 状态从[x](启用)变成[ ](禁用),theme_customization_overlay_packages返回null或空串。然后换一张色彩强烈的壁纸,观察设置页、通知栏、音量条的颜色是否变化。如果颜色保持不变,说明运行时禁用成功。
如果发现颜色还是会变,优先检查两个东西:一是系统里是否还有别的 Monet overlay 没被禁用,比如第三方定制的 overlay;二是部分 GMS 应用(如 Google 提供的壁纸应用)会主动从壁纸取色,跟系统 Monet 无关,这种情况需要单独处理。这些干扰项我放到后面第 4 章专门说。
3. 量产固件:编译期把 Monet 关死在系统里
3.1 用 RRO 覆盖动态取色开关
调试机验证通过后,就要把改动固化到固件里。最省事的方式是给系统打一个运行时资源覆盖包(RRO),把控制动态取色的开关直接覆盖成关闭状态。在 AOSP 分支里,framework 资源里通常有一个布尔值控制是否允许动态颜色,常见字段名是config_allowDynamicColors,不同分支的具体字段可能会有出入,需要先搜索确认。
在设备目录下新建一个 overlay 目录,例如device/rockchip/rk3588/overlay/frameworks/base/core/res/res/values/config.xml,内容如下:
<resources> <bool name="config_allowDynamicColors">false</bool> </resources>然后在这个 overlay 目录下添加编译配置,如果项目用 Android.bp,可以这样写:
runtime_resource_overlay { name: "MyFrameworkOverlay", theme: "android", product_specific: true, }再把 overlay 加进产品打包列表,例如在 device.mk 里加:
PRODUCT_PACKAGES += MyFrameworkOverlay这样编译出来的固件就会携带一个名为MyFrameworkOverlay的 overlay,在开机时覆盖 framework 里的动态取色开关。SystemUI 读取到false后,不会生成动态主题资源,Monet 相当于被系统废弃了。
这种方式的好处是不改源码,只加资源和配置,风险最低,后续要恢复也容易。需要注意字段名一定要以当前源码为准,如果编译后没效果,先确认 overlay 有没有真正编译进系统,再看字段名对不对,别盲目相信网上的固定写法。
3.2 直接断掉 SystemUI 的取色调度
如果 RRO 覆盖不生效,或者你希望更彻底地干掉取色链路,就得动 SystemUI 源码。核心文件是ThemeOverlayController.java,这个类负责监听壁纸变化、调度取色、应用颜色 overlay。定位到start()方法,找到壁纸变化回调相关的注册,直接禁用。
以 AOSP 12 常见代码为例,修改思路如下:
@Override public void start() { // 禁用 Monet:不再注册壁纸变化监听 // mContext.registerReceiver(mWallpaperChangedReceiver, // new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED)); } @Override public void onWallpaperChanged() { return; // 直接忽略壁纸更新,阻止取色调度 }不同版本的代码结构会有差异,建议先搜索WallpaperChanged和onWallpaperChanged关键字,找到对应位置再决定注释哪些行。这个方案最彻底,因为连取色信号都没有了,不管后续怎么配置 overlay,都不会有动态变色发生。
但改 SystemUI 源码需要留意副作用:如果同一段代码里还承载了深色模式切换、用户手动选择主题色等功能,粗暴注释会把正常功能也干掉。建议在改之前,先把这个类的完整逻辑读一遍,弄清楚哪些方法只服务于 Monet,哪些方法被多个功能共用,只注释掉 Monet 专属的部分。
3.3 改掉 SettingsProvider 的默认值
第 2 章运行时方案里,我们其实是手工把theme_customization_overlay_packages的当前值置空了。这个设置如果不改默认值,设备恢复出厂设置后还是会回到原始状态,因为 SettingsProvider 的 defaults 配置里会写入默认 overlay 列表。
所以在量产固化时,还需要找到frameworks/base/packages/SettingsProvider/res/values/defaults.xml,在文件里搜索theme_customization关键字,找到对应的默认值配置项,把默认值改成空串:
<string name="def_theme_customization_overlay_packages"></string>这样设备无论怎么恢复出厂设置,动态取色的 overlay 列表始终是空的,Monet 不会复活。这个改动建议和第 3.1 节的 RRO 覆盖一起做,一个管运行态,一个管出厂默认值,双管齐下,稳定性更高。
注意,如果你的系统是 RK3588 这类 BSP,SettingsProvider 可能已经被厂商定制过,默认值字段的位置不一定和 AOSP 完全一致,最快的定位方式是全局搜索theme_customization_overlay_packages字符串,找到之后顺着资源索引去 defaults 文件里改。
3.4 清理系统里默认携带的 Monet Overlay 包
最后一步,是把系统里预置的 Monet overlay 包本身移除。这些包通常以com.android.internal.systemui.monet、com.android.systemui.monet之类的名字出现在frameworks/base/packages/SystemUI或vendor目录下。如果它们不存在,即使 RRO 开关失效、Settings 默认值被清空,系统也没有资源可应用,颜色自然回退到默认 Material 色。
移除方式有两种:一是从PRODUCT_PACKAGES列表里去掉对应包,二是直接用 overlay 覆盖掉这些包的资源。第一种方式更彻底,但需要确认这些包没有被其他模块依赖,建议在PRODUCT_PACKAGES里搜索一下包名,确认不影响到其他功能再移除。
如果不想移除,也可以在编译脚本里把 overlay 的默认启用状态改成 false。这个要看具体包实现,有的是在 AndroidManifest 里声明android:overlay="true"和priority,有的是在资源里控制config_enableMonet之类的开关,找到对应资源改掉即可。整体原则是:能不动源码就不动源码,资源层能解决的就别碰 Java 代码。
4. 改完后的坑,我基本都踩过一遍
4.1 刚关掉又变色:Settings 被系统服务还原
最常见的坑,是调试机上执行settings put后,当时确实不变色了,但重启设备或者过一段时间,颜色又回来了。原因很简单:theme_customization_overlay_packages这个值会被 SystemUI 或者其他系统服务周期性写回,你手动置空很可能在一段时间后就被系统兜底逻辑恢复了。
解决办法有两个方向:一是别依赖单一的 Settings 修改,配合cmd overlay disable一起做,至少保证当前 overlay 是禁用状态,即使 Settings 被还原,没有 overlay 可用也变不了色;二是把改动放在开机启动阶段,在 init 脚本或开机广播里再次执行settings put,确保每次开机都先把值置空。
我在实际项目里测过,单独靠settings put在 RK3588 上重启后大概率失效,必须配合编译期改动才能稳定。所以快速验证和量产固化要分开看,不要觉得调试机跑通了,固件就一定能复现。
4.2 Launcher 图标颜色仍然会随壁纸变
禁用 Monet 之后,还有一个容易遗漏的地方是 Launcher 的动态图标。AOSP 的 Launcher3 在 Android 12 上会直接读取壁纸取色结果,用于生成动态图标背景,这个逻辑不一定走系统 overlay,可能是 Launcher 自己内置的取色算法。所以经常出现“SystemUI 已经不变色了,Launcher 的图标背景还在跟着壁纸变”的尴尬情况。
处理方式是在 Launcher 的资源或代码里关闭动态图标。Launcher3 里一般会有FLAG_USE_DYNAMIC_COLOR或类似的宏,搜索dynamic关键字,把开关关掉,让图标固定使用默认取色或品牌色。如果用的是 RK3588 BSP 自带的第三方 Launcher,那就得看 Launcher 自己的设置项,通常在壁纸设置或主题设置里可以关闭跟随壁纸变色,实在不行只能改 Launcher 源码。
另外还要提醒一点,部分第三方应用会在 targetSdk 31 之后主动调用WallpaperManager的颜色提取 API,自己做主题换肤,这些跟系统 Monet 完全无关。遇到这种应用,只能靠应用的设置项关闭,系统层面没法一刀切。
4.3 编译不通过或 Overlay 不生效的排查
编译期方案最常遇到的问题,是改了资源、加了 overlay,但烧进设备后一点反应都没有。排查顺序我一般是这样:
- 先确认 overlay 有没有真正打包进固件。烧机后执行
adb shell cmd overlay list | grep MyFrameworkOverlay,如果列表里找不到,说明产品打包配置有问题,检查PRODUCT_PACKAGES有没有写对名字,Android.bp 里的name和 product 变量是否一致。 - 再确认 overlay 是否处于启用状态。如果是静态 overlay,默认是启用的;如果是动态 overlay,可能还需要用
cmd overlay enable开启。 - 接着确认资源字段名是否正确。
config_allowDynamicColors在不同 AOSP 分支里不一定存在,如果你覆盖了一个不存在的资源,编译时可能不会报错,但运行时也不会有效果。用grep -r "allowDynamicColors" frameworks/base/core/res/确认字段存在。 - 最后确认 target package 是否写对。RRO 里的
theme: "android"表示覆盖 framework 资源,如果目标包写成了 SystemUI,需要改成theme: "com.android.systemui",否则资源覆盖不到对应模块。
我遇到过最奇葩的情况是 overlay 编译进去了、也启用了,但 SystemUI 有自己的一套动态取色缓存,需要重启两次才生效。所以排查的时候不要着急,先看 overlay 状态,再看资源字段,最后考虑缓存问题,按顺序来能省很多时间。
4.4 禁用后深色模式异常
还有一个比较容易踩的坑,是禁用了 Monet 后,深色/浅色模式的切换逻辑跟着乱了。原因是动态取色资源里不仅包含主色调,还包含深色模式下的一组衍生色,你把这条链路切断后,系统如果对深色模式的颜色资源依赖比较重,就会出现“切到深色模式后部分文字看不清”或“背景色发灰”的现象。
遇到这种情况,不要想着重新开启 Monet,那是拆东墙补西墙。正确做法是检查 SystemUI 和 framework 的深色模式资源,看缺少哪个颜色项,在 overlay 里手工补一个固定的深色配色。最简单的方式是复用默认的system_accent_color系列资源,把深色模式下缺失的颜色项指过去,保证整体 UI 颜色统一但不动态变化。
这个问题的排查思路也不复杂:先对比深色模式下显示异常的颜色名称,再去资源目录里搜索对应的 color 资源,看看哪一项被动态取色覆盖了,然后显式写死一个默认值即可。
最后分享一点我处理这类需求的排序:先想清楚是验证还是出货,验证就 adb 命令快上快下;出货就把编译期改动做完整,别指望靠几行settings put应付过去。Monet 虽然只是 Android 12 的一个默认行为,但在行业终端上,它和权限预授权一样,都是会被客户拿放大镜看的功能点。我这边踩完这些坑后,后续项目基本固件一出厂就是关掉的,再也没被退回过。