You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决Firebase消息服务下Android聊天通知的竞态条件问题

解决Android聊天通知的竞态条件问题

问题根源

当前代码的核心问题在于读取旧通知→修改→重新发布是一个非原子操作:当多条消息同时到达时,多个线程会同时读取到同一个旧通知状态,各自添加新消息后发布,后执行的线程会覆盖前一个的结果,导致部分消息丢失。


高效解决方案

方案1:用线程安全的本地消息队列替代从NotificationManager获取旧状态

直接在本地维护每个会话的消息历史,避免依赖系统通知的状态,从根源上消除竞态。

  1. 维护线程安全的消息存储:
    在NotificationHelper中用线程安全的集合存储每个会话的消息:

    // 用ConcurrentHashMap保证会话级别的线程安全,CopyOnWriteArrayList保证消息列表的线程安全
    private final ConcurrentHashMap<String, List<NotificationCompat.MessagingStyle.Message>> conversationMessageCache = new ConcurrentHashMap<>();
    
  2. 新消息到达时先更新本地缓存:

    // 生成会话唯一标识(比如用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);
    
  3. 基于本地缓存构建通知:
    不再从系统读取旧通知,直接用本地缓存的完整消息列表构建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:用单线程串行处理所有通知更新

把所有通知更新任务放到一个单线程队列中,确保同一时间只有一个任务在处理同一个会话的通知,避免竞态。

  1. 初始化单线程Handler:
    在NotificationHelper的构造方法中创建一个单线程的Handler:

    private Handler notificationHandler;
    
    public NotificationHelper(Context context) {
        HandlerThread handlerThread = new HandlerThread("NotificationUpdateThread");
        handlerThread.start();
        notificationHandler = new Handler(handlerThread.getLooper());
    }
    
  2. 将通知更新任务提交到单线程执行:
    把原来的通知更新逻辑封装成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:用同步锁保护通知更新逻辑

如果不想修改太多现有代码,可以给每个会话的通知更新逻辑加锁,确保同一时间只有一个线程能处理该会话的通知。

  1. 维护会话锁集合:

    private final HashMap<String, Object> conversationLocks = new HashMap<>();
    
    // 获取对应会话的锁
    private Object getConversationLock(String conversationKey) {
        synchronized (conversationLocks) {
            return conversationLocks.computeIfAbsent(conversationKey, k -> new Object());
        }
    }
    
  2. 在通知更新逻辑外层加锁:

    String conversationKey = String.valueOf(notificationId);
    Object lock = getConversationLock(conversationKey);
    synchronized (lock) {
        // 原来的完整通知更新逻辑放在这里
        Notification oldNotification = getActiveNotification(notificationId);
        // ... 后续构建和发布逻辑
    }
    

这种方式改动最小,但如果锁的粒度控制不好,可能会影响性能,适合快速修复现有代码。


推荐方案

优先选择方案1(本地消息队列),因为它不仅解决了竞态问题,还能避免系统通知状态不一致的潜在问题,同时便于后续扩展(比如通知清除后恢复消息列表)。如果现有代码改动成本高,可以先用**方案3(同步锁)**快速修复,再逐步迁移到方案1。

内容的提问来源于stack exchange,提问作者Sujith S Manjavana

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 19:03:08