3. Android 通知栏与前台服务通知
前台服务必须绑定一个可见通知,这是 Android 系统的硬性要求。说白了,就是告诉用户:“嘿,我还在跑呢,别杀我”。但这个通知如果处理不当,很容易跟 Compose 的渲染管线抢资源。
3.1 前台服务通知的“保命”作用
系统从 Android 8.0(API 26)开始强制要求:任何前台服务必须在 5 秒内调用 startForeground(),否则会抛 IllegalStateException。而且通知栏必须一直显示,用户能手动关闭,但服务不会因此停止。
我早年踩过这个坑——项目里有个后台音乐播放服务,开发时忘了调 startForeground,结果三星手机上频繁闪退。查日志才发现是系统杀的,原因就是没通知。后来老老实实加了通知,问题解决。
POST_NOTIFICATIONS 权限必须动态申请。即便你的服务是前台服务,用户拒绝权限后,startForeground() 会静默失败,通知栏不显示,但服务仍然运行——这会导致后台行为不可控。
3.2 通知的构建与渠道
创建一个前台服务通知,通常需要三步:
- 创建
NotificationChannel(API 26+) - 构建
Notification对象,设置标题、内容、小图标 - 调用
startForeground(NOTIFICATION_ID, notification)
一个典型实现如下:
// 创建通知渠道(只需一次)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"后台任务",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "用于前台服务的持久通知"
}
val nm = getSystemService(NotificationManager::class.java)
nm.createNotificationChannel(channel)
}
// 构建通知
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("正在上传文件")
.setContentText("进度:${progress}%")
.setSmallIcon(R.drawable.ic_upload)
.setOngoing(true)
.build()
// 启动前台服务
startForeground(NOTIFICATION_ID, notification)
这里有个细节:IMPORTANCE_LOW 会让通知不会弹出声音,但依然在状态栏显示。如果你的通知频繁更新,IMPORTANCE_HIGH 会频繁震动或响铃,用户体验很差。
3.3 通知更新:一个容易被忽视的性能陷阱
很多同学以为通知更新只是调一下 startForeground 传个新 notification 就完事了。其实不然——每次更新通知,系统内部会经历:
- Binder 跨进程通信,将新的 notification 发送到 SystemUI 进程
- SystemUI 解析
RemoteViews,重新 inflate 布局 - 触发系统渲染管线,更新通知栏 UI
而你想想看,如果 Compose 主线程正在密集渲染一个动画(比如 Lottie 或列表快速滑动),这个时候通知每 100ms 更新一次进度,主线程就要同时处理 Compose 的测量/布局/绘制,以及 SystemUI 回调带来的 IPC 唤醒。资源竞争就这么产生了。
我项目中曾有一个实时位置追踪的前台服务,每 1 秒更新一次通知显示距离。用户反馈通知栏很流畅,但地图页面掉帧严重。抓 Trace 一看,android.app.ActivityManager.updateLruProcessLocked 和 com.android.server.notification.NotificationManagerService.enqueueNotificationInternal 占了大量时间。果断改成每 5 秒更新一次通知,页面帧率立刻恢复。
3.4 实战中的避坑经验
避免通知更新与 Compose 帧率竞争,我总结了三条原则:
| 原则 | 说明 | 典型反例 |
|---|---|---|
| 降低更新频率 | 非必要不更新,至少 1 秒以上间隔 | 每 200ms 更新一次下载进度 |
| 合并更新 | 多个数据变化时,等 500ms 窗口再刷新 | 进度、状态、剩余时间各发一次 |
| 避免复杂 RemoteViews | 使用系统内置模板,不要自定义复杂布局 | 自定义通知包含 ImageView 和动画 |
我曾经接手过一个智能家居 App,前台服务通知里放了一个可点击的“停止”按钮,还自定义了背景色。结果每次更新通知,RemoteViews 都要重新解析,耗时暴增。后来改成系统默认布局,只用 setContentIntent 实现停止动作,性能提升明显。
嗯,这里还要补充一个坑:当你更新通知时,记得检查 NotificationManager.areNotificationsEnabled()。如果用户关闭了通知权限,你频繁调 startForeground 虽然不会崩溃,但每次都会走一次无效的 Binder 调用,白白消耗 CPU 时间片。
3.5 再说一个容易忽略的点
前台服务通知的 RemoteViews 默认是在主线程解析的。如果你在子线程构建 Notification,看起来没问题,但 startForeground 内部还是会走到主线程去处理 Binder 回包。所以别指望切线程能完全避免竞争——关键是减少调用次数。
我曾经在日志里看到过 BindingDied 异常,就是因为通知更新太频繁导致 SystemUI 进程 OOM 重启。系统杀掉了 SystemUI,通知栏就崩了,用户直接“白条”。还好我们当时有后台监控,发现是通知更新频率每小时超过 10 万次……