如何在通知中添加操作按钮(Action)?
解读
国内面试中,这道题表面问“怎么加按钮”,实则考察候选人对 Android 通知体系、版本差异、权限管控、后台启动限制以及用户可感知体验的全链路掌握。面试官希望听到:
- 从 API 16 到 API 34 的兼容性写法;
- 对“后台高功耗应用”无法弹出通知、无法响应 Action 的国内 ROM 特殊限制有认知;
- 能区分普通 Action、直接回复 Action、语义 Action(Wear/Auto)及折叠屏大通知布局;
- 能讲清 PendingIntent 的 FLAG、权限、SELinux 标签与 TEE 可信调用链,防止面试追问安全模型;
- 最后落到线上灰度与性能:通知过多导致系统杀进程、ANR、耗电,如何用 Battery Historian 量化。
一句话:把“写代码”升级到“写能在国内 4×ROM×渠道×折叠屏×后台管控下稳定运行的代码”。
知识点
- Notification.Action 构建要素:icon(纯 Alpha 图标,国内 ROM 常强制单色)、title、PendingIntent;
- PendingIntent 类型与生命周期:getBroadcast()、getService()、getActivity()、getForegroundService(),API 31+ 需显式指定 FLAG_IMMUTABLE 或 FLAG_MUTABLE;
- 后台启动限制:API 26 引入 Background Execution Limits,API 31 新增“通知 trampoline”禁止,国内小米/华为/OPPO 在 28-33 区间追加“后台高耗电”名单,直接丢弃 Action 事件;
- 直接回复 Action:RemoteInput + NotificationCompat.Action.Builder,需申请 Android 13 运行时权限 POST_NOTIFICATIONS;
- 语义 Action:setSemanticAction(SemanticAction.REPLY) 用于车载/穿戴,需配合 androidx.car.app 或 WearableExtender;
- 大视图样式:MediaStyle、MessagingStyle、BigTextStyle,折叠屏设备在 12L 以上自动展开大通知,Action 数量上限 3 个,超过会被系统折叠进“更多”;
- 安全与权限:PendingIntent 必须显式 Intent,禁止隐式;SELinux 对 system_server 通知通道有 mac_permissions.xml 校验;TEE 场景下支付类通知需走 TrustedUI,不能暴露 Action;
- 性能与灰度:通知栏刷新 500 ms 防抖,高频 Action 触发导致 system_server 高 CPU,可用 Perfetto 跟踪 binder 调用;Battery Historian 查看“notification_enqueue”标签,控制每日通知量 < 50 条;
- 线上监控:Firebase/友盟埋点统计 Action 点击率,区分 ROM 版本,发现小米 13 上点击无响应时,可动态降级为跳转到前台 Activity 方案。
答案
分五步给出可在国内全机型落地的最小完备实现,兼容 API 21-34,并避开后台启动陷阱。
- 声明权限与渠道(targetSdk 34)
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
val channel = NotificationChannel("chat", "即时消息", NotificationManager.IMPORTANCE_HIGH).apply {
description = "聊天消息快速回复"
setShowBadge(true)
}
val nm = getSystemService(NotificationManager::class.java)
nm.createNotificationChannel(channel)
- 构建显式 Intent 与 PendingIntent(使用 BroadcastReceiver trampoline-free)
val replyIntent = Intent(context, ReplyReceiver::class.java).apply {
action = "com.example.REPLY"
putExtra("msgId", msgId)
}
val flags = PendingIntent.FLAG_UPDATE_CURRENT or
(if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_IMMUTABLE else 0)
val replyPendingIntent = PendingIntent.getBroadcast(context, msgId, replyIntent, flags)
- 添加 RemoteInput 实现直接回复
val remoteInput = RemoteInput.Builder("key_text_reply")
.setLabel("快捷回复")
.build()
val replyAction = NotificationCompat.Action.Builder(
R.drawable.ic_reply_mono, "回复", replyPendingIntent)
.addRemoteInput(remoteInput)
.setSemanticVersion(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
.build()
- 组装通知并发送
val notification = NotificationCompat.Builder(context, "chat")
.setSmallIcon(R.drawable.ic_mono)
.setContentTitle("面试官")
.setContentText("请实现通知按钮")
.setStyle(NotificationCompat.BigTextStyle().bigText("请点击下方按钮完成面试题"))
.addAction(replyAction) // 最多 3 个,系统折叠屏自动展开
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(context).notify(msgId, notification)
- 在 ReplyReceiver 中处理后台限制
class ReplyReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
val results = RemoteInput.getResultsFromIntent(intent)
val text = results?.getCharSequence("key_text_reply")?.toString() ?: return
// 立即启动前台 Service,避免后台限制
val serviceIntent = Intent(ctx, ReplyService::class.java).apply {
putExtra("text", text)
}
ctx.startForegroundService(serviceIntent)
}
}
ReplyService 在 onStartCommand 中启动前台通知(ID 不能与聊天通知冲突),执行网络请求,结束后 stopSelf。至此,Action 按钮在国内任何 ROM 都能稳定响应,且符合后台启动与耗电管控要求。
拓展思考
- 折叠屏与平板大通知:Android 12L 引入 Notification.Builder.setExpandable(true),系统会自动把前三个 Action 以按钮形式露出,剩余 Action 放入“更多”。如何针对展开/折叠状态动态调整 Action 数量?
- 语义 Action 与 AI 排序:Android 14 推出 NotificationListenerService.onNotificationRankingUpdate(),系统会根据用户习惯把高频 Action 提前。如何结合 ML Kit 训练本地模型,预测用户最可能点的按钮,从而把对应 Action 放在索引 0 提升点击率?
- 安全攻防:某些恶意应用通过构造相同 Action 名称的隐式 Intent 劫持 PendingIntent,虽然 Android 12 已禁止隐式,但国内 ROM 回退补丁不全。如何利用 TEE 可信 UI 通道,把支付类通知 Action 的点击事件直接路由到安全世界,避免恶意截获?
- 省电策略:Battery Historian 显示通知 Action 触发后 30 s 内 CPU 占用 15%,经查为网络请求未批处理。如何结合 WorkManager 的加急任务(expedited Work)把多条回复合并上传,降低唤醒次数?
- 无代码热更新:若线上发现某 ROM 对 icon 尺寸校验导致 Action 按钮不显示,如何利用 Google Play 的“通知样式实验”或国内华为 A/B 实验平台,在不发版的情况下动态替换 icon 资源,验证修复效果?