刷过机、解锁过引导加载程序的朋友,大概都在通知栏里见过这条顽固提示:“性能受到影响,要停用。请查看引导加载程序通知”。它不像普通推送那样右滑就消失,重启、清后台、甚至换一张 SIM 卡,它依旧挂在那里,点进去要么跳到一个语焉不详的说明页,要么干脆没有任何反应。我第一次遇到它是在一台刚升级到 Android 11 的测试机上,当时第一反应是某个预装应用在乱推消息,抓着dumpsys翻了一遍才发现,发这条通知的根本不是第三方 App,而是系统自己。
这条通知牵扯的东西其实不少:引导加载程序(bootloader)的解锁状态、Android 11 之后收紧的完整性校验、系统级常驻通知的渠道机制,以及在它背后一整套“通知该怎么发、能不能关、点击之后跳到哪里”的设计逻辑。后面这几件事,不光是刷机玩家会遇到,做 App 推送的、做跨端通知的、甚至做 PC 端提醒模块的同学,迟早都要碰。所以这篇不打算只讲“怎么把这条通知弄掉”,而是把它的来龙去脉、状态读取链路、可选的几种处理路线,以及从开发视角看通知体系时容易忽略的坑,一次讲透。有基础的同学可以直接跳到第 3 章看操作,完全不懂的从第 1 章顺着读也不会卡。
1. 这条通知到底是什么,为什么偏偏在 Android 11 上冒出来
1.1 引导加载程序解锁与系统完整性状态
先把概念捋直。手机开机时,最先跑起来的不是 Android,而是一小段叫引导加载程序的代码,它的职责是校验并加载后面的系统镜像。厂商出厂状态下,这段代码处于“锁定”状态,只肯加载官方签名过的镜像;一旦你手动解锁,它就变成了“谁签名的都认”。方便是真方便,代价是整台设备的信任链从根上就断了。
Android 从 7.0 开始全面推行AVB(Android Verified Boot),到 Android 11 这一代,验证结果会被固化成一个启动属性,常见的就是ro.boot.verifiedbootstate。它的典型取值有三个:green表示完整校验通过,也就是官方状态;yellow表示加载了自定义密钥签名的镜像;orange表示引导加载程序已解锁、校验被跳过。系统里有专门的应用会在启动后读取这个属性,一旦发现不是green,就挂一条通知出来提醒你。
不同厂商的 ROM 对这套逻辑的实现差异很大,所以文案也不统一。有的写“检测到引导加载程序已解锁”,有的写“设备安全性降低”,而这次这条“性能受到影响,要停用。请查看引导加载程序通知”,本质上是同一个来源的另一种措辞。它不是病毒,不是广告,也不是某个 App 的推送,而是系统对自己状态的自我陈述。
1.2 “性能受到影响”影响的到底是什么
这句话读起来很吓人,好像手机马上要变卡,但实际上它说的“性能”跟跑分、帧率关系不大。真正受影响的是依赖硬件级信任链的那一类能力:
| 受影响的能力 | 具体表现 | 原因 |
|---|---|---|
| 高等级 DRM 解码 | 视频类 App 只能放出标清或中清,无法启用最高画质 | 解锁后 Widevine 安全等级会从 L1 掉到 L3 |
| 支付与金融类校验 | 部分 App 启动时提示环境异常或直接拒绝运行 | 完整性校验结果不再可信 |
| 系统在线更新 | 部分机型无法接收官方 OTA 或更新失败 | 更新包校验依赖锁定状态 |
| 部分厂商的云服务 | 云同步、查找设备等功能可能被限制 | 厂商策略绑定设备可信状态 |
所以“性能受到影响”更像是一个笼统的提示语,厂商用它来概括上面这一串连锁反应。实测下来,日常刷视频、聊天、打游戏的流畅度基本没变,真正被卡住的是那几项对安全性敏感的功能。想验证一下也简单,连上电脑跑一句:
adb shell getprop ro.boot.verifiedbootstate adb shell getprop ro.boot.flash.locked adb shell getprop ro.boot.veritymode如果第一条返回orange,第二条返回0,那基本可以确定这台机器处于解锁状态。想看得更全,直接过滤一遍:
adb shell getprop | grep -i -E "boot|verity|lock|warranty"注意:不同芯片平台、不同厂商的属性命名不完全一致,比如有的机器用
ro.secureboot.lockstate,有的还会多一个记录解锁次数的属性。上面这几条读不到不代表没解锁,以能不能刷入非官方镜像为准,属性只是参考。
1.3 为什么它是常驻、不可清除的通知
普通通知用NotificationManager发出来,用户可以右滑清掉。这条不一样,它通常带着FLAG_ONGOING_EVENT和FLAG_NO_CLEAR两个标记,前者告诉系统“这是个正在进行的事件”,后者直接禁用了滑动清除。这类通知的设计初衷是会话类、导航类场景,比如正在通话、正在导航,用户不该误清。系统把它用在设备状态提醒上,逻辑上也说得通,只是体验上确实够烦人。
还有一个细节容易被忽略:Android 8.0 之后所有通知都必须归属于某个通知渠道(NotificationChannel),渠道一旦创建,它的名称和描述只有应用自己能改,用户只能在系统设置里调它的重要级别或者整个关掉。这条提醒挂在哪个渠道上,直接决定了你能不能靠“关渠道”把它彻底静音。有些厂商把渠道名字起得很有误导性,比如叫“系统优化提醒”,你根本猜不到它是干这个的。所以第 2 章我们会先讲清楚怎么把这条件通知的“户口”查出来。
2. 从开机到通知栏:一条状态是怎么被系统读出来的
2.1 启动链上的验证与属性固化
整条链路其实很短,但每一环都有讲究。开机时,引导加载程序做两件事:一是校验下一级镜像的签名,二是把自己的状态写进启动参数。内核起来之后,这些参数会被解析成ro.boot.*系列属性,固化在属性服务里。因为前缀是ro,意味着它们是只读的,运行时改不了,只能重启后重新指定,这也是为什么你改不了这个状态——除非动引导加载程序本身。
AVB 2.0 里还有一个vbmeta分区的概念,里面存着各个分区的哈希描述和签名信息。解锁状态下,vbmeta里的校验可以被跳过(常见做法是刷入一个禁用校验的 vbmeta 或带--disable-verification参数),这就是为什么有些机器解锁后依然能正常开机。但跳过校验这件事本身会被记录下来,系统一读就知道。所以这里没有“伪装成官方”这种操作空间,状态是事实,不是可以随手擦掉的标记。
2.2 系统应用读属性、发通知的链路
属性固化之后,会有系统级的应用或服务在启动完成后去读它。这一步通常发生在开机广播之后,所以你看到的通知往往是开机几十秒才出现,而不是立刻就有。发通知的代码逻辑大概是这样:
// 简化示意,不同厂商实现差异很大 String state = SystemProperties.get("ro.boot.verifiedbootstate", "green"); if (!"green".equals(state)) { NotificationChannel channel = new NotificationChannel( "bootloader_status", "设备状态", NotificationManager.IMPORTANCE_LOW); nm.createNotificationChannel(channel); Notification n = new Notification.Builder(ctx, "bootloader_status") .setSmallIcon(R.drawable.ic_warning) .setContentTitle("性能受到影响,要停用") .setContentText("请查看引导加载程序通知") .setOngoing(true) // 变成常驻 .setFlag(Notification.FLAG_NO_CLEAR, true) .build(); nm.notify(TAG_ID, n); }代码本身没什么玄机,关键在于两点:一是渠道的重要性级别,二是是否带 ongoing 标记。前者决定了它会不会弹到屏幕顶部变成悬浮通知(heads-up),后者决定了你能不能划掉。看明白这两行,你就知道为什么“点一下停用”这种动作有时候只是把这个渠道降级,而不是真的解决问题。
2.3 通知渠道、悬浮通知与“停用”按钮的真实含义
“要停用”这几个字最容易让人误解。它并不是在说“请把这个功能停用”,而是厂商给这段提示配的一个按钮文案,点下去一般有两种结果:一种是跳到“开发者选项”页面,暗示你去关掉某些调试开关;另一种是直接把这个提示渠道的重要级别降到最低,通知从此不弹横幅,但依然静静躺在通知栏里。换句话说,点“停用”多数情况下只是让它闭嘴,不是让它消失,更不是把设备恢复成官方状态。
顺带说一句悬浮通知。它对应的就是IMPORTANCE_HIGH加上全屏 Intent 那套机制,在 Android 11 上,使用全屏 Intent 需要声明USE_FULL_SCREEN_INTENT权限,并且系统会对非通话、非闹钟类场景做限制。有些团队做“紧急公告”时喜欢用这种大通知来强提醒,实测下来在 Android 11 上成功率并不稳定,用户只要在设置里关掉渠道,或者把通知权限收回,就完全收不到了。强提醒这件事,靠系统通知本身是保证不了的,这点后面第 4 章还会展开。
3. 三种处理路线:静音、屏蔽、回滚
3.1 路线一:只求安静,把通知降级
这是最省事、风险最低的做法,适合只是嫌它烦、并不打算改变设备状态的人。操作路径是:长按这条通知,系统会弹出通知设置入口,进去之后你会看到它所属的渠道名称,把重要级别从“高/默认”改成“静默”,或者把“允许打扰”关掉。改完之后圈栏里还是会有一条,但不再弹横幅、不再响铃,也不会在锁屏上强占位置。
这里有个小技巧:如果长按之后只给了“关闭通知”而没让你选级别,说明这条通知走的是应用级开关,你可以先把整个应用的通知关掉,再回去逐个渠道打开你需要的那些。系统应用的渠道通常在“应用信息 → 通知”里能看到完整列表,名字五花八门,挑的时候认准跟你看到的那条文案对得上的那个。
提示:把渠道降级之后,部分机型的引导加载程序提醒会换个渠道再来一次。这时候别急着骂,重复上面的操作,把所有相关渠道都调一遍,基本就安静了。我自己的那台机器一共调了两个渠道,之后半年没再弹过。
3.2 路线二:adb 屏蔽渠道与通知权限
如果你连“通知栏里挂着一条”都不能忍,可以用 adb 从系统层面把它的通知权限摘掉。第一步是找出到底是谁发的通知:
adb shell dumpsys notification --noredact | grep -i -E "pkg=|channel=" | head -50带上--noredact是为了让输出不被脱敏,能看到真实的包名和渠道 ID。找到目标包名后,可以用 appops 直接掐掉它的通知能力:
# 关闭通知权限,Android 13 及以上用这个 appop 最直接 adb shell cmd appops set <包名> POST_NOTIFICATION ignore # 想恢复的时候 adb shell cmd appops set <包名> POST_NOTIFICATION allow在 Android 11 上,POST_NOTIFICATION这个 appop 并不是所有机型都支持,因为运行时通知权限是 Android 13 才引入的。更通用一点的办法是直接停用对应的系统组件:
adb shell pm disable-user --user 0 <包名>或者只停用它下面负责发通知的那个 service,粒度高一点,副作用也小一点:
adb shell pm list packages -d # 看看已经被停用的都有谁 adb shell pm list packages | grep -i boot注意:
pm disable-user是真的把组件停掉了,不是让它闭嘴。系统级应用被停用之后,可能出现设置项打不开、状态栏图标异常、OTA 无法进行等连带问题。动这一层之前,一定先记下完整的包名,出问题用pm enable原样恢复。另外这些操作在部分厂商 ROM 上会因为权限收紧而失败,报SecurityException或Permission Denial,这时候别硬刚,回到路线一。
3.3 路线三:从根上解决,重新锁定引导加载程序
如果你希望彻底恢复官方状态,唯一正路是把引导加载程序重新锁上。前提是设备必须已经刷回官方原厂固件,并且版本和机型完全对得上。步骤大体如下,但每个厂商的命令差异不小,务必先查自己的机型文档:
# 进入 fastboot 模式后 fastboot devices fastboot flashing lock # 部分老机型用 fastboot oem lock锁回去之后,ro.boot.verifiedbootstate会恢复成green,那条通知自然就没了,前面提到的 DRM、支付类校验也会陆续恢复正常。
代价必须说清楚:锁定操作几乎一定会触发一次数据清除,等同于恢复出厂设置,照片、聊天记录、账号登录状态统统归零,所以动手前先做完整备份。另外,如果机器上跑的不是官方固件,锁定后可能直接无法开机,这就需要重新解锁救回来,一来一回数据还是保不住。还有个现实问题——解锁这个动作本身在很多厂商的记录里是留痕的,锁回去也不会自动抹掉,这属于厂商政策范畴,需要自己权衡。
我个人的做法是:手上这台测试机就一直保持解锁状态,接受那几项功能不可用,但把通知静音掉;真正要用的主力机从来不碰引导加载程序。测试用机和日常用机分开,是省掉一大堆麻烦的最简单办法。
4. 开发者与测试同学更该关注的事:自测包的通知差异
4.1 测试机总在提示,量产机为什么没有
很多人是在做 Android 开发时撞上这条通知的,原因是团队的测试机大多解锁过、刷过自定义镜像,方便抓日志和调内核。这时候容易产生一种错觉:明明代码没动,怎么测试机上一直弹提示,用户那边却安安静静?答案就在引导加载程序状态上,它跟你的应用代码完全无关。
这件事的实际影响是排查干扰。测试机上同时挂着系统提醒、你自己应用的通知、还有推送 SDK 的通知,肉眼根本分不清谁是谁。我一般的做法是先把系统那些常驻通知静音掉,再用dumpsys notification按包名过一遍,确认自己应用的通知确实发出去了、渠道确实建对了。特别是 Android 8.0 之后,忘记createNotificationChannel会导致通知静默丢失,代码不报错、日志没异常,新手最容易在这里卡半天。
顺便提一句跨引擎的场景,比如在 UE4 里通过 JNI 桥接调用系统通知,很多时候只是传了一个通知 ID 和一个字符串就完事,渠道那一步直接省了。结果就是 Android 8.0 以上完全收不到通知,8.0 以下反而正常,很容易被误判成“高版本兼容性问题”。只要走NotificationManager,渠道就必须建,这是硬性要求。
4.2 通知栏消息的送达链路与异步通知验签
把视野放宽一点,“通知”这个词在工程里其实有两种完全不同的含义。一种是给人看的通知栏消息,比如推送 SDK 下发到手机上的那条横幅;另一种是给系统看的通知,也就是服务端之间的回调消息,比如订单支付完成后第三方异步回调你,告诉你“这笔单子成功了”。前者的关键指标是到达率和点击率,后者的关键是不能被伪造,所以必须做验签。
异步通知验签的通用做法是:服务端把参数按约定顺序拼接,用双方共享的密钥做一次 HMAC,或者用对方给的公钥做一次签名校验,比对通过才认为这条通知有效。常见踩坑点有三个:一是拼接时把空值参数漏掉了,导致签名对不上;二是编码不统一,一方用 UTF-8 一方用 GBK;三是没做幂等,同一个回调被重复推送时业务被重复执行。这三个坑跟 Android 版本没关系,但带来的困扰往往比那条引导加载程序通知大得多。
至于通知栏消息这一侧,链路一般是这样:业务服务端调用推送服务下发,推送服务把消息投递到系统级通道,系统再交给目标应用展示。点击之后跳到哪里,靠的是消息里携带的跳转标识。以鸿蒙设备上的场景为例,如果想做到“点击通知直接进到 App 内某个页面”,需要在消息里带上目标页面的标识参数,并且端侧要同时处理冷启动和热启动两个入口——冷启动时要等应用初始化完成再跳,热启动时要在回调里解析参数。只处理其中一个入口,就会出现“有时候能跳、有时候跳到首页”的诡异现象。
同理,用 uni-push 这类方案给 App 推通知栏消息时,透传消息和通知栏消息的处理路径完全不同:通知栏消息由系统直接展示,应用拿到的是点击事件;透传消息则要应用自己建通知。混用这两种模式,是“线上有点击、线下没点击”这类数据对不上的常见原因。
4.3 跨端提醒机制横向对比
现在做产品很少只做一端,通知这件事在各端的机制差异相当大,放在一起看看会更清楚:
| 平台 | 通知机制要点 | 常见限制 | 排查入口 |
|---|---|---|---|
| Android 11 及以上 | 必须建渠道,渠道级别决定是否弹横幅 | 渠道被用户关闭后收不到;全屏 Intent 受限 | 应用信息 → 通知 → 渠道列表 |
| 鸿蒙设备 | 推送消息点击可携带跳转参数 | 冷启动与热启动需分别处理 | 系统设置里的通知管理 |
| Windows 桌面端 | 走系统 Toast,受“通知和操作”设置影响 | 专注助手开启时全部静默 | 系统设置 → 系统 → 通知 |
| Linux 服务器 | 登录时由系统打印过期提醒,如密码到期 | 属于系统行为,应用层关不掉 | chage -l <用户>查看策略 |
最后一行稍微展开说。Linux 上那条“密码即将过期”的提醒,机制跟这次的引导加载程序通知有点像——都是系统在启动或登录阶段读取某个持久化状态,然后主动告诉你。它由chage配合/etc/login.defs里的PASS_MAX_DAYS等参数控制,到期前若干天开始提醒,到期后强制改密。想看当前策略:
chage -l username它同样属于“系统说了算”的那一类,应用层没有开关可以关掉。做跨端通知模块的同学最好记住这条规律:凡是来源是操作系统的提醒,先去找系统侧的配置入口,别在应用代码里折腾。
5. 常见问题与排查速查表
实际操作中遇到的问题五花八门,把高频的几条整理成表,遇到时可以直接对号入座:
| 现象 | 可能原因 | 处理思路 |
|---|---|---|
| 通知划不掉 | 带 ongoing 与 no-clear 标记 | 长按进设置,调低渠道级别 |
| 点“停用”后过几天又出现 | 只是降级了渠道,状态没变 | 重复调渠道,或走第 3.3 路线 |
| 关闭应用通知后其他提醒也没了 | 关的是应用级开关而非渠道 | 重新打开应用通知,逐渠道调整 |
| adb 命令报权限拒绝 | 厂商 ROM 收紧了权限 | 换用系统设置界面操作 |
| 恢复出厂设置后仍有提示 | 引导加载程序状态没变 | 确认是否已重新锁定 |
| 应用通知在测试机收不到 | 未创建通知渠道 | 检查渠道 ID 是否与代码一致 |
再说几条文档里不会写、但实际很有用的经验。
第一条,改任何东西之前先截图记录。渠道名称、包名、命令输出,全部存一份。系统设置这种地方,改乱了很难凭记忆还原,有截图能省掉大量试错时间。
第二条,别在主力机上做实验。解锁、停用系统组件、刷 vbmeta 这类操作,失败成本可能是整机无法开机。手上备一台便宜的测试机,几百块的投入能换来极大的心理安全感。
第三条,dumpsys是你最好的朋友。不管是查通知来源、查渠道状态还是查某条通知为什么没显示,adb shell dumpsys notification基本都能给出答案。刚开始输出很吓人,用grep顺着包名和渠道 ID 过滤几次就熟悉了。
第四条,推送类和系统类通知分开治理。我见过不止一个团队,因为系统提醒太吵,干脆在设置里把整个应用的通知关了,结果自家业务推送一起被掐掉,数据断崖式下跌还找不到原因。养成习惯:系统来源的提醒单独处理,业务推送单独维护渠道,两者互不干扰。
第五条,验签失败先查参数拼接,再查密钥。绝大多数签名不通过都是参数顺序、空值处理、编码格式这三件事,真正密钥配错的比例反而很低。排错顺序搞反了,会在密钥上浪费很多时间。
最后分享一个我自己一直在用的做法:把测试机上的系统级常驻通知统一静音,然后在每次排查推送问题时,先用dumpsys确认这条通知的包名和渠道 ID,跟预期对上了再往下查。这个习惯听起来很笨,但它帮我省掉的返工时间,远比调那几行 adb 命令多得多。