手把手:干掉 MIUI 状态栏那个碍眼的 VPN 钥匙——cmd overlay fabricate 实战

手把手:干掉 MIUI 状态栏那个碍眼的 VPN 钥匙——cmd overlay fabricate 实战

Oct 08 ·
13 Min Read

手把手:干掉 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:

Terminal window
$ settings get secure icon_blacklist
wifi,bluetooth,rotate,location,headset,volume,alarm_clock,mobile,vpn

vpn 明明在里面,图标却纹丝不动。

这篇文章就带你走完整条链路:为什么系统设置不管用 → 图标到底是谁画的 → 三条路怎么死的 → 最后用一条冷门命令收尾 → 怎么让它开机自动生效。


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 后可以确认,里面确实有:

mSlotVpn
mCurrentVpns={}
stat_sys_vpn
com.miui.vpnsdkmanager
currentVpnIconId

但那个短 slot 名列表里,就是没有 vpn。

结论:icon_blacklist=vpn 是个摆设。写着好看,实际不动。

1.2 图标是谁触发的

先看事实。FlClash 的 VpnService 会建一张 tun0 网卡:

Terminal window
$ ip link show tun0
47: tun0: <POINTOPOINT,UP,LOWER_UP> mtu 9000 qdisc pfifo_fast state UNKNOWN ...
link/none
Terminal window
$ 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 出资源表:

Terminal window
$ 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 掉图标显示。结果:

Terminal window
$ 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:

Terminal window
$ ls /product/overlay/*.apk | head
/product/overlay/MiuiSystemUIResOverlay.apk
/product/overlay/GmsSystemUIOverlay.apk
/product/overlay/AospFrameworkResOverlay.apk
...

那我照着做一个不就行了?思路很清楚:

  1. 用 aapt2 编译一个 overlay APK,target 指 com.android.systemui;
  2. 把 4 个 VPN 图标资源替换成透明版本;
  3. --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.systemui
signed with different certificates,
and the overlay lacks <overlay android:targetName>

翻译一下:overlay 和目标签名不一致,而目标没有声明 overlayable。

再查两者的签名:

Terminal window
$ apksigner verify --print-certs MiuiSystemUI.apk | grep DN
DN: ... CN=MIUI, O=Xiaomi ...
$ apksigner verify --print-certs MiuiSystemUIResOverlay.apk | grep DN
DN: ... CN=MIUI, O=Xiaomi ... ← 完全一样
$ aapt2 dump resources MiuiSystemUI.apk | grep -i overlayable
# (空,SystemUI 没有 overlayable 定义)

三条事实叠在一起:

  • SystemUI 用小米平台签名;
  • 系统 overlay 也全是这个签名;
  • SystemUI 没有 <overlayable>,所以只能同签名覆盖。

第三方签不出小米的签名,这条路死。

2.3 路三:cmd overlay 普通 enable —— 连装都装不上

既然 APK 装不上,就试试命令行:

Terminal window
$ 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 里本来就存在、且画出来什么都没有的资源。翻资源表,真就有一个:

Terminal window
$ 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

先做主力那个:

Terminal window
# stat_sys_vpn -> ic_blank
su -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。

没报错。 看看它挂在哪儿:

Terminal window
$ su -c 'cmd overlay dump com.android.shell:vpniconhide' | grep -A3 IDMAP
IDMAP OF com.android.shell:vpniconhide
Paths:
target path : /system_ext/priv-app/MiuiSystemUI/MiuiSystemUI.apk
overlay path : /data/resource-cache/com.android.shell-vpniconhide-xxxx.frro
Mapping:
0x7f081055 -> reference 0x7f08073a (drawable/stat_sys_vpn)

映射正确。 再用 lookup 验证解析结果:

Terminal window
$ su -c 'cmd overlay lookup --user 0 com.android.systemui \
com.android.systemui:drawable/stat_sys_vpn'
@2131232570 -> res/drawable/ic_blank.xml

stat_sys_vpn 现在解析到 ic_blank.xml。图标变空白了,SystemUI 进程还活着,logcat 干净。

3.3 第三步:把另外三个变体也补上

只改 stat_sys_vpn 可能在某些模式(深色、tint)下露馅,所以四个一起处理:

Terminal window
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'

最终状态:

Terminal window
$ 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 第三步的坑:重启就没了

高兴没两天,重启一次,钥匙又回来了。

Terminal window
$ 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 动作里:

Terminal window
sleep 15
IC_BLANK=0x7f08073a
SYS=com.android.systemui
HOST=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_vpn
hide vpniconhide_dark stat_sys_vpn_darkmode
hide vpniconhide_ic stat_sys_vpn_ic
hide vpniconhide_tint stat_sys_vpn_tint

4.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/sh
for 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:$n
done
rm -f /data/resource-cache/*vpniconhide*
pkill -f com.android.systemui

或者只关一个:

Terminal window
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 必读

  1. 0x7f08073a 不是通用值。这是我这台设备上 ic_blank 的 ID。换机型必须自己 dump:

    Terminal window
    aapt2 dump resources /path/to/MiuiSystemUI.apk | grep -iE "ic_blank"

    没有 ic_blank 也不要紧,找一个”空”的 drawable 即可,关键是它必须真实存在。

  2. stat_sys_vpn 的 ID 同理,必须从自己设备的 MiuiSystemUI.apk 里 dump 出来,不能照搬。

  3. 绝对不要指向 0x00000000。那是无效引用,SystemUI 有崩溃风险。这一点是”安全”与”作死”的分界线。

  4. user 0 之外的用户(比如第二空间 user 11):想让那边也生效,把 --user 0 换成对应 user 或再加一条 enable。

  5. Termux 后台执行不可靠。我一开始想用 Termux 做开机脚本,结果发现重启后 Termux 进程被系统置于后台受限状态,su -c 直接报:

    Not allowed to start service Intent ... app is in background

    所以持久化要么交给 MacroDroid,要么放进 Magisk 的 service.sh,别指望 Termux。

  6. 不同 ROM 的图标资源名可能不同。AOSP 原生是 stat_sys_vpn,各家 ROM 可能改成别的名字。找不到就先把 MiuiSystemUI.apk/SystemUI.apk 的所有 *vpn* 资源列一遍。


8. 复盘

这一路的教训其实很朴素:

  1. 先观察,别先下结论。icon_blacklist 有 vpn 不等于它有用,要去看 SystemUI 到底认哪些 slot。
  2. 一条路走不通,要弄清它为什么不通。普通 RRO 失败不是因为思路错,而是卡在「签名」这一环——把原因搞清楚,才可能找到 fabricate 这个绕法。
  3. 安全的边界很具体。不是抽象地“小心点”,而是:别指向 0x00000000,要指向真实存在的 ic_blank;动态 overlay 随时能 disable;先备好还原脚本再动手。
  4. 持久化要选对工具。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 表现可能不同,请以自己的实际环境为准。

Last edited Oct 31