手把手:干掉 MIUI 状态栏那个碍眼的 VPN 钥匙——cmd overlay fabricate 实战
手把手:干掉 MIUI 状态栏那个碍眼的 VPN 钥匙—— cmd overlay fabricate 实战
设备:POCO 22041216G · Android 13 · MIUI V14.0.6.0.TLOEUXM 环境:Magisk 26.4(root)+ Termux + MacroDroid 5.67.8 结论:绕开 LSPosed 和普通 RRO,用 root 的
cmd overlay fabricate把 4 个 VPN 图标资源重定向到系统自带的ic_blank,再用 MacroDroid 开机重建,图标秒变空白。
0. 起因:一把去不掉的钥匙
用 FlClash 挂着后台跑代理,状态栏顶部的 VPN 小钥匙就成了常驻嘉宾。
一开始想当然,去翻 icon_blacklist:
$ settings get secure icon_blacklistwifi,bluetooth,rotate,location,headset,volume,alarm_clock,mobile,vpnvpn 明明在里面,图标却纹丝不动。
这篇文章就带你走完整条链路:为什么系统设置不管用 → 图标到底是谁画的 → 三条路怎么死的 → 最后用一条冷门命令收尾 → 怎么让它开机自动生效。
1. 先否掉最省事的猜测
1.1 icon_blacklist=vpn 为什么无效
AOSP 的 StatusBarIconControllerImpl 里,能被黑名单的 slot 名是这一批:
wifi / bluetooth / rotate / location / headset / volume / alarm_clock / mobile唯独没有 vpn。
原因也不难理解:AOSP 的 VPN 图标走的是 SystemUI 内部自己的注册路径,从不经过 StatusBarManagerService.mIcons。dumpsys statusbar 里只能看到 stealth、call_record、mute、speakerphone 这几个,VPN 根本不在其中。
反编译 MIUI 的 MiuiSystemUI.apk 后可以确认,里面确实有:
mSlotVpnmCurrentVpns={}stat_sys_vpncom.miui.vpnsdkmanagercurrentVpnIconId但那个短 slot 名列表里,就是没有 vpn。
结论:icon_blacklist=vpn 是个摆设。写着好看,实际不动。
1.2 图标是谁触发的
先看事实。FlClash 的 VpnService 会建一张 tun0 网卡:
$ ip link show tun047: tun0: <POINTOPOINT,UP,LOWER_UP> mtu 9000 qdisc pfifo_fast state UNKNOWN ... link/none$ dumpsys connectivity | grep -i vpn# 172.19.0.0/30 dev tun0 ...只要 tun0 在,系统就会挂上 VPN 图标。这是系统行为,App 自己关不掉——FlClash 的 config.yaml 里 tun.enable=false 也没用,因为 FlClash 用的是 Android 的 VpnService API,跟 Clash 内核的 tun 配置是两码事。
顺手把 FlClash 能翻的 shared_prefs 全翻了一遍,也没有”隐藏 VPN 图标”这种选项。
1.3 图标资源在哪
既然关不掉「显示行为」,那就换个思路——去动资源。
从 MiuiSystemUI.apk(/system_ext/priv-app/MiuiSystemUI/)里 dump 出资源表:
$ aapt2 dump resources MiuiSystemUI.apk | grep stat_sys_vpn resource 0x7f081055 drawable/stat_sys_vpn resource 0x7f081056 drawable/stat_sys_vpn_darkmode resource 0x7f081057 drawable/stat_sys_vpn_ic resource 0x7f081058 drawable/stat_sys_vpn_tint四个都是矢量 XML(vector drawable)。只要是资源,就有人能覆盖它。
2. 三条看起来可行的路,以及它们怎么死的
2.1 路一:LSPosed 模块 —— 框架没在跑
第一反应是写个 Xposed 模块 Hook 掉图标显示。结果:
$ ps -A | grep lspd# (空)lspd 进程不存在。继续查 /data/adb/modules/,发现几乎所有 Magisk 模块都被批量禁用了:
zygisk_lsposed [禁用]riru_lsposed [禁用]zygisk_shamiko [禁用]riru-core [禁用]box5 [禁用]Automatic_brick_rescue [禁用]...仅 GGAT_10007 启用起因是某个 Automatic_brick_rescue 被触发过一次,把模块全关了。
这条路直接断。(想恢复也可以,但那是另一个话题,而且有开机风险。)
2.2 路二:普通 RRO overlay(APK 形式)—— 签名不匹配
MIUI 自己就大量使用 RRO overlay:
$ ls /product/overlay/*.apk | head/product/overlay/MiuiSystemUIResOverlay.apk/product/overlay/GmsSystemUIOverlay.apk/product/overlay/AospFrameworkResOverlay.apk...那我照着做一个不就行了?思路很清楚:
- 用
aapt2编译一个 overlay APK,target 指com.android.systemui; - 把 4 个 VPN 图标资源替换成透明版本;
--stable-ids保证资源 ID 精确对齐目标。
ID 对齐这一步我确实做到了:
0x7f081055 drawable/stat_sys_vpn ✅0x7f081056 drawable/stat_sys_vpn_darkmode ✅0x7f081057 drawable/stat_sys_vpn_ic ✅0x7f081058 drawable/stat_sys_vpn_tint ✅APK 编译、签名(v3)全部通过。结果一安装:
INSTALL_FAILED_INTERNAL_ERROR: Scanning Failed.:Overlay com.aether.vpniconhide and target com.android.systemuisigned with different certificates,and the overlay lacks <overlay android:targetName>翻译一下:overlay 和目标签名不一致,而目标没有声明 overlayable。
再查两者的签名:
$ apksigner verify --print-certs MiuiSystemUI.apk | grep DNDN: ... CN=MIUI, O=Xiaomi ...
$ apksigner verify --print-certs MiuiSystemUIResOverlay.apk | grep DNDN: ... CN=MIUI, O=Xiaomi ... ← 完全一样
$ aapt2 dump resources MiuiSystemUI.apk | grep -i overlayable# (空,SystemUI 没有 overlayable 定义)三条事实叠在一起:
- SystemUI 用小米平台签名;
- 系统 overlay 也全是这个签名;
- SystemUI 没有
<overlayable>,所以只能同签名覆盖。
第三方签不出小米的签名,这条路死。
2.3 路三:cmd overlay 普通 enable —— 连装都装不上
既然 APK 装不上,就试试命令行:
$ cmd overlay enable --user 0 com.aether.vpniconhide# Error: package not found没用。
3. 转机:cmd overlay fabricate
翻 cmd overlay help 的时候,看到一条平时根本不会注意的命令:
fabricate [--user USER_ID] [--target-name OVERLAYABLE] --target PACKAGE --name NAME PACKAGE:TYPE/NAME ENCODED-TYPE-ID ENCODED-VALUE
Create an overlay from a single resource. Caller must be root.关键词:Caller must be root。
这条命令的精髓在于:它让 system_server 自己去创建一个「伪造的」overlay。因为是系统自己生成的,签名检查直接被绕过——恰好补上 2.2 里缺的那块拼图。
3.1 第一步:找一个”空白”资源,别乱填
这一步是安全的关键。
fabricate 的最后一个参数是资源值。如果随手填 0x00000000,那是个无效引用,SystemUI 取图标时很可能直接 NPE 崩掉。
正确做法:指向 SystemUI 里本来就存在、且画出来什么都没有的资源。翻资源表,真就有一个:
$ aapt2 dump resources MiuiSystemUI.apk | grep -B1 ic_blank resource 0x7f08073a drawable/ic_blank () (file) res/drawable/ic_blank.xml type=XML名字就叫 ic_blank,内容是个空 vector。完美。
3.2 第二步:fabricate + enable
先做主力那个:
# stat_sys_vpn -> ic_blanksu -c 'cmd overlay fabricate --target com.android.systemui --name vpniconhide \ com.android.systemui:drawable/stat_sys_vpn 0x01 0x7f08073a'su -c 'cmd overlay enable --user 0 com.android.shell:vpniconhide'参数含义:
0x01=TYPE_REFERENCE(这是个资源引用);0x7f08073a=ic_blank的资源 ID。
没报错。 看看它挂在哪儿:
$ su -c 'cmd overlay dump com.android.shell:vpniconhide' | grep -A3 IDMAPIDMAP OF com.android.shell:vpniconhidePaths: target path : /system_ext/priv-app/MiuiSystemUI/MiuiSystemUI.apk overlay path : /data/resource-cache/com.android.shell-vpniconhide-xxxx.frroMapping: 0x7f081055 -> reference 0x7f08073a (drawable/stat_sys_vpn)映射正确。 再用 lookup 验证解析结果:
$ su -c 'cmd overlay lookup --user 0 com.android.systemui \ com.android.systemui:drawable/stat_sys_vpn'@2131232570 -> res/drawable/ic_blank.xmlstat_sys_vpn 现在解析到 ic_blank.xml。图标变空白了,SystemUI 进程还活着,logcat 干净。
3.3 第三步:把另外三个变体也补上
只改 stat_sys_vpn 可能在某些模式(深色、tint)下露馅,所以四个一起处理:
su -c 'cmd overlay fabricate --target com.android.systemui --name vpniconhide_dark \ com.android.systemui:drawable/stat_sys_vpn_darkmode 0x01 0x7f08073a'su -c 'cmd overlay enable --user 0 com.android.shell:vpniconhide_dark'
su -c 'cmd overlay fabricate --target com.android.systemui --name vpniconhide_ic \ com.android.systemui:drawable/stat_sys_vpn_ic 0x01 0x7f08073a'su -c 'cmd overlay enable --user 0 com.android.shell:vpniconhide_ic'
su -c 'cmd overlay fabricate --target com.android.systemui --name vpniconhide_tint \ com.android.systemui:drawable/stat_sys_vpn_tint 0x01 0x7f08073a'su -c 'cmd overlay enable --user 0 com.android.shell:vpniconhide_tint'最终状态:
$ su -c 'cmd overlay list com.android.systemui'com.android.systemui[x] com.android.overlay.systemui[x] com.miui.systemui.devices.overlay[x] com.miui.systemui.carriers.overlay--- com.android.systemui.overlay.miui[x] com.android.shell:vpniconhide[x] com.android.shell:vpniconhide_dark[x] com.android.shell:vpniconhide_ic[x] com.android.shell:vpniconhide_tint全部启用,钥匙消失。
3.4 第三步的坑:重启就没了
高兴没两天,重启一次,钥匙又回来了。
$ su -c 'cmd overlay list com.android.systemui'# [ ] com.android.shell:vpniconhide ← 变回未启用/消失查了一下 /data/system/overlays.xml:fabricate 的条目过不了重启。它是动态 overlay,重启时不会自动重建。
所以必须”开机自动重建”。
4. 让它开机自动生效(MacroDroid)
Magisk service.sh 当然也行,但对不想再折腾模块的人来说,用 MacroDroid 最省事。
4.1 宏的结构
触发器:Device Boot(开机)动作 :Shell Script(勾选 root / 以 root 身份运行)4.2 脚本内容
把下面整段填进 Shell Script 动作里:
sleep 15
IC_BLANK=0x7f08073aSYS=com.android.systemuiHOST=com.android.shell
hide() { su -c "cmd overlay fabricate --target $SYS --name $1 $SYS:drawable/$2 0x01 $IC_BLANK" su -c "cmd overlay enable --user 0 $HOST:$1"}
hide vpniconhide stat_sys_vpnhide vpniconhide_dark stat_sys_vpn_darkmodehide vpniconhide_ic stat_sys_vpn_ichide vpniconhide_tint stat_sys_vpn_tint4.3 几个细节
sleep 15:等系统 overlay 服务完全就绪,太早执行会失败;su -c:cmd overlay是 root 命令,普通 shell 跑不动;- 图标会先闪一下:MacroDroid 是 App 启动后才执行,所以开机瞬间 VPN 图标可能会短暂出现再消失。这是正常现象——如果你无法接受这一闪,就改用 Magisk
service.sh。
如果系统里 su 路径特殊,把 su -c "..." 换成 MacroDroid 自带的 “Run as root” 选项包裹即可。
5. 随时能退回去
折腾的第一原则:先准备好还原脚本。
#!/system/bin/shfor n in vpniconhide vpniconhide_dark vpniconhide_ic vpniconhide_tint; do cmd overlay disable --user 0 com.android.shell:$n cmd overlay disable --user 11 com.android.shell:$ndonerm -f /data/resource-cache/*vpniconhide*pkill -f com.android.systemui或者只关一个:
su -c 'cmd overlay disable --user 0 com.android.shell:vpniconhide'因为是动态 overlay,disable 即刻生效,不用重启。这也是它比改系统文件、改 APK 安全得多的地方。
6. 为什么这套方案值得记下来
| 维度 | 表现 |
|---|---|
| 需要 LSPosed / Xposed | ❌ 不需要 |
| 需要 Hook 代码 | ❌ 不需要 |
| 改系统文件 | ❌ 不需要(纯 overlay 映射) |
| 崩溃风险 | 🟢 很低(指向系统自带 ic_blank,不是非法引用) |
| 可逆性 | 🟢 disable 秒级恢复,无需重启 |
| 持久化 | ⚠️ 需开机脚本(MacroDroid / Magisk) |
核心思路一句话:既然图标是个资源,就别跟”显示逻辑”较劲,直接把那个资源换成一张白纸。
7. 换设备 / 换 ROM 必读
-
0x7f08073a不是通用值。这是我这台设备上ic_blank的 ID。换机型必须自己 dump:Terminal window aapt2 dump resources /path/to/MiuiSystemUI.apk | grep -iE "ic_blank"没有
ic_blank也不要紧,找一个”空”的 drawable 即可,关键是它必须真实存在。 -
stat_sys_vpn的 ID 同理,必须从自己设备的MiuiSystemUI.apk里 dump 出来,不能照搬。 -
绝对不要指向
0x00000000。那是无效引用,SystemUI 有崩溃风险。这一点是”安全”与”作死”的分界线。 -
user 0 之外的用户(比如第二空间 user 11):想让那边也生效,把
--user 0换成对应 user 或再加一条 enable。 -
Termux 后台执行不可靠。我一开始想用 Termux 做开机脚本,结果发现重启后 Termux 进程被系统置于后台受限状态,
su -c直接报:Not allowed to start service Intent ... app is in background所以持久化要么交给 MacroDroid,要么放进 Magisk 的
service.sh,别指望 Termux。 -
不同 ROM 的图标资源名可能不同。AOSP 原生是
stat_sys_vpn,各家 ROM 可能改成别的名字。找不到就先把MiuiSystemUI.apk/SystemUI.apk的所有*vpn*资源列一遍。
8. 复盘
这一路的教训其实很朴素:
- 先观察,别先下结论。
icon_blacklist有vpn不等于它有用,要去看 SystemUI 到底认哪些 slot。 - 一条路走不通,要弄清它为什么不通。普通 RRO 失败不是因为思路错,而是卡在「签名」这一环——把原因搞清楚,才可能找到
fabricate这个绕法。 - 安全的边界很具体。不是抽象地“小心点”,而是:别指向
0x00000000,要指向真实存在的ic_blank;动态 overlay 随时能disable;先备好还原脚本再动手。 - 持久化要选对工具。Termux 会被系统限制,MacroDroid/Magisk
service.sh才是能开机跑起来的。
9. 一键回滚速查
| 目的 | 命令 |
|---|---|
| 临时关掉 | su -c 'cmd overlay disable --user 0 com.android.shell:vpniconhide' |
| 全部关掉 | su -c 'for n in vpniconhide vpniconhide_dark vpniconhide_ic vpniconhide_tint; do cmd overlay disable --user 0 com.android.shell:$n; done' |
| 彻底清理 | su -c 'rm -f /data/resource-cache/*vpniconhide*' |
| 刷新状态栏 | su -c 'pkill -f com.android.systemui' |
折腾有风险,动手前先备份、先准备好还原脚本。本文记录的是个人设备上的探索过程,不同 ROM 表现可能不同,请以自己的实际环境为准。