在这里插入图片描述

每一个 Android 应用都会借助 NotificationManager.notify() 来推送消息、提醒和更新。但在幕后,从应用代码到状态栏里的小图标或者悬浮横幅,究竟发生了什么呢?在这篇博客里,我们会结合视频里的关键环节,深入剖析通知推送流程的每一个阶段。

还可以在 YouTube 上观看视频讲解。

1. 为何要深入探究?

你大概率已经调用过 notify() 数百次了——可你有没有好奇过之后会发生什么?搞懂这个流程,能帮你排查显示问题、优化性能,还能遵守电池和隐私方面的限制。

2. 入口——notify() 方法

你的应用会像这样创建并发布一条通知:

val notification = NotificationCompat.Builder(context, "chat_channel")
    .setContentTitle("新消息")
    .setSmallIcon(R.drawable.ic_chat)
    .setContentText("来消息啦~")
    .build()
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(1001, notification)
  • 外观 API:NotificationManager.notify() 运行在你的应用进程中(具体在 NotificationManager.java 里)。
  • Binder 交接:在底层,它会调用 INotificationManager.enqueueNotification(),从而进入 system_server 进程。

3. 核心数据结构与 Binder

组件 作用
INotificationManager.aidl AIDL 接口,定义了 enqueueNotification()cancelNotification() 等方法。
NotificationRecord 内部记录,包含 uid、包名、channel、importance、user 等信息。
StatusBarNotification 可序列化的包装类,发送给 SystemUI,包含通知的关键数据。

4. 完整通知流程解析

在这里插入图片描述

应用 → NotificationManager API(notify(tag, id, Notification)

你的应用通过 NotificationCompat.Builder 创建一个 Notification,然后调用:

NotificationManager.notify(tag, id, notification);

这是一个框架外观类——它是一个很薄的客户端包装器,会把你的有效载荷打包并为进程间通信(IPC)进行编组。

NotificationManager API → NotificationManagerService

在底层,这个 API 会调用 AIDL 接口 INotificationManager.enqueueNotification(...)。这个调用会跨越进程边界(从你的应用进程通过 Binder IPC 进入 system_server)。

NotificationManagerService 内部,会执行 enqueueNotificationInternal() 方法,主要做这些事:

  • 验证:确保调用应用的 UID 和包名匹配。
  • 渠道与重要性解析(Android 8.0+)。
  • 包装:把新通知转换成内部的 NotificationRecord,添加发布时间、用户 ID、持续标志等元数据。

NMS → 通知存储

NMS 会把 NotificationRecord 写入磁盘上的 SQLite 表(notification_records),这样就算发生重启或者服务崩溃,通知也能留存下来。

NMS → StatusBarService/SystemUI

持久化完成后,NMS 会调用 StatusBarService.enqueueNotificationRecord(...)StatusBarService 会把记录打包成 StatusBarNotification,然后调用 SystemUI 中已注册的回调函数 INotificationListener.onNotificationPosted(...)。接着,SystemUI 会决定是显示状态栏图标、通知栏条目,还是悬浮横幅。

NMS → NotificationListenerService(notifyListeners(NotificationRecord)

可选的是,NMS 还会把新记录广播给任何 NotificationListenerService 实现(比如 Wear OS 配套应用或者辅助功能服务),支持像可穿戴设备同步或者自动回复这类外部响应。

SystemUI → 展示给用户(图标、通知栏、悬浮横幅)

SystemUI 会实例化这些组件:

  • 状态栏里的小图标。
  • 通知栏里带有操作按钮的可展开行。
  • 用于高优先级提醒的临时悬浮横幅。

NotificationListenerService → 外部服务

每个 NotificationListenerService 都会接收同样的事件,还能把数据转发给外部设备、记录分析数据,或者触发自定义行为(比如自动回复消息)。

5. 系统托盘与 SystemUI 集成解析

上面的时序图展示了 SystemUI(Android 系统的核心应用)是如何从框架服务那里注册并接收通知的。下面我们来逐一分析每个阶段。

在这里插入图片描述

初始化/启动

  • SystemUI(应用)在自己的进程(com.android.systemui)中启动。
  • 在设备启动或者 SystemUI 重启时,它会调用 StatusBarService.registerStatusBar(ISTatusBar.Stub)
  • 这个 Binder 注册会传递一个回调对象(IStatusBar.Stub),这样 StatusBarService 之后就能把新事件通知给 SystemUI 了。

通知发布

  • 位于 system_server 进程的 NotificationManagerService(NMS)通过 StatusBarService.enqueueNotificationRecord() 把新的通知记录入队。
  • 随后,StatusBarService(SBS)会向 WindowManagerService 请求状态栏图标(通过 registerStatusBarIcon()),以便在状态栏中预留空间。
  • 接下来,SBS 会调用 SystemUI 中已注册的回调函数 IStatusBar.onNotificationPosted(...),并传递一个包含所有必要数据的 StatusBarNotification 对象。

权限与渠道检查

在处理 enqueueNotificationRecord() 时,StatusBarService 在通知 SystemUI 之前,会执行两项关键检查:

  • 通知权限:在 Android 13+(API 33)上,它会验证发布应用是否还拥有 POST_NOTIFICATIONS 运行时权限。
  • 渠道启用状态:确认通知渠道仍处于启用状态,且未被用户在设置中屏蔽。

在 SystemUI 中渲染

收到回调后,SystemUI 就会接管:

  • 它会在 NotificationsShadeWindowView 中实例化通知行,处理布局、图标、文本、操作按钮以及分组逻辑。
  • 它还会通过 HeadsUpManager 显示悬浮 UI,HeadsUpManager 会根据重要性、时间以及当前设备状态(比如是否有正在进行的通话或者是否处于免打扰模式)来决定是否显示悬浮横幅。
    最终,通知会在屏幕上显示出来——要么是小图标,要么是通知栏条目,要么是悬浮提醒。

6. 进阶内容:特殊情况

Android 的省电和安全功能带来了一些特殊情况:

  • 离线状态:Firebase 云消息(FCM)会暂存高优先级消息,等设备重新联网后再尝试投递。
  • 打盹模式:在 Android 23+ 上,低优先级的工作会延迟到设备唤醒后处理,但紧急通知仍然可以突破限制。
  • 后台限制:从 Android 26+ 开始,长时间运行的后台工作需要前台服务(以及它自己的通知),否则系统会终止你的进程。
  • 状态丢失:如果 SystemUI 丢失了内存中的状态,它会向 NMS 请求活跃通知,从而重建通知栏。

7. 通知渠道与权限

Android 8.0+ 的 NotificationChannel

val channel = NotificationChannel(
    "chat_channel",
    "聊天提醒",
    NotificationManager.IMPORTANCE_HIGH
).apply {
    description = "聊天应用的通知"
}
notificationManager.createNotificationChannel(channel)

渠道组

val group = NotificationChannelGroup("social_group", "社交与消息")
val channel = NotificationChannel("social_chat", "聊天", NotificationManager.IMPORTANCE_DEFAULT).apply {
    group = "social_group"
}
notificationManager.createNotificationChannel(channel)

运行时权限(Android 13+)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    requestPermissions(
        arrayOf(Manifest.permission.POST_NOTIFICATIONS),
        REQUEST_CODE_NOTIFY
    )
}

要是没有这些运行时权限,你的应用通知肯定会被拦截。

8. 总结与最佳实践

  • 定义表意清晰的渠道,要有明确的名称和描述。
  • 遵守打盹和后台限制,有节制地使用高优先级。
  • 尊重用户的通知设置,避免过度打扰。
  • 对于周期性通知(Android 12+),使用 FLAG_IMMUTABLE
  • 在真实设备上测试,还要考虑各种 SystemUI 定制情况。

掌握了这些细节,你就能打造出更可靠、更省电,也更尊重用户的通知了。编码愉快!

Logo

欢迎加入我们的广州开发者社区,与优秀的开发者共同成长!

更多推荐