解决Firebase消息服务下Android聊天通知的竞态条件问题
问题根源
当前代码的核心问题在于读取旧通知→修改→重新发布是一个非原子操作:当多条消息同时到达时,多个线程会同时读取到同一个旧通知状态,各自添加新消息后发布,后执行的线程会覆盖前一个的结果,导致部分消息丢失。
高效解决方案
方案1:用线程安全的本地消息队列替代从NotificationManager获取旧状态
直接在本地维护每个会话的消息历史,避免依赖系统通知的状态,从根源上消除竞态。
维护线程安全的消息存储:
在NotificationHelper中用线程安全的集合存储每个会话的消息:// 用ConcurrentHashMap保证会话级别的线程安全,CopyOnWriteArrayList保证消息列表的线程安全 private final ConcurrentHashMap<String, List<NotificationCompat.MessagingStyle.Message>> conversationMessageCache = new ConcurrentHashMap<>();新消息到达时先更新本地缓存:
// 生成会话唯一标识(比如用notificationId转字符串) String conversationKey = String.valueOf(notificationId); // 获取或创建当前会话的消息列表 List<NotificationCompat.MessagingStyle.Message> messageList = conversationMessageCache.computeIfAbsent(conversationKey, k -> new CopyOnWriteArrayList<>()); // 创建新消息对象(和你原来的逻辑一致) NotificationCompat.MessagingStyle.Message newMessage; if (fromMe) { Person you = new Person.Builder().setName("You").setIcon(IconCompat.createWithResource(this, android.R.color.transparent)).build(); newMessage = new NotificationCompat.MessagingStyle.Message(msg, System.currentTimeMillis(), you); } else { IconCompat tIcon = dp != null ? IconCompat.createWithBitmap(dp) : null; Person them = new Person.Builder().setIcon(tIcon).setName(senderName).build(); newMessage = new NotificationCompat.MessagingStyle.Message(msg, System.currentTimeMillis(), them); } // 添加到本地缓存(CopyOnWriteArrayList支持线程安全的添加) messageList.add(newMessage);基于本地缓存构建通知:
不再从系统读取旧通知,直接用本地缓存的完整消息列表构建MessagingStyle:Person you = new Person.Builder().setName("You").setIcon(IconCompat.createWithResource(this, android.R.color.transparent)).build(); NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(you); messagingStyle.setConversationTitle(senderName); messagingStyle.setGroupConversation(false); // 把本地缓存的所有消息添加到样式中 for (NotificationCompat.MessagingStyle.Message msg : messageList) { messagingStyle.addMessage(msg); } // 后续构建NotificationCompat.Builder和发布通知的逻辑和原来一致 NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, channelId) .setDefaults(Notification.DEFAULT_ALL) .setContentTitle(senderName) .setSmallIcon(R.drawable.ic_buddy_request_notification) .setAutoCancel(true) .setStyle(messagingStyle) .setLargeIcon(dp) .setColorized(true) .setColor(getColor(R.color.message)) .setContentIntent(activityLaunchIntent) .addAction(replyAction) .setSound(soundUri) .setCategory(Notification.CATEGORY_MESSAGE) .setPriority(NotificationCompat.PRIORITY_HIGH); Notification myNotification = mBuilder.build(); if (ActivityCompat.checkSelfPermission(NotificationHelper.this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { return; } managerCompat.notify(notificationId, myNotification);
这种方式完全避免了对系统通知状态的依赖,所有消息都先存入本地缓存,构建通知时用完整的消息列表,不会出现丢失。
方案2:用单线程串行处理所有通知更新
把所有通知更新任务放到一个单线程队列中,确保同一时间只有一个任务在处理同一个会话的通知,避免竞态。
初始化单线程Handler:
在NotificationHelper的构造方法中创建一个单线程的Handler:private Handler notificationHandler; public NotificationHelper(Context context) { HandlerThread handlerThread = new HandlerThread("NotificationUpdateThread"); handlerThread.start(); notificationHandler = new Handler(handlerThread.getLooper()); }将通知更新任务提交到单线程执行:
把原来的通知更新逻辑封装成Runnable,提交到Handler:// 把所有参数封装到Runnable中 notificationHandler.post(() -> { // 这里放入你原来的完整通知更新逻辑: // 读取旧通知、构建MessagingStyle、添加新消息、发布通知 Notification oldNotification = getActiveNotification(notificationId); NotificationCompat.MessagingStyle messagingStyle = null; if (oldNotification != null) { messagingStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(oldNotification); } Person you = new Person.Builder().setName("You").setIcon(IconCompat.createWithResource(this, android.R.color.transparent)).build(); if (messagingStyle == null) { messagingStyle = new NotificationCompat.MessagingStyle(you); messagingStyle.setConversationTitle(senderName); messagingStyle.setGroupConversation(false); } NotificationCompat.MessagingStyle.Message notificationMessage; if (fromMe) { notificationMessage = new NotificationCompat.MessagingStyle.Message(msg, System.currentTimeMillis(), you); } else { IconCompat tIcon = dp != null ? IconCompat.createWithBitmap(dp) : null; Person them = new Person.Builder().setIcon(tIcon).setName(senderName).build(); notificationMessage = new NotificationCompat.MessagingStyle.Message(msg, System.currentTimeMillis(), them); } messagingStyle.addMessage(notificationMessage); NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, channelId) .setDefaults(Notification.DEFAULT_ALL) .setContentTitle(senderName) .setSmallIcon(R.drawable.ic_buddy_request_notification) .setAutoCancel(true) .setStyle(messagingStyle) .setLargeIcon(dp) .setColorized(true) .setColor(getColor(R.color.message)) .setContentIntent(activityLaunchIntent) .addAction(replyAction) .setSound(soundUri) .setCategory(Notification.CATEGORY_MESSAGE) .setPriority(NotificationCompat.PRIORITY_HIGH); Notification myNotification = mBuilder.build(); if (ActivityCompat.checkSelfPermission(NotificationHelper.this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { return; } managerCompat.notify(notificationId, myNotification); });
所有通知更新任务都会在同一个线程串行执行,不会出现同时读取和修改同一个通知的情况,从流程上避免竞态。
方案3:用同步锁保护通知更新逻辑
如果不想修改太多现有代码,可以给每个会话的通知更新逻辑加锁,确保同一时间只有一个线程能处理该会话的通知。
维护会话锁集合:
private final HashMap<String, Object> conversationLocks = new HashMap<>(); // 获取对应会话的锁 private Object getConversationLock(String conversationKey) { synchronized (conversationLocks) { return conversationLocks.computeIfAbsent(conversationKey, k -> new Object()); } }在通知更新逻辑外层加锁:
String conversationKey = String.valueOf(notificationId); Object lock = getConversationLock(conversationKey); synchronized (lock) { // 原来的完整通知更新逻辑放在这里 Notification oldNotification = getActiveNotification(notificationId); // ... 后续构建和发布逻辑 }
这种方式改动最小,但如果锁的粒度控制不好,可能会影响性能,适合快速修复现有代码。
推荐方案
优先选择方案1(本地消息队列),因为它不仅解决了竞态问题,还能避免系统通知状态不一致的潜在问题,同时便于后续扩展(比如通知清除后恢复消息列表)。如果现有代码改动成本高,可以先用**方案3(同步锁)**快速修复,再逐步迁移到方案1。
内容的提问来源于stack exchange,提问作者Sujith S Manjavana

