第4章:去重逻辑:NotificationKey生成、旧通知替换、相同key合并策略

4.1 原理概述

通知去重是SystemUI通知队列处理的核心机制,其目标是为每条通知生成一个唯一标识符(NotificationKey),并在新通知到达时,通过该标识符判断是否为已有通知的更新版本。如果Key相同且满足替换条件,则直接替换旧通知的数据字段,而非追加为新条目。当多条通知共享同一Key且需同时展示时(如消息类通知的折叠),则执行合并策略,将内容聚合为一条通知。

通知Key的生成规则由Android平台定义,通常由以下三部分复合构成:

  • 包名(PackageName):通知所属应用的唯一标识。
  • 标签(Tag):由应用开发者指定的字符串,可为null。
  • ID(Id):由应用开发者指定的整数,在相同包名和标签下唯一。

生成公式如下:

String key = packageName + ":" + (tag != null ? tag : "null") + ":" + id;

在SystemUI内部,当新通知到达(NotificationDataManager.onNotificationPosted)时,GroupManager与NotificationData组件会检查现有通知列表中是否存在相同Key的条目:

  • 若不存在:执行插入操作,生成新的通知条目。
  • 若存在:根据通知的Flags与shouldRecycle判断执行替换或合并。
注意点:Tag为null时不能拼接为"null"字符串,标准做法是直接跳过该部分,保留冒号分隔符。部分厂商定制中会额外加入用户ID(UserHandle)或频道ID(ChannelId)以增强唯一性。

4.2 实现流程

去重逻辑的完整流程如下表所示:

步骤 动作 关键代码或方法
1 通知到达SystemUI NotificationDataManager.onNotificationPosted()
2 提取包名、Tag、ID并合成Key NotificationUtils.getKey(sbn)
3 查询现有队列中是否存在相同Key NotificationData.get(key) != null
4 若不存在,直接插入排序位置 NotificationData.add(entry)
5 若存在,检查是否可替换 shouldRecycle(oldEntry, newEntry)
6 替换:更新oldEntry字段,保留Key和创建时间 oldEntry.copyFrom(newEntry)
7 合并:若为可合并类型,触发合并策略 GroupManager.onPostAsync()内部判断
替换条件:只有当新通知的所有字段均为更新内容,且旧通知未处于“不可替换”状态(如已被用户清除或已置顶),才执行替换。合并策略则针对支持分组的消息通知(Notification.MessagingStyle)或具有相同sortKey的非分组通知。

4.3 核心API与代码示例

以下展示NotificationKey的生成与去重检查关键实现。该代码摘自Android AOSP,位于frameworks/base/core/java/android/service/notification/NotificationListenerService.java和packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java。

Key生成(简化版)

public static String getKey(StatusBarNotification sbn) {
    String pkg = sbn.getPackageName();
    String tag = sbn.getTag();
    int id = sbn.getId();
    return pkg + "|" + (tag != null ? tag : "") + "|" + id;
}

去重检查与替换

public void onNotificationPosted(StatusBarNotification sbn) {
    String key = NotificationUtils.getKey(sbn);
    NotificationEntry oldEntry = mNotificationData.get(key);
    if (oldEntry == null) {
        // 新通知,直接添加
        NotificationEntry entry = new NotificationEntry(sbn);
        mNotificationData.add(entry);
    } else {
        // 存在旧通知,尝试替换
        if (shouldRecycle(oldEntry, sbn)) {
            oldEntry.setNotification(sbn);
            // 更新视图,但保留历史位置
            updateNotificationView(oldEntry);
        } else {
            // 不可替换时仍作为新通知(极少发生)
            NotificationEntry entry = new NotificationEntry(sbn);
            mNotificationData.replaceEntry(key, entry);
        }
    }
}

合并策略触发入口

public boolean shouldBeGrouped(NotificationEntry entry) {
    // 仅当通知携带 MessagingStyle 或 hasSameGroupKey 时合并
    return entry.getNotification().isStyle(Notification.MessagingStyle.class)
            || (entry.getSbn().getNotification().getGroup() != null
                && !entry.getSbn().getNotification().isGroupSummary());
}
常见错误:部分开发者直接将tag和id简单拼接成整数,忽略了包名和tag为空的情况,导致不同应用的通知互相覆盖。另外,在替换时忘记清理旧的RemoteViews缓存,可能导致LeakCanary告警或UI闪烁。

4.4 去重流程图

通知去重与替换合并流程 通知去重与替换合并流程 新通知到达 提取包名+Tag+ID 生成Key Key是否已存在? 否 插入新条目到队列 是 是否可替换? 是 替换旧通知数据字段 否 触发合并策略 更新UI为合并摘要 通知队列更新完成

4.5 调试与验证

开发阶段可通过以下命令模拟通知的替换与合并场景,以验证去重逻辑是否正确:

# 发送一条带tag和id的通知(例如tag="chat1", id=1)
adb shell cmd notification post --tag chat1 --id 1 "com.example.app" "标题1" "内容1"

# 再次发送相同tag和id但内容不同的通知,预期应替换
adb shell cmd notification post --tag chat1 --id 1 "com.example.app" "标题1更新" "内容更新"

# 查看SystemUI日志确认Key匹配和替换动作
adb logcat -b events | grep "NotificationKey\|onNotificationPosted\|replaceEntry"
关键日志TAG:通知去重相关日志通常输出在NotificationData、GroupManager和NotificationDataManager下。使用过滤命令:adb logcat -s NotificationData GroupManager NotificationDataManager。

4.6 性能与稳定性优化建议

  • Key散列缓存:对于频繁通知的应用(如IM),建议在读取通知列表时预计算Key的HashMap,避免每次遍历O(n)查找。
  • 替换时避免全量刷新:只更新变更字段所影响的视图部分(如文本、图标),减少RecyclerView的刷新范围。
  • 合并策略的收敛:对同一Key的多条通知,限制最大合并条数为50,防止极端情况下UI卡顿或OOM。
  • 死锁防范:去重逻辑中如果涉及跨进程回调(如通知列表变更通知应用),需使用异步Handler或协程,避免在主线程中等待锁。

4.7 常见错误排查

症状 可能原因 解决方案
相同通知反复出现,不被替换 Tag或id提取错误,或Key生成时使用了不同分隔符 对比adb log中打印的key字符串与预期是否一致
替换后通知界面闪烁或闪白 替换时未复用旧View或动画冲突 使用replaceEntry而非add,调用NotificationEntry.apply()增量更新
合并后通知内容丢失消息 合并策略只保留了最新一条消息,未聚合 检查MessagingStyle.addMessage()是否在构建时追加而非替换