资讯详情

资讯详情

深入Android 10通知服务NMS:渠道、排名与免打扰机制解析

Android 10 的通知体验和 8、9 差别很大通知渠道NotificationChannel、通知分组、免打扰策略在 10 上已经趋于成熟但真正让我差点被逼疯的不是怎么调 API而是系统侧那个负责所有通知生杀大权的服务——NotificationManagerService业内直呼 NMS。App 发了一条通知状态栏没影同一个应用用户把通知渠道调成了静默系统却还是弹出了声音后台进程明明没死通知就是排到了最后……这些奇奇怪怪的现场几乎都能在 NMS 这一层找到解释。如果你跟我一样平时会翻 framework 源码、做 ROM 定制、优化系统通知体验或者单纯只是想搞清楚我的应用通知为什么总被系统收拾这篇分析就是为你准备的。下面我把 Android 10 上 NMS 的判断逻辑、数据结构、排名规则、免打扰限制这些核心内容按我实际调试时的思路逐层拆开。1. NMS在Android 10中的定位与启动流程1.1 先搞清楚NMS到底是个啥很多人会把 NotificationManager、NotificationManagerService、NotificationListenerService 三个概念搞混。我刚开始看源码时也绕了一大圈。简单说App 层调用的 NotificationManager 是一个客户端代理内部持有 Binder 对象指向系统进程里的 INotificationManager 接口真正干活的实现就是 NotificationManagerService。意思就是通知从应用进程到系统进程之间走的是跨进程的 Binder 调用而 NMS 就是那个坐在系统进程里的大管家负责接收、验证、排序、存储、分发、展示、提醒……几乎所有跟通知生命周期有关的事。而 NotificationListenerService 是监听器像系统 UI、手环厂商、第三方通知管理 App都是通过它来订阅通知事件流。这不改变 NMS 的核心行为但如果你在定制 ROM经常会跟它打交道。这一章必须先把谁负责什么理清楚后面分析代码时才不会被各种类名绕晕。顺带说一句Android 10 的 framework 里已经把很多服务下沉到 SystemServiceManager 统一管理NMS 不再是孤立的一个类它还和 ActivityManagerService、AppOpsManager、PowerManagerService 等有大量交互。真正排查问题时往往需要跨服务联动分析这也是为什么我后面会反复提到 dumpsys 和各种系统日志。1.2 从SystemServer看它的出生在 Android 10 中NMS 是在 SystemServer 的 startOtherServices() 里被创建并注册的。代码大致是这样// frameworks/base/services/java/com/android/server/SystemServer.java private void startOtherServices() { ... if (!disableSystem) { ... if (!disableNotificationManager) { traceBeginAndSlog(StartNotificationManager); mSystemServiceManager.startService(NotificationManagerService.class); traceEnd(); } ... } }注意它是通过 SystemServiceManager.startService() 这样一条标准的系统服务启动路径进来的所以会走 SystemService 的标准生命周期构造、onStart、onBootPhase。实际点说NMS 的构造函数里会创建自己专用的 HandlerThread 和 Worker 线程把很重的 IO 或定时操作比如清理过期通知、重新排名放在后台线程里跑不会阻塞 system_server 的主线程。我当时跟项目时最有印象的是 NMS 构造时会初始化几个硬件相关的 Light也就是那颗通知呼吸灯和 AttentionLight。很多板子根本不带呼吸灯但还是会注册代码里也有不少条件判断。你在移植 AOSP 到非标准设备时这些灯相关代码经常要仔细排查否则明明没有灯设备系统还在那里循环驱动极个别情况会引起休眠异常。1.3 多Handler线程模型NMS 内部有 mHandler、mWorker、mRankingHandler 好几条线索。有必要说下它为什么这么设计。消息队列是异步的如果所有回调都压在 system_server 主线程一旦有应用频繁发通知或者通知排名需要频繁计算主线程很容易出现卡顿甚至 ANR。Android 10 的 framework 里已经开始强调不要让服务主线程替 App 干活NMS 的 mRankingHandler 专门负责通知排序mWorker 处理和通知服务相关的后台任务比如清理无用的 NotificationRecord、处理延迟操作等。看代码的时候有一条关键经验你如果想追踪一条通知从发出到展示在状态栏的完整链路不能只盯一个类要把 enqueue → handleRankingReconsideration → notifyPostedLocked → maybeSendForegroundNotificationRemoved 这条链路串起来。我最早排查通知有时候弹出来、有时候不弹的 bug 时就是没注意 mWorker 和 mHandler 之间的调度关系导致顺序压根对不上。2. 通知生命周期从 notify() 到状态栏2.1 enqueue 的那一瞬间App 里最常见的写法是NotificationManager nm (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(test_tag, 1001, builder.build());这一行最终会走 Binder 到 system_server进入 NMS 的 enqueueNotificationWithTag()。Android 10 源码里这个方法做了非常多前置检查包名是否允许、uid 是否匹配、通知是否已过时、是否处于免打扰模式、App 是否被冻结、渠道是否存在…… 一系列检查通过后才会真正把这条通知包装成 NotificationRecord 放进 mNotifications 这个以包名:id:tag为 key 的 ArrayMap 里。记得当时有个线上 case某应用在前台发通知没问题切后台以后立刻不弹查了半天是 App 进程被系统冻结导致 PendingIntent 无法启动 Activity。这里 NMS 里有一个 mAppOps 相关权限检查逻辑还有一整套包冻结判断初学者很容易忽略。2.2 NotificationRecord 的核心字段很多人在读 NMS 时最困惑的就是往 mNotifications 里加各种包装类。告诉你我实际调试时最常看的几个字段pkg哪个应用发的通知uid调用方的 uid用来判断权限归属notification原始的 Notification 对象里面基本全是你 App 的设置channel对应的渠道对象重要性、震动声音都在这score排名分数代表了这条通知在当前排名体系里的权重sortKey排序 key决定状态栏里的排布如果你用adb shell dumpsys notification看到一堆 NotificationRecord第一眼找不到这些字段很正常因为 dump 输出里是压缩格式需要对着源码看字段名。字段太多的时候我一般直接 grep 出目标那一条然后逐字段对着源码注释看比人脑硬记靠谱得多。2.3 渠道Channel在Android 10的重要性Android 8.0 已经引入了 NotificationChannel但到 Android 10 才被真正当作基础设施在用原因在于排名、免打扰、通知分组这些都是围绕渠道在做策略。渠道的重要性IMPORTANCE_HIGH、DEFAULT、LOW、MIN、NONE几乎决定了通知的表现形式。有个坑必须提醒用户一旦修改了渠道重要性App 侧后续调用 createNotificationChannel 不能静默覆盖回默认值代码里会有比较逻辑如果新值比用户设置低系统会忽略你的修改。这就是为什么很多 App 在升级后想偷偷调回默认重要性根本不生效。我见过不少开发在这里反复确认为什么我改了 channel 代码线上用户还是旧行为根因就在这里。2.4 分组的逻辑通知分组NotificationGroup在 Android 10 也有演进。一条通知可以通过 setGroup(groupKey) 归组组摘要 setGroupSummary(true) 可以让系统在状态栏用一条汇总代替多条子通知。这里实际调试时的难点是如果你设置了 groupKey 但没有正确设置 participants/children排序经常会出现摘要反复跳动的 bug。厂商 ROM 在这个问题上更是千奇百怪。特别是用了前台服务通知Foreground Service Notification时组摘要的表现很容易被误判。我曾遇到一个 case音乐类应用播放时不断更新进度结果状态栏里组摘要和进度通知来回抢位置看着就像在闪烁。最后定位到是 groupKey 在多次 update 时发生了不一致系统把它当成两组通知在处理。3. 排名系统与展示顺序3.1 RankingHelper 的职责Android 10 里有一个 RankingHelper负责计算每条通知的分数和 rank。它内部维护了一个 NotificationComparator这套排序逻辑不是单纯的按时间正序而是综合了渠道重要性、用户对应用的直接反馈、是否被归类为提升promoted等因子。大概的流程是每次有通知增删改、或者渠道重要性变化时NMS 会调用 mRankingHandler.requestReRank()然后 RankingHelper 重新计算所有 NotificationRecord 的 score并按序排好最后通过 notifyRankingUpdateLocked() 把变更通知给监听器。如果你在自定义 ROM 或者做桌面级通知 UI可以直接在 NotificationComparator 里改排序权重但要注意 Android 10 加了几个保护逻辑改得不好可能导致状态栏通知重复跳位。之前我试着把媒体通知的权重调高结果音乐应用的通知几乎永远置顶用户打电话过来投诉说看不到消息通知这就说明权重不能拍脑袋改。3.2 排名分怎么算出来的这部分我不贴大段代码挑重点说。每条通知有一个 rankingScore 和 rankingTimeMs前者表示业务上的排名分数后者用于时间降序的兜底。在实际展示时NMS 先把 score 大的放前面score 相同再用时间时间相同再比 sortKey。有同学问为什么我设置了高重要性的渠道通知还是排在很后面大概率是你的 App 发通知时用了同一个 tag 反复更新系统会认为这是同一条通知的更新而不是新的通知从而降低了它在用户注意力上的权重。这种细节文档上找不到全是排查时一条条比对日志看出来的。在 Android 10 的源码里还有一个相关性判断如果通知类别设置成 CATEGORY_PROGRESS系统会认为它属于进度型通知排名权重也会自动降一档。你不设置正确的 category不仅影响排名还可能在定制 UI 上出现奇怪的动画或图标行为。4. 免打扰、电源与权限的限制4.1 ZenMode 免打扰在 Android 10 的策略Android 10 的免打扰Do Not Disturb被分成了三个模式完全关闭、仅闹钟、全部禁止。系统通过 ZenModeHelper 来管理状态并且有一套复杂的规则引擎包括时间计划、事件规则、例外联系人等。免打扰模式对通知的影响非常直接在全部禁止时非例外渠道的通知会被压制并且状态栏会有特殊的提示图形。NMS 里会调用 isNotificationAllowedInZenMode() 来判断这条通知是否被允许弹出。这里最容易忽略的一点是音量和振动有时候会被静音但通知仍然进入状态栏除非你去查看通知流日志否则很难察觉。我自己在复现问题的时候习惯性先看当前系统是不是处于免打扰因为十次有八次用户说通知不响了最后都是免打扰或者音量问题背锅。别一上来就翻源码逻辑先确认环境状态。4.2 与电源相关的限制Android 10 对后台通知的管控明显加强了。像 App Standby Buckets、电池优化白名单、进程冻结这些机制会间接影响通知的到达率。NMS 里有专门针对非活跃应用的通知隔离逻辑如果一个应用长时间未被用户使用系统会将其通知归类为低优先级甚至直接丢弃。这是很多人反应为什么我的 App 后台收不到推送的重灾区很多时候问题不在推送通道而在系统侧的通知限制策略。某开发者的应用做成了 Todo 类用户下载后几天没打开后台通知就常常丢这就是被系统分进了 Standy Buckets 的 Restricted 桶。优化方向是引导用户调高应用的后台优先级或者在合适的时机申请电池优化白名单而不是在推送通道层面反复折腾。4.3 权限那块POST_NOTIFICATIONS 还没入运行时这里要特别提醒Android 10 还没有把通知权限变成运行时权限也就是说你不能像申请定位那样向用户申请通知权限。系统默认所有应用都具备发通知的能力但用户可以通过系统设置关闭某个应用的允许通知。NMS 里是通过 checkNotificationPermission 等逻辑判断应用是否有权限。这跟 Android 13 以后差别很大。如果你在做手机卫士、通知管理类应用要注意这些差异尽早把目标平台和要求调整好。Android 10 时代很多厂商还会默认把推送和通知权限打包到自带管家应用里第三方应用只能在用户手动操作后才能正常弹出通知这类环境差异在你做兼容验证时特别恼人。5. 实操调试方法与关键经验5.1 dumpsys notification 的正确姿势我最常用的调试方式是在每次复现场景后执行adb shell dumpsys notification | tee /tmp/notification_dump.txt然后用文本编辑器搜索包名或通知 id。输出里的 NotificationRecord 行会告诉你这条通知当前存储在 NMS 里还是已经被移除以及它的 rankingScore、channel 的 importance、flags 等。如果记录还在但状态栏不显示多半是 UI 侧的问题或者渠道重要性为 NONE如果记录都没了说明通知在 NMS 层就被拦截了重点观察mDisabledNotifications、mEnqueuedToClearNotificationAlerts等字段。我一般还会把 dumpsys 输出里相关的包名行单独截出来做成一个小的速查表格方便在不同版本之间对比。很多 bug 是厂商基于 AOSP 改了策略之后才出现的别人测不出来就是因为原厂没有那些策略差异。5.2 日志观察技巧在 Android 10 上系统日志里会出现大量与通知服务有关的输出关键是过滤adb logcat -s NotificationManagerService:V NotificationService:V NotificationListeners:V我一般还会加上ActivityManager:I来确认应用进程状态。如果通知确实发到了 NMS但状态栏没弹大概率在 UI 一侧反过来如果日志里根本没有 NMS 的 enqueue 记录则问题在前面链路就要去查 Binder 或应用进程死没死。有一点要提醒日志太多的时候不要只看 Event 标签要结合 pid 和 uid 一起看。有些厂商定制 ROM 会把通知相关日志打得很碎单纯搜包名可能搜不到需要搜 uid 或者 notification id这是实操经验不是理论推导。5.3 断点调试 NMS源码调试更适合做系统开发的人。我用 IDE 或者直接改 AOSP 源码添加 Log 的方式比较多。推荐在 enqueueNotificationInternal() 的入口、addNotification() 输出相关日志时加断点沿着调用栈就能看到从 Binder 到 ranking 的完整路径。如果你是在模拟器里调试直接把源码替换后编出 userdebug 包速度会快很多。如果是真实设备记得保留 dumpsys 和 logcat 的权限否则你改了代码也看不到运行日志。这类调试需要一定耐心因为 system_server 进程一旦挂了会直接重启断点不能在关键锁住的方法上停留太久。5.4 工具组合事半功倍dumpsys notification 是主力logcat 过滤更快的定位systrace 用于看 UI 渲染卡顿binder 抓取 Binder 调用耗时。这个组合用下来绝大多数通知相关疑难杂症都能搞定。我特别想强调 binder 抓取这一步很多人不知道adb shell bd可以查看当前 Binder 调用状态。当通知发送出现明显延迟时这个工具能帮你判断是 Binder 排队太久还是 NMS 内部某个锁在阻塞。6. 常见问题与排查技巧实录6.1 我发了一条通知状态栏没反应把上面的排查顺序背下来先查渠道、再查权限、再查进程、最后看 NMS 日志。大概率不是 NMS 本身的问题而是以下几个之一渠道重要性被用户调成了 NONE应用被用户关掉了允许通知应用被系统冻结或处于低电耗模式通知被免打扰拦截。下面是一个我整理过的速查表实际定位问题时帮助很大现象检查点常见根因状态栏完全不显示渠道重要性、应用通知权限渠道为 NONE 或权限被关有通知记录但不提示音渠道声音设置、DEFAULT_ALL 覆盖setDefaults 覆盖了渠道配置通知排在很后面rankingScore、tag 重复更新同 tag 高频更新导致权重下降组摘要反复跳动groupKey、participantsgroupKey 不一致后台通知丢失App Standby、Doze、电池优化系统后台限制策略6.2 通知有声音但渠道明明设了静音反过来也有。渠道设的是 IMPORTANCE_LOW但实际却出声了通常是因为 App 调用 notify 时用了 setDefaults(Notification.DEFAULT_ALL)这会覆盖渠道中的声音设置。Android 10 上依然是这个逻辑要小心。我在给某个即时通讯应用做适配时遇到过用户已经在系统设置里把渠道改成静音但来消息还是响就是因为 App 在 code 里强制 setDefaults。其实用户侧看到的静音只是渠道重要性声音和震动是否跟随 DEFAULT_ALL 要看系统实现不同厂商处理还不完全一样。6.3 分组摘要一直闪跳组内通知太多时摘要会被反复重排。建议仔细检查 groupKey 的值是否一致尤其是使用动态前台服务通知时如果摘要和子通知的 groupKey 没有保持稳定系统可能时不时把它们拆成多个组。我的做法是把 groupKey 固定写死成一个常量不要用时间戳或随机数拼接除非你真的想每次更新都创建新组。很多通知闪跳其实不是 NMS 的问题而是数据源侧 key 不稳定造成的改完 key 就安静了。6.4 通知总是排到最后重点看 rankingScore 和 rankingTimeMs。App 如果用同一个 tag 频繁更新系统会认为这是更新而不是新通知权重自然低。如果这条通知真的需要每次提醒用户可以考虑换唯一的 tag或者不要把同一个 tag 复用太多次。另一个风险点是如果应用长期在后台被系统标记为不活跃那么即便你换了 tag排名依然会被系统性压低。6.5 修改 NMS 源码后的编译和测试问题改动后需要编译 services 相关的模块常在产物路径 services.jarpush 后最好做一次冷启动因为很多服务状态是在启动时初始化的。改动 RankingHelper 后还要验证一下 dumpsys 里的排序是否符合预期。另外提醒一句改 system_server 相关代码一定要保留 userdebug 或 eng 版本的现场否则线上 devices 上你抓不到足够日志。我有一次只改了排序规则结果在发布版本上出现崩溃就是因为没做足够的并发验证。NMS 内部很多方法都加了 synchronized改排序逻辑时稍不注意就会造成死锁或性能回退。说到底NMS 在 Android 10 里更像一个分诊台——它决定通知该不该出现、以什么形式出现、出现在什么位置。如果你想深入了解它我的建议是别上来就啃整份源码先把一条最简单的通知从 notify 到状态栏的链路走通再逐步扩展。那些逻辑分支和边界情况多是在实际设备、真实用户数据里才会冒出来。你可能会在某次排查后发现真正出问题的地方往往不是你以为的渠道、浮层那个组件而是被系统服务悄悄替换掉的 NotificationRecord 里的某个字段。这种体验还是在反复踩坑之后才感受最深的。如果有机会也建议你把这套分析思路迁移到 Android 新版本上看看差异比想象中大。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →