4. 通知(Notification)基础:创建通知渠道、构建通知、通知样式
通知这件事,说简单也简单,说复杂也复杂。尤其是在 Android 8.0 之后,整个通知体系被重新梳理了一遍。如果你还在用老方式直接 new Notification 然后往通知栏一丢,那多半会踩坑。这一章我会把通知的核心脉络理清楚,帮你在做前台服务任务时,少走弯路。
4.1 通知渠道——Android 8.0 的分水岭
在 Android 8.0(API 26)之前,通知可以说是"野蛮生长"。开发者想怎么发就怎么发,用户只能被动接受。8.0 之后引入了通知渠道(NotificationChannel),说白了就是一个"分类"机制。每个通知必须归属到一个渠道,用户可以在系统设置里单独控制每个渠道的开关和提醒方式。
为什么要搞这么复杂?你想想看,假如你的 App 同时有"聊天消息"和"促销广告"两种通知,用户可能只想关掉广告,但不想错过聊天。没有通知渠道,要么全开要么全关,很尴尬。有了渠道,用户就能精确控制。
"channel_upload" 代表上传任务,"channel_sync" 代表数据同步。这样后期维护一目了然。
4.2 创建通知渠道
创建渠道的代码其实不多,但有一个关键点:渠道只需要创建一次,后续重复创建不会覆盖已有配置。所以通常在 Application 的 onCreate() 里统一初始化。
// 渠道 ID 和名称最好用常量管理
public static final String CHANNEL_ID = "foreground_service_channel";
public static final String CHANNEL_NAME = "前台服务通知";
// 创建渠道(API 26+)
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW // 前台服务推荐 LOW
);
channel.setDescription("用于长时间任务的前台服务通知");
channel.setShowBadge(false); // 前台任务通常不需要角标
NotificationManager manager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
这里有个细节:setShowBadge(false)。我在项目中遇到过一个问题——通知本来只是上传进度,结果 App 图标上一直顶着角标数字,用户以为有未读消息。后来才发现是忘了关掉角标。前台服务的通知,大多数场景不需要显示角标。
IMPORTANCE_NONE,结果通知直接不显示,系统还把前台服务给杀了。前台服务通知的最低要求是 IMPORTANCE_LOW,低于这个等级,系统会认为通知不可见,从而判定前台服务无效。
不同重要性等级的行为差异很大,我整理了一张表:
| 重要性等级 | 弹窗提醒 | 声音/震动 | 状态栏显示 | 推荐场景 |
|---|---|---|---|---|
| IMPORTANCE_HIGH | 是 | 是 | 是 | 紧急消息、来电 |
| IMPORTANCE_DEFAULT | 否 | 是 | 是 | 普通通知、社交动态 |
| IMPORTANCE_LOW | 否 | 否 | 是 | 前台服务、后台任务推荐 |
| IMPORTANCE_MIN | 否 | 否 | 否 | 静默通知、同步提示 |
4.3 构建通知
构建通知用的是 NotificationCompat.Builder,这个类兼容到低版本。对于前台服务,有几个属性是必设的:标题、内容、小图标,以及最重要的 setOngoing(true)。
Notification notification = new NotificationCompat.Builder(
this,
CHANNEL_ID
)
.setContentTitle("文件上传中")
.setContentText("已上传 60%")
.setSmallIcon(R.drawable.ic_upload)
.setOngoing(true) // 用户不可滑动清除
.setPriority(NotificationCompat.PRIORITY_LOW) // 低优先级,不打扰
.build();
setOngoing(true) 这个标志很关键。前台服务的通知必须是 "ongoing" 状态,否则用户一划就没了,系统会认为前台服务失去通知绑定,直接把它干掉。嗯,这里要注意:ongoing 通知在通知栏里会稍微偏灰,不能左滑或右滑清除,只能通过代码取消。
setContentTitle()— 通知标题,必填setContentText()— 通知正文,必填setSmallIcon()— 小图标,必填(Android 要求)setOngoing(true)— 前台服务通知必须设置setPriority()— 建议用 PRIORITY_LOW 或 PRIORITY_DEFAULTsetSilent(true)— 静默通知,不发声不震动(可选)
4.4 通知样式
默认的通知样式只有一行标题 + 一行正文,对于前台服务的进度更新或者长文本来说,显然不够用。Android 提供了几种扩展样式,我常用的有三种:
- BigTextStyle: 显示大段文本,适合日志输出或详情展示
- BigPictureStyle: 显示大图,适合图片上传/下载的场景
- InboxStyle: 显示多行列表,适合批量任务的状态汇总
// BigTextStyle 示例:展示长文本日志
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("同步日志")
.setContentText("同步进行中...")
.setSmallIcon(R.drawable.ic_sync)
.setOngoing(true)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText("文件1: 已完成\n文件2: 上传中 45%\n文件3: 等待中\n文件4: 等待中"))
.build();
// InboxStyle 示例:多任务状态列表
Notification inboxNotif = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("批量下载")
.setContentText("3个任务进行中")
.setSmallIcon(R.drawable.ic_download)
.setOngoing(true)
.setStyle(new NotificationCompat.InboxStyle()
.addLine("任务A: 已完成")
.addLine("任务B: 下载中 60%")
.addLine("任务C: 待开始"))
.build();
BigPictureStyle 我用的相对少一些,但在做图片上传功能时很有用。可以在通知里直接显示缩略图,用户不用点开就知道上传的是哪张图。
// BigPictureStyle 示例:显示上传图片的缩略图
Notification pictureNotif = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("图片上传")
.setContentText("正在上传第 3/8 张")
.setSmallIcon(R.drawable.ic_photo)
.setOngoing(true)
.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(bitmap) // bitmap 为图片缩略图
.setSummaryText("上传中..."))
.build();
4.5 前台服务通知的特殊要求
前台服务的通知跟普通通知有几个本质区别,搞不清楚的话很容易翻车。
- 必须显示: 启动前台服务后 5 秒内,必须调用
startForeground()并传入通知。超时了系统会直接杀掉服务,并打印 "Context.startForegroundService() did not then call Service.startForeground()" 的崩溃日志。 - 不可清除: 通知必须设置
setOngoing(true),用户无法滑动清除。只有服务主动调用stopForeground()或stopSelf()才会移除。 - 渠道不可静音关闭: 从 Android 12 开始,用户无法通过系统设置将前台服务的通知渠道设为"静音"或"关闭",系统强制要求通知必须可见。
- 最低重要性: 渠道的 importance 至少为
IMPORTANCE_LOW,否则通知不显示,前台服务无效。
IMPORTANCE_MIN,结果测试同学反馈说"上传任务经常莫名其妙中断"。查了一天日志才发现是通知没显示出来,系统判定前台服务不合法,直接给杀了。自那以后,我每次发版前都会在设置里检查一遍通知渠道的状态。
还有一点容易被忽略:如果你同一个 App 里启动了多个前台服务,每个服务都需要有自己的通知。不能复用同一个通知对象。系统会通过通知的 ID 来区分不同的前台服务通知。
- 渠道 importance 用
IMPORTANCE_LOW,平衡可见性和打扰 - 通知必须设置
setOngoing(true)和setSmallIcon() - 每个前台服务使用独立的通知 ID 和通知对象
- 在 Application 启动时统一创建渠道,保证渠道已注册
- 调试时先在系统设置里确认渠道状态,不要盲目查代码