5、通知点击与PendingIntent:Flag详解与三大组件启动
通知发出去,用户看到了,然后呢?
当然是要点击它,触发后续动作。这个“后续动作”,在Android里就是通过PendingIntent来实现的。说白了,PendingIntent就是一个“别人帮我执行的Intent”。你想想看,通知是系统进程管理的,它不能直接调用你的Activity或Service,所以你需要把Intent“包装”成PendingIntent,交给系统,等用户点击通知时,系统再帮你把这个Intent发出去。
我个人习惯把PendingIntent理解成“代金券”——你提前买好(创建),指定好用途(启动Activity/Service/广播),别人拿着它去消费(系统执行)。
5.1 PendingIntent的Flag详解
创建PendingIntent时,必须指定一个Flag。这个Flag决定了当系统发现“已经存在一个相同的PendingIntent”时,该怎么处理。嗯,这里很容易踩坑,我一个个说。
| Flag | 含义 | 典型场景 |
|---|---|---|
FLAG_ONE_SHOT |
PendingIntent只能被使用一次。使用后自动取消。 | 通知只能点击一次,防止重复触发 |
FLAG_NO_CREATE |
如果PendingIntent不存在,不创建,返回null。 | 检查是否存在,很少直接用 |
FLAG_CANCEL_CURRENT |
如果已存在,先取消旧的,再创建新的。 | 更新通知时,确保使用最新的Intent数据 |
FLAG_UPDATE_CURRENT |
如果已存在,保留旧的PendingIntent,但更新其Intent中的Extra数据。 | 多个通知使用同一个PendingIntent,但携带不同参数 |
FLAG_IMMUTABLE (API 23+) |
PendingIntent创建后,其内部的Intent不可被修改。 | 安全性要求高的场景,防止外部篡改 |
FLAG_MUTABLE (API 23+) |
PendingIntent创建后,其内部的Intent可以被修改。 | 需要动态填充数据的场景(如全屏通知) |
我个人最常用的是FLAG_UPDATE_CURRENT。为什么呢?因为通知经常需要更新进度或状态,用这个Flag可以保留PendingIntent的“身份”,同时更新里面的数据。如果你用FLAG_CANCEL_CURRENT,旧的通知会失效,有时候会导致点击无响应。
5.2 点击通知打开Activity
这是最常见的场景。用户点击通知,跳转到App的某个页面。
// 创建Intent,指定目标Activity
Intent intent = new Intent(context, NotificationDetailActivity.class);
intent.putExtra("notification_id", 1001);
// 设置Activity的启动方式
// 如果目标Activity不在栈顶,就创建新的Task
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 创建PendingIntent
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
0, // requestCode,用于区分不同的PendingIntent
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 设置到通知上
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle("新消息")
.setContentText("点击查看详情")
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pendingIntent)
.setAutoCancel(true); // 点击后自动移除通知
setAutoCancel(true)。我曾经忘记加,用户点击通知后页面跳转了,但通知栏里那条通知还在,用户以为没点到,又点了一次……结果开了两个页面。
关于Intent.FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_CLEAR_TOP的组合,说白了就是:如果目标Activity已经在后台栈里,就把它上面的Activity都清掉,把它拉到前台。这样用户按返回键时,不会看到一堆中间页面。
5.3 点击通知打开Service
有些场景不需要打开界面,只需要在后台执行任务。比如下载文件、上报日志、播放音乐。这时候就可以用PendingIntent启动Service。
// 创建Intent,指定目标Service
Intent serviceIntent = new Intent(context, DownloadService.class);
serviceIntent.putExtra("download_url", "https://example.com/file.zip");
// 创建PendingIntent,注意这里用的是getService()
PendingIntent pendingIntent = PendingIntent.getService(
context,
1,
serviceIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 设置到通知上
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle("下载任务")
.setContentText("点击开始下载")
.setSmallIcon(R.drawable.ic_download)
.addAction(R.drawable.ic_play, "开始下载", pendingIntent) // 作为按钮动作
.setContentIntent(pendingIntent); // 也可以作为点击通知本身
startForegroundService(),并在5秒内调用startForeground()显示前台通知。否则系统会杀你的Service并报错。
我记得有一次做音乐播放器,用户点击通知上的“播放”按钮,我用PendingIntent启动了Service。结果在Android 8.0的手机上,Service刚启动就被杀了。查了半天才发现是没转成前台Service。嗯,从那以后我养成了习惯:所有通过通知启动的Service,一律用前台Service。
5.4 点击通知打开BroadcastReceiver
BroadcastReceiver适合处理一些轻量级的、不需要长时间运行的任务。比如“标记已读”、“稍后提醒”、“取消订阅”等。
// 创建Intent,指定目标BroadcastReceiver
Intent broadcastIntent = new Intent(context, NotificationActionReceiver.class);
broadcastIntent.setAction("com.example.ACTION_MARK_READ");
broadcastIntent.putExtra("notification_id", 1001);
// 创建PendingIntent,注意这里用的是getBroadcast()
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
2,
broadcastIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 在通知上添加按钮
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle("新消息")
.setContentText("来自张三的消息")
.setSmallIcon(R.drawable.ic_message)
.addAction(R.drawable.ic_check, "标记已读", pendingIntent);
对应的BroadcastReceiver实现:
public class NotificationActionReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if ("com.example.ACTION_MARK_READ".equals(intent.getAction())) {
int notificationId = intent.getIntExtra("notification_id", -1);
// 执行标记已读的逻辑
// 比如更新数据库、通知服务器等
Log.d("NotificationAction", "标记已读,ID: " + notificationId);
}
}
}
<intent-filter>匹配Action。
为什么有时候用BroadcastReceiver而不是Service?说白了,广播是异步的、轻量的。你只是想让用户点一下“标记已读”,没必要开一个Service。而且广播执行完就结束了,不占资源。我做过一个IM App,所有通知按钮(已读、回复、删除)都是用BroadcastReceiver处理的,性能很好。
5.5 知识体系总览
下面这张图总结了本章的核心逻辑:
这张图很直观:用户点击通知后,系统通过PendingIntent把Intent分发给三大组件。你选择哪个组件,取决于你的业务需求。Flag则决定了PendingIntent的“复用策略”。
5.6 避坑指南
最后,我总结几个实际项目中容易踩的坑:
- PendingIntent重复问题: 如果你用相同的requestCode和Intent创建多个PendingIntent,系统会认为它们是同一个。这时候Flag就起作用了。我建议每个通知使用不同的requestCode,或者用
FLAG_UPDATE_CURRENT。 - Android 12的显式Flag要求: 前面提过,targetSdk 31+必须指定FLAG_IMMUTABLE或FLAG_MUTABLE。不指定直接崩溃。我曾经在升级时漏了一个地方,线上崩溃率直接涨了0.3%。
- 通知点击后不消失: 记得加
setAutoCancel(true)。如果你用自定义RemoteViews,需要手动调用NotificationManager.cancel()。 - Service启动限制: Android 8.0+后台Service限制,Android 12+前台Service也有新限制(需要声明前台服务类型)。做通知启动Service时,一定要适配这些变化。
好了,这一章的内容就到这里。PendingIntent是通知交互的核心,理解了它,你就能灵活控制用户点击通知后的行为。下一章我们会聊通知分组和聚合,那是处理大量通知时的利器。
公众号:蓝海资料掘金营,微信deep3321