先把结论放在前面:通知这题的三层追问是"渠道能不能改 → 点击怎么进正确的任务栈 → 权限被拒怎么办"。 第一层考机制理解(渠道的用户侧设置优先于应用侧),第二层考工程规范(PendingIntent 与栈设计),第三层考产品落地(引导用户去设置里开)。能按这三层顺序接住,就是资深候选人的答法。
Android 8.0 之后通知的核心变化是通知渠道(NotificationChannel):应用不再能直接决定通知的呈现细节,用户在系统设置里对某个渠道做的调整(关闭、降低重要性、屏蔽)优先级高于应用设置,且应用侧无法改回。 这条规则是本篇的机制核心,也是"删除重建想复位用户设置"这个坑的根因。
第一层:渠道的属性与不可逆的用户设置
// 渠道创建:重要性在创建后不可再改(可以改名称与描述)
val channel = NotificationChannel(
CH_ID_MSG,
getString(R.string.channel_msg_name),
NotificationManager.IMPORTANCE_HIGH // 关键属性,创建后不可变
).apply {
description = getString(R.string.channel_msg_desc)
enableVibration(true)
vibrationPattern = longArrayOf(0, 200, 100, 200)
setShowBadge(true)
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
}
manager.createNotificationChannel(channel) // 重复调用是幂等的,不会覆盖用户设置
四个必须讲清的机制点:
| 机制 | 说明 | 工程含义 |
|---|---|---|
| 重要性不可变 | importance 在创建后无法通过 createNotificationChannel 修改 |
需要区分重要性时,建多个渠道而不是改同一个 |
| 用户设置优先 | 用户改过重要性后,应用再调 createNotificationChannel 不生效 |
通知不显示多半是用户关过,不是代码问题 |
| 删除会丢设置 | deleteNotificationChannel 后用户设置一并清除 |
频繁删除重建会造成设置反复重置 |
| 渠道数量有上限 | 系统对渠道数量有配额限制 | 渠道命名要有分类规划,不能按业务随意建 |
"importance 不可变"这条是本篇最实用的机制:如果产品要求"消息通知高优先级、促销通知低优先级",正确做法是建两个渠道,而不是试图改同一个渠道的级别。能主动说出这个设计取舍,说明理解到了 API 设计的意图。
第二层:PendingIntent 与任务栈
点击通知进入 App 的正确路径,靠的是 PendingIntent 的三个参数:
// 点击通知:复用已有任务栈,没有则新建
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
val contentIntent = PendingIntent.getActivity(
context, requestCode,
Intent(context, DetailActivity::class.java).apply {
putExtra(EXTRA_ID, messageId)
// 关键:把任务栈策略写进 Intent
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
},
flags
)
val notification = NotificationCompat.Builder(context, CH_ID_MSG)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle(title)
.setContentText(body)
.setStyle(NotificationCompat.BigTextStyle().bigText(body)) // 长文本用 BigTextStyle
.setPriority(NotificationCompat.PRIORITY_HIGH) // 8.0 前的兼容字段
.setCategory(NotificationCompat.CATEGORY_MESSAGE) // 分类影响勿扰策略
.setContentIntent(contentIntent)
.setAutoCancel(true) // 点击后自动消失
.build()
PendingIntent 的三个坑:
1. requestCode 相同会导致 PendingIntent 被复用。多条通知传相同 requestCode 且 Intent 的 extras 不参与区分时,会复用同一个 PendingIntent,表现为"点第二条通知打开的是第一条的详情"。规范是每条通知用唯一 requestCode(例如用消息 id 取模),或者用 FLAG_UPDATE_CURRENT 配合唯一 extras。 2. FLAG_IMMUTABLE 是必须的(Android 12+ 强制要求显式声明)。通知点击的 PendingIntent 不需要被外部修改,用 FLAG_IMMUTABLE 更安全。 3. setAutoCancel 与 setContentIntent 要配套。只设前者不设后者,点击通知没有反应;setAutoCancel(true) 表示点击后自动取消通知。
栈设计的三个典型场景:
| 场景 | Intent flags | 效果 | |
|---|---|---|---|
| 回到已有详情页 | `NEW_TASK \ | CLEAR_TOP` | 复用栈中该 Activity,收到新的 extras 并走 onNewIntent |
| 每次开新页面 | NEW_TASK |
栈中已有也新建实例 | |
| 从通知直接进主界面 | 不加 flags | 复用主 Activity 并回调 onNewIntent |
关键点在于 onNewIntent 不会自动触发 onCreate,所以页面需要自己处理"二次进入"的情况(常见错误是只在 onCreate 里读 extras)。能提到这条,说明踩过"从通知二次进入拿不到数据"的坑。
第三层:权限与降级路径
Android 13(API 33)引入 POST_NOTIFICATIONS 运行时权限:
private val requestNotif = registerForActivityResult(RequestPermission()) {
granted ->
if (granted) {
pushTokenToServer() }
else showRationaleAndSettings() // 去设置页引导,而不是反复弹
}
fun ensureNotificationPermission() {
if (Build.VERSION.SDK_INT < 33) {
pushTokenToServer(); return }
if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED) {
pushTokenToServer(); return }
requestNotif.launch(Manifest.permission.POST_NOTIFICATIONS)
}
三条必答的降级原则:
① 权限被拒后不应反复弹窗(系统会限制弹窗次数,且体验极差)。规范是先解释价值,再引导到系统设置页。 ② 通知被关掉不影响推送通道。POST_NOTIFICATIONS 只控制通知展示,FCM 通道依然能收到消息——所以"通知权限被拒"和"推送收不到"是两个问题,排查时不要混。 ③ Android 8.0 前必须提供 NotificationContent:低版本没有渠道,通知的呈现完全由 Notification 对象决定,长文本要靠 BigTextStyle、大图要靠 BigPictureStyle 才能显示。规范是同一套通知内容用兼容构建同时提供两套样式。
通知的进阶形态与实战取舍
| 形态 | 用途 | 关键 API | 代价 |
|---|---|---|---|
| 长文本 | 消息类 | BigTextStyle |
低版本需兼容 |
| 大图/大图预览 | 资讯类 | BigPictureStyle |
需下载大图,流量与内存 |
| 进度条 | 下载类 | setProgress + 重复更新 |
高频更新会耗电 |
| 快捷回复 | 即时通讯 | RemoteInput |
需服务端支持 |
| 分类消息 | 社交类 | NotificationCompat.MessagingStyle |
需维护参与者列表 |
| 免打扰 | 会议/夜间 | setInterruptionFilter (O+) |
渠道级配置 |
| 悬浮通知 | 导航/通话 | setCategory(CALL) |
系统权限敏感 |
| 免打扰时段 | 减少打扰 | setAllowGeneratedContextualActions |
版本差异 |
实战取舍要点:通知的点击率远低于推送到达率,所以"什么时候不发通知"比"怎么发通知"更值得讲——静默时段、按重要度分级、聚合通知(同一类型合并为"3 条新消息")都是产品侧的答案。能补上聚合与静默策略,说明是从产品与工程两个角度考虑。
最常见的坑是
第一层坑:渠道删除重建想复位用户设置,实际无效且堆积垃圾渠道。
deleteNotificationChannel 会清除用户在该渠道上的所有设置;重新 createNotificationChannel 拿到的是系统默认。规范是需要"重置"效果时引导用户去系统设置手动改,或改用新的渠道 id(并接受新渠道的默认状态)。
第二层坑:通知点击未设置任务栈,返回键直接回到桌面。
根因是 Intent 未加 FLAG_ACTIVITY_NEW_TASK,从通知进入时没有自己的任务。规范是通知 Intent 显式声明栈策略,并在 onNewIntent 里处理二次进入。
第三层坑:多条通知共用一个 requestCode,点击后打开了错误的那条。
规范是requestCode 用消息 id 保证唯一,或在 Intent extras 里带足够区分信息并配合 FLAG_UPDATE_CURRENT。
这题在面试里怎么问、怎么答
问:通知渠道是什么?为什么它的重要性创建后不能改?
答:Android 8.0 引入的通知渠道把通知的呈现属性(重要性、振动、锁屏可见性等)下沉到渠道级,理由是这些属性直接影响用户体验与耗电,应该由用户掌控而不是应用单方面决定。因此创建后 importance 不可变——在用户已经表达过偏好的渠道上重新赋值会造成语义冲突。工程上如果需要不同重要性,就要建不同渠道。
问:PendingIntent 有什么坑?
答:三个。① 相同 requestCode 会被复用,导致多条通知共用一个 PendingIntent,表现为点开的内容不对——规范是 requestCode 唯一化。② Android 12+ 必须显式声明可变性,通知场景用 FLAG_IMMUTABLE。③ Intent 的栈策略要在 Intent 上声明(NEW_TASK/CLEAR_TOP),否则从通知进入时栈结构不对,返回键行为异常。再补一条:带 FLAG_ACTIVITY_CLEAR_TOP 时页面走 onNewIntent 而不是 onCreate,只读 extras 会拿不到新数据。
问:Android 13 通知权限被拒了怎么办?
答:先说清 POST_NOTIFICATIONS 只影响通知展示,不影响推送通道收到消息。然后按体验分三步:① 不反复弹窗(系统会限流);② 弹出说明解释通知价值;③ 引导到系统设置页。要提一句"引导文案要具体说明通知带来的价值,不要用'需要通知权限'这种表述"——这是产品细节但很能体现落地经验。
问:长消息通知怎么展示?低版本怎么处理?
答:用 BigTextStyle(setStyle)而不是普通 setContentText,普通通知在低版本会被截断到一行。Android 8.0 之前没有渠道,长文本与大图完全依赖 Style,所以要在兼容构建里同时提供。更早的版本还有 setStyle 的兼容分支需要按渠道分别处理。能提"通知样式与渠道是两个正交维度"就完整了。
落到项目里怎么做
一条能写进规范的红线:通知渠道按"用户认知的分类"规划(不按业务模块),每个渠道创建后不再删除重建;通知点击的 PendingIntent 必须唯一 requestCode + 显式栈策略 + 处理 onNewIntent。 这三条覆盖了本篇三类主要问题。
配套实践三条:① 做通知不显示的排查清单(渠道是否被用户关闭、重要性是否被降级、是否在免打扰时段、是否有渠道配额、权限是否授予),避免"代码没错但用户收不到"的扯皮;② 为通知点击路径写端到端测试(含从通知二次进入的 onNewIntent 场景);③ 聚合与静默策略在服务端侧落地,让"什么消息值得打扰用户"成为可配置规则而不是客户端硬编码。
给正在准备面试的你
这题的答法要落到"通知的三层追问是渠道不可逆、栈设计、权限降级"这条主线。推荐这条线:先讲渠道四个机制(重要性不可变、用户设置优先、删除丢设置、数量配额)→ 讲 PendingIntent 三坑与栈设计三场景 → 讲权限降级三原则 → 补三个坑 → 收在"渠道分类规划 + PendingIntent 三要求"。能被追问"渠道能不能改"并答出"用户设置优先、需要不同重要性就建不同渠道",说明理解到 API 设计意图;能主动提到 onNewIntent 不触发 onCreate 这个细节,说明处理过真实点击路径。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:图片选择与相机调用:Photo-Picker-与-SAF
下一篇预告:组件化-UI-收尾:从控件思维到组件思维
有任何问题欢迎在评论区留言交流。