NotificationManagerService:通知管理与优先级控制

0 阅读14分钟

Android 15 核心子系统系列 - 第24篇

本篇深入分析Android通知系统的核心服务NotificationManagerService,理解通知渠道、优先级控制和免打扰模式的工作机制。

引言

想象一下:你的手机一天收到几十上百条通知——即时消息、邮件、新闻推送、应用更新...如果每条通知都同样"吵闹",你会疯掉。

这就是为什么Android从8.0开始引入了通知渠道(Notification Channels)机制——让用户精细控制每个应用的不同类型通知。而管理这一切的核心服务,就是NotificationManagerService(NMS)。

在上一篇电源管理中,我们看到Android如何限制后台应用;今天我们将看到,Android如何让前台通知既有效又不烦人。

一、NotificationManagerService整体架构

1.1 架构设计哲学

NMS的设计遵循几个核心原则:

用户至上:用户的通知偏好优先于应用的发送请求 精细控制:通过渠道机制实现类型级别的控制 智能排序:根据重要性、时间、用户习惯排序 隐私保护:锁屏通知可隐藏敏感内容

1.2 四层架构

24-01-notification-manager-service-architecture.png

1.3 核心组件

// frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

public class NotificationManagerService extends SystemService {
    // 核心组件
    private RankingHelper mRankingHelper;              // 排序助手
    private NotificationListeners mListenerManager;    // 监听器管理
    private ZenModeHelper mZenModeHelper;             // 免打扰模式
    private SnoozeHelper mSnoozeHelper;               // 延迟助手
    private GroupHelper mGroupHelper;                 // 分组助手

    // 通知记录
    private final NotificationRecordList mNotificationList;  // 所有通知
    private final ArrayMap<String, NotificationRecord> mNotificationsByKey;  // 按key索引

    // 配置
    private AtomicFile mPolicyFile;                   // 策略配置文件
    private RankingConfig mRankingConfig;             // 排序配置
}

二、通知渠道(Notification Channels)机制

2.1 为什么需要通知渠道?

在Android 8.0之前,应用的所有通知共享一个设置,用户只能选择"全开"或"全关"。这导致:

  • 想收重要消息,就得忍受垃圾推送
  • 不想被打扰,就收不到关键通知

通知渠道解决了这个问题——同一应用的不同类型通知,可以分别控制

生活类比:就像邮件的"收件箱分类"——工作邮件设置提醒,营销邮件静音,垃圾邮件直接屏蔽。

2.2 渠道的三级层次结构

应用 (App)
  ├─ 渠道组 (Channel Group) - 可选
  │   ├─ 渠道1 (Channel)
  │   └─ 渠道2 (Channel)
  └─ 渠道3 (Channel) - 未分组

示例:即时通讯应用

// 创建渠道组
val groupId = "message_group"
notificationManager.createNotificationChannelGroup(
    NotificationChannelGroup(groupId, "消息通知")
)

// 创建渠道
val channels = listOf(
    NotificationChannel("private_msg", "私聊消息", IMPORTANCE_HIGH).apply {
        group = groupId
        description = "一对一聊天消息"
        enableVibration(true)
        setSound(privateMsgSound, audioAttributes)
    },
    NotificationChannel("group_msg", "群聊消息", IMPORTANCE_DEFAULT).apply {
        group = groupId
        description = "群组聊天消息"
        enableVibration(false)  // 群消息不震动
    },
    NotificationChannel("system_notice", "系统通知", IMPORTANCE_LOW).apply {
        description = "账号、安全相关通知"
        setSound(null, null)  // 无声音
    }
)

notificationManager.createNotificationChannels(channels)

2.3 渠道重要性级别

24-02-notification-channel-importance-levels.png

Android定义了5个重要性级别:

级别行为使用场景
IMPORTANCE_HIGH4🔴 声音+横幅+锁屏来电、闹钟、紧急消息
IMPORTANCE_DEFAULT3🟠 声音+状态栏即时消息、邮件
IMPORTANCE_LOW2🟡 无声音+状态栏推荐内容、社交更新
IMPORTANCE_MIN1🟢 仅通知栏(无图标)后台状态、天气
IMPORTANCE_NONE0⚪ 完全屏蔽用户禁用的渠道

源码实现

// NotificationChannel.java

// 根据重要性决定通知行为
boolean shouldShowBadge() {
    return mImportance >= IMPORTANCE_LOW;  // 低及以上显示角标
}

boolean canShowBanner() {
    return mImportance >= IMPORTANCE_HIGH;  // 仅高级别显示横幅
}

boolean canBypassDnd() {
    return mImportance >= IMPORTANCE_HIGH
        && mBypassDnd;  // 高级别可绕过免打扰
}

2.4 渠道的不可逆降级

关键设计:用户降低渠道重要性后,应用无法通过代码恢复

// NotificationManagerService.java

void updateNotificationChannelInt(String pkg, int uid,
        NotificationChannel channel, boolean fromTargetApp) {

    NotificationChannel existing = getChannel(pkg, uid, channel.getId());

    if (fromTargetApp) {
        // 应用尝试更新渠道
        // 检查是否试图提升重要性
        if (channel.getImportance() > existing.getImportance()) {
            // 禁止!保持用户设置的较低值
            channel.setImportance(existing.getImportance());
            Log.w(TAG, "App cannot increase channel importance");
        }
    }

    // 更新渠道
    updateChannel(pkg, uid, channel);
}

设计理念用户说了算——应用不能推翻用户的选择,这是Android通知系统的核心原则。

三、通知发送与处理流程

3.1 完整的发送流程

App调用NotificationManager.notify()
            ↓
    Binder IPC到NMS
            ↓
    权限检查(AppOps)
            ↓
    渠道验证(必须有效渠道)
            ↓
    重要性评估(RankingHelper)
            ↓
    免打扰模式过滤(ZenModeHelper)
            ↓
    创建NotificationRecord
            ↓
    通知排序(mRankingHelper.sort())
            ↓
    通知监听器回调
            ↓
    显示到StatusBar

3.2 源码分析:enqueueNotificationInternal

// NotificationManagerService.java

void enqueueNotificationInternal(final String pkg, final String opPkg,
        final int callingUid, final int callingPid, final String tag,
        final int id, final Notification notification, int userId) {

    // 1. 权限检查
    checkCallerIsSystemOrSameApp(pkg);
    final boolean isSystemNotification = isUidSystem(callingUid);

    // 2. 速率限制检查(防刷屏)
    if (!isSystemNotification) {
        synchronized (mNotificationLock) {
            final float rate = mUsageStats.getAppEnqueueRate(pkg);
            if (rate > mMaxPackageEnqueueRate) {
                // 超过速率限制,延迟发送
                mSnoozeHelper.snooze(r, SNOOZE_UNTIL_UNTHROTTLED);
                return;
            }
        }
    }

    // 3. 获取或创建通知渠道
    final NotificationChannel channel = getChannel(pkg, callingUid,
            notification.getChannelId());
    if (channel == null) {
        // Android 8.0+必须指定有效渠道
        throw new IllegalArgumentException(
            "No Channel found for pkg=" + pkg
            + ", channelId=" + notification.getChannelId());
    }

    // 4. 创建通知记录
    final NotificationRecord r = new NotificationRecord(
        getContext(), notification, channel);

    // 5. 检查免打扰模式
    if (mZenModeHelper.shouldIntercept(r)) {
        r.setSuppressed(true);  // 被免打扰模式拦截
    }

    // 6. 添加到通知列表
    synchronized (mNotificationLock) {
        // 移除旧通知(相同id)
        NotificationRecord old = mNotificationsByKey.get(r.getKey());
        if (old != null) {
            cancelNotificationLocked(old, false, REASON_APP_CANCEL);
        }

        // 添加新通知
        mNotificationList.add(r);
        mNotificationsByKey.put(r.getKey(), r);

        // 7. 排序
        mRankingHelper.sort(mNotificationList);

        // 8. 通知监听器
        mListenerManager.notifyPostedLocked(r, old);

        // 9. 更新状态栏
        mHandler.post(() -> mStatusBar.addNotification(r));
    }
}

关键步骤

  1. 权限检查——确保调用者身份合法
  2. 速率限制——防止应用短时间发送大量通知
  3. 渠道验证——Android 8.0+强制要求
  4. 免打扰过滤——根据ZenMode规则拦截
  5. 排序——按重要性、时间排序
  6. 回调监听器——通知其他应用(如launcher)
  7. 显示——最终展示给用户

3.3 通知更新与取消

// 更新通知(相同id)
notificationManager.notify(NOTIFICATION_ID, newNotification)

// 取消单个通知
notificationManager.cancel(NOTIFICATION_ID)

// 取消所有通知
notificationManager.cancelAll()

源码实现

// NotificationManagerService.java

void cancelNotificationLocked(NotificationRecord r, boolean sendDelete,
        int reason, boolean wasPosted) {

    // 1. 从列表移除
    mNotificationList.remove(r);
    mNotificationsByKey.remove(r.getKey());

    // 2. 取消系统资源
    cancelSoundVibrationLights(r);  // 停止声音/震动/灯光

    // 3. 通知监听器
    if (wasPosted) {
        mListenerManager.notifyRemovedLocked(r, reason);
    }

    // 4. 发送删除Intent(如果应用设置了)
    if (sendDelete && r.getNotification().deleteIntent != null) {
        r.getNotification().deleteIntent.send();
    }

    // 5. 更新状态栏
    mHandler.post(() -> mStatusBar.removeNotification(r.getKey()));
}

四、通知排序与分组

4.1 RankingHelper排序逻辑

NMS通过RankingHelper对通知进行智能排序:

// RankingHelper.java

void sort(ArrayList<NotificationRecord> notificationList) {
    Collections.sort(notificationList, mRankingComparator);
}

// 排序比较器
private final Comparator<NotificationRecord> mRankingComparator = (a, b) -> {
    // 1. 前台服务通知优先
    int aFg = a.isForegroundService() ? 1 : 0;
    int bFg = b.isForegroundService() ? 1 : 0;
    if (aFg != bFg) return bFg - aFg;

    // 2. 用户锁定的通知优先
    int aLocked = a.isUserLocked() ? 1 : 0;
    int bLocked = b.isUserLocked() ? 1 : 0;
    if (aLocked != bLocked) return bLocked - aLocked;

    // 3. 重要性高的优先
    int aImp = a.getImportance();
    int bImp = b.getImportance();
    if (aImp != bImp) return bImp - aImp;

    // 4. 相同重要性,最近的优先
    return Long.compare(b.getRankingTimeMs(), a.getRankingTimeMs());
};

排序优先级

  1. 前台服务通知(最高)
  2. 用户锁定的通知
  3. 重要性级别
  4. 时间(最近的优先)

4.2 通知分组

Android支持将相同应用的通知分组显示:

// 设置分组key
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setSmallIcon(R.drawable.ic_message)
    .setContentTitle("新消息")
    .setGroup("message_group")  // 分组key
    .build()

// 创建分组摘要通知
val summaryNotification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setSmallIcon(R.drawable.ic_message)
    .setContentTitle("5条新消息")
    .setGroup("message_group")
    .setGroupSummary(true)  // 标记为分组摘要
    .build()

notificationManager.notify(SUMMARY_ID, summaryNotification)

分组效果

  • 收起状态显示摘要(如"5条新消息")
  • 展开后显示所有通知
  • 省空间,提升可读性

4.3 通知类别(Category)

// 通知类别,用于系统优化显示
notification.category = Notification.CATEGORY_MESSAGE;  // 消息类
// 其他类别:
// - CATEGORY_CALL: 来电
// - CATEGORY_ALARM: 闹钟
// - CATEGORY_EMAIL: 邮件
// - CATEGORY_EVENT: 日历事件
// - CATEGORY_PROMO: 促销信息
// - CATEGORY_PROGRESS: 进度通知

系统根据类别做特殊处理:

  • CATEGORY_CALL在锁屏显示全屏界面
  • CATEGORY_ALARM绕过免打扰模式
  • CATEGORY_PROGRESS不计入通知数量

五、免打扰(DND)模式

5.1 免打扰模式的三种类型

// ZenModeHelper.java

public static final int ZEN_MODE_OFF = 0;              // 关闭
public static final int ZEN_MODE_IMPORTANT_INTERRUPTIONS = 1;  // 仅重要通知
public static final int ZEN_MODE_NO_INTERRUPTIONS = 2;  // 完全静音
public static final int ZEN_MODE_ALARMS = 3;           // 仅闹钟

5.2 免打扰规则

用户可以配置哪些通知被允许:

// ZenModeConfig.java

public static class ZenRule {
    boolean allowCalls;            // 允许来电
    boolean allowMessages;         // 允许消息
    boolean allowEvents;           // 允许日历事件
    boolean allowReminders;        // 允许提醒
    boolean allowRepeatCallers;    // 允许重复来电
    boolean allowAlarms;           // 允许闹钟
    boolean allowMedia;            // 允许媒体声音
    boolean allowSystem;           // 允许系统声音

    // 来电/消息的来源过滤
    int allowCallsFrom = ZenModeConfig.SOURCE_ANYONE;
    int allowMessagesFrom = ZenModeConfig.SOURCE_ANYONE;
    // 可选值:
    // - SOURCE_ANYONE: 任何人
    // - SOURCE_CONTACT: 联系人
    // - SOURCE_STAR: 星标联系人
}

5.3 通知是否被拦截的判定

// ZenModeHelper.java

boolean shouldIntercept(NotificationRecord record) {
    if (mZenMode == ZEN_MODE_OFF) {
        return false;  // 免打扰关闭,不拦截
    }

    // 1. 前台服务通知不拦截
    if (record.isForegroundService()) {
        return false;
    }

    // 2. 检查通知类别
    String category = record.getNotification().category;
    if (Notification.CATEGORY_ALARM.equals(category)) {
        // 闹钟类通知,检查规则
        return !mConfig.allowAlarms;
    }

    if (Notification.CATEGORY_CALL.equals(category)) {
        // 来电类通知,检查来源
        return !mConfig.allowCalls
            || !isFromAllowedSource(record, mConfig.allowCallsFrom);
    }

    // 3. 检查渠道是否可绕过DND
    NotificationChannel channel = record.getChannel();
    if (channel.canBypassDnd()) {
        return false;
    }

    // 4. 完全静音模式拦截所有
    if (mZenMode == ZEN_MODE_NO_INTERRUPTIONS) {
        return true;
    }

    // 5. 检查重要性
    if (record.getImportance() < IMPORTANCE_DEFAULT) {
        return true;  // 低重要性被拦截
    }

    return false;
}

5.4 自动规则

Android支持基于时间/地点的自动免打扰:

// 创建自动规则:工作日晚上10点到早上8点
val rule = AutomaticZenRule(
    name = "睡眠时段",
    interruptionFilter = INTERRUPTION_FILTER_PRIORITY,  // 仅重要通知
    conditionId = ZenModeConfig.toScheduleConditionId(
        ScheduleInfo().apply {
            days = intArrayOf(
                Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
                Calendar.THURSDAY, Calendar.FRIDAY
            )
            startHour = 22
            startMinute = 0
            endHour = 8
            endMinute = 0
        }
    ),
    enabled = true
)

notificationManager.addAutomaticZenRule(rule)

六、通知监听器服务(NotificationListenerService)

6.1 什么是NotificationListenerService?

NLS允许应用监听系统中所有应用的通知,常用于:

  • 智能手表同步通知
  • 通知管理应用
  • 自动化工具(如Tasker)
  • 辅助功能服务

6.2 实现NotificationListenerService

class MyNotificationListener : NotificationListenerService() {

    // 通知发布时回调
    override fun onNotificationPosted(sbn: StatusBarNotification) {
        val packageName = sbn.packageName
        val notification = sbn.notification
        val extras = notification.extras

        val title = extras.getString(Notification.EXTRA_TITLE)
        val text = extras.getString(Notification.EXTRA_TEXT)

        Log.d(TAG, "New notification: $packageName - $title: $text")

        // 可以进行处理,如:
        // - 同步到穿戴设备
        // - 智能分类
        // - 自动回复
    }

    // 通知移除时回调
    override fun onNotificationRemoved(sbn: StatusBarNotification) {
        Log.d(TAG, "Notification removed: ${sbn.packageName}")
    }

    // 通知排序变化时回调
    override fun onNotificationRankingUpdate(rankingMap: RankingMap) {
        // 通知的排序/重要性发生变化
    }

    // 监听器连接成功
    override fun onListenerConnected() {
        // 可以获取当前所有活跃通知
        val activeNotifications = getActiveNotifications()
    }

    // 可以主动取消通知
    fun dismissNotification(key: String) {
        cancelNotification(key)
    }
}

注册服务

<!-- AndroidManifest.xml -->
<service
    android:name=".MyNotificationListener"
    android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
    android:exported="true">
    <intent-filter>
        <action android:name="android.service.notification.NotificationListenerService" />
    </intent-filter>
</service>

用户授权

// 检查是否有权限
fun isNotificationListenerEnabled(context: Context): Boolean {
    val listeners = Settings.Secure.getString(
        context.contentResolver,
        "enabled_notification_listeners"
    )
    return listeners?.contains(context.packageName) == true
}

// 引导用户授权
fun requestNotificationListenerPermission(context: Context) {
    val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)
    context.startActivity(intent)
}

6.3 源码分析:监听器回调

// NotificationManagerService.java

void notifyPostedLocked(NotificationRecord r, NotificationRecord old) {
    // 遍历所有注册的监听器
    for (ManagedServiceInfo info : mServices) {
        boolean sbnVisible = isVisibleToListener(r.sbn, info);
        boolean oldSbnVisible = old != null
            ? isVisibleToListener(old.sbn, info)
            : false;

        if (oldSbnVisible && !sbnVisible) {
            // 对此监听器不可见了,回调removed
            mHandler.post(() -> notifyRemoved(info, old.sbn));
        } else if (!oldSbnVisible && sbnVisible) {
            // 对此监听器变为可见,回调posted
            mHandler.post(() -> notifyPosted(info, r.sbn));
        } else if (sbnVisible) {
            // 可见且更新,回调posted
            mHandler.post(() -> notifyPosted(info, r.sbn));
        }
    }
}

七、通知限制策略

7.1 速率限制

防止应用刷屏:

// NotificationManagerService.java

private static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5f;  // 每秒5条

// 检查速率
float getAppEnqueueRate(String pkg) {
    return mUsageStats.getAppEnqueueRate(pkg);
}

// 超过速率后延迟
if (rate > mMaxPackageEnqueueRate) {
    long snoozeDuration = (long) (rate / mMaxPackageEnqueueRate * 1000);
    mSnoozeHelper.snooze(r, snoozeDuration);
}

7.2 通知数量限制

// 每个应用最多50条活跃通知
private static final int MAX_PACKAGE_NOTIFICATIONS = 50;

// 超过限制,移除最老的通知
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
    NotificationRecord oldest = findOldestNotification(pkg);
    cancelNotificationLocked(oldest, false, REASON_PACKAGE_LIMIT);
}

7.3 后台通知限制

// Android 8.0+ 后台应用发送通知需要前台服务或特殊权限

boolean canShowNotificationWhileBackground(String pkg, int uid) {
    // 1. 有前台服务 → 允许
    if (mAm.hasRunningForegroundService(pkg, uid)) {
        return true;
    }

    // 2. 白名单应用 → 允许
    if (isWhitelisted(pkg)) {
        return true;
    }

    // 3. 最近有用户交互 → 短暂允许
    if (mUsageStats.isRecentlyUsed(pkg, uid)) {
        return true;
    }

    return false;
}

八、调试与问题诊断

8.1 常用调试命令

# 查看NMS状态
adb shell dumpsys notification

# 输出包含:
# - 所有活跃通知
# - 通知渠道配置
# - 免打扰规则
# - 排序信息
# - 速率限制状态

# 查看特定应用的通知
adb shell dumpsys notification | grep -A 20 "com.example.app"

# 查看通知渠道
adb shell dumpsys notification | grep -A 5 "NotificationChannel"

# 查看免打扰配置
adb shell dumpsys notification | grep -A 20 "zen mode"

# 模拟通知(需root或调试应用)
adb shell cmd notification post -t "Test Title" "Test Body"

8.2 通知不显示问题排查

问题1:通知完全不显示

# 1. 检查应用是否被禁用通知
adb shell cmd notification allowed_listeners
adb shell cmd notification disallowed_assistants

# 2. 检查渠道是否存在且启用
adb shell dumpsys notification | grep -A 10 "com.example.app"
# 查看 channel importance 是否为 NONE

# 3. 检查是否在免打扰模式
adb shell dumpsys notification | grep "mZenMode"

# 4. 检查权限
adb shell dumpsys notification | grep "NotificationAccess"

问题2:通知无声音/震动

# 检查渠道配置
adb shell dumpsys notification | grep -A 5 "sound\|vibration"

# 检查系统音量
adb shell dumpsys audio | grep "volume"

问题3:通知被延迟

# 检查速率限制
adb shell dumpsys notification | grep "enqueue rate"

# 检查Doze模式
adb shell dumpsys deviceidle

8.3 通知日志分析

// 开启通知日志
adb shell cmd notification set_log_level 2  // 0=OFF, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG

// 查看日志
adb logcat -s NotificationManager:* NotificationService:*

// 常见日志:
// - "Posting notification" - 通知发送
// - "Canceling notification" - 通知取消
// - "Ranking updated" - 排序更新
// - "Intercepted by zen mode" - 被免打扰拦截

九、最佳实践

9.1 正确创建通知渠道

// ✅ 正确:在Application.onCreate中创建渠道
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        createNotificationChannels()
    }

    private fun createNotificationChannels() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channels = listOf(
                NotificationChannel(
                    "important_msg",
                    "重要消息",
                    NotificationManager.IMPORTANCE_HIGH
                ).apply {
                    description = "需要立即查看的重要消息"
                    enableVibration(true)
                    enableLights(true)
                    lightColor = Color.RED
                },
                NotificationChannel(
                    "general",
                    "一般通知",
                    NotificationManager.IMPORTANCE_DEFAULT
                ).apply {
                    description = "常规消息和更新"
                }
            )

            notificationManager.createNotificationChannels(channels)
        }
    }
}

// ❌ 错误:发送通知时才创建渠道
fun showNotification() {
    // 太晚了!应该在应用启动时就创建
    notificationManager.createNotificationChannel(channel)
    notificationManager.notify(ID, notification)
}

9.2 合理设置重要性级别

// ✅ 正确:根据通知类型选择合适的重要性
val channels = mapOf(
    "chat" to IMPORTANCE_HIGH,        // 即时消息 - 高
    "news" to IMPORTANCE_LOW,         // 新闻推送 - 低
    "download" to IMPORTANCE_MIN,     // 下载进度 - 最小
    "promotion" to IMPORTANCE_LOW     // 促销信息 - 低
)

// ❌ 错误:所有通知都设置为HIGH
// 用户会觉得烦,直接禁用你的应用通知

9.3 响应用户的渠道设置

// 检查渠道是否被用户禁用
fun isChannelBlocked(channelId: String): Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val channel = notificationManager.getNotificationChannel(channelId)
        return channel?.importance == NotificationManager.IMPORTANCE_NONE
    }
    return false
}

// 引导用户开启渠道
fun promptToEnableChannel(channelId: String) {
    if (isChannelBlocked(channelId)) {
        AlertDialog.Builder(context)
            .setTitle("通知已关闭")
            .setMessage("您已关闭此类通知,可能会错过重要消息")
            .setPositiveButton("去设置") { _, _ ->
                // 跳转到渠道设置
                val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
                    putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
                    putExtra(Settings.EXTRA_CHANNEL_ID, channelId)
                }
                startActivity(intent)
            }
            .setNegativeButton("取消", null)
            .show()
    }
}

9.4 正确使用通知分组

// ✅ 正确:消息类应用使用分组
val GROUP_KEY = "message_group"

// 发送多条通知
messages.forEach { msg ->
    val notification = NotificationCompat.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_message)
        .setContentTitle(msg.sender)
        .setContentText(msg.content)
        .setGroup(GROUP_KEY)  // 设置分组
        .build()

    notificationManager.notify(msg.id, notification)
}

// 发送分组摘要(消息数>1时)
if (messages.size > 1) {
    val summaryNotification = NotificationCompat.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_message)
        .setContentTitle("${messages.size}条新消息")
        .setGroup(GROUP_KEY)
        .setGroupSummary(true)  // 标记为摘要
        .build()

    notificationManager.notify(SUMMARY_ID, summaryNotification)
}

9.5 避免通知滥用

行为影响正确做法
短时间发送大量通知触发速率限制合并为一条或使用分组
所有通知都设HIGH用户禁用应用通知仅紧急消息用HIGH
深夜发送非紧急通知打扰用户休息尊重免打扰模式
过度使用声音/震动引起反感只在必要时使用
通知内容过长显示被截断使用BigTextStyle展开

十、Android 15新特性

10.1 增强的通知权限

// Android 13+ 需要运行时权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
        != PackageManager.PERMISSION_GRANTED) {

        // 请求通知权限
        requestPermissions(
            arrayOf(Manifest.permission.POST_NOTIFICATIONS),
            REQUEST_CODE
        )
    }
}

10.2 通知预测与智能排序

// Android 15使用机器学习预测通知重要性
class SmartRankingHelper extends RankingHelper {
    // 基于用户行为学习
    void updateImportanceByUserInteraction(NotificationRecord r, int action) {
        // action: DISMISSED, CLICKED, SNOOZED, etc.

        // 记录用户行为
        mUserInteractionLog.log(r.getPackageName(), r.getChannel(), action);

        // 更新预测模型
        if (mMLModel != null) {
            mMLModel.train(getUserInteractionData());
        }

        // 动态调整排序权重
        float predictedImportance = mMLModel.predict(r);
        r.setRankingScore(predictedImportance);
    }
}

10.3 隐私保护增强

// Android 15对锁屏通知的隐私保护更严格
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setSmallIcon(R.drawable.icon)
    .setContentTitle("新消息")
    .setContentText("您有一条新消息")
    // 设置锁屏可见性
    .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)  // 锁屏不显示内容
    // 设置公开版本(锁屏显示的简化版)
    .setPublicVersion(
        NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.icon)
            .setContentTitle("新消息")
            .build()
    )
    .build()

10.4 对话通知(Conversation Notifications)

// Android 11+引入,Android 15进一步优化
val person = Person.Builder()
    .setName("张三")
    .setIcon(IconCompat.createWithResource(context, R.drawable.avatar))
    .build()

val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setSmallIcon(R.drawable.ic_message)
    .setContentTitle("张三")
    .setContentText("你好!")
    // 标记为对话通知
    .setStyle(
        NotificationCompat.MessagingStyle(person)
            .addMessage("你好!", System.currentTimeMillis(), person)
    )
    .setShortcutId("chat_zhangsan")  // 关联聊天快捷方式
    .setCategory(NotificationCompat.CATEGORY_MESSAGE)
    .build()

对话通知优势

  • 在通知抽屉中优先显示
  • 支持气泡(Bubbles)浮窗
  • 快捷回复更便捷

总结

本篇我们深入分析了Android通知系统的核心机制:

  1. NotificationManagerService架构:四层架构,核心组件协作
  2. 通知渠道机制:精细化控制,用户至上的设计理念
  3. 通知发送流程:从应用到显示的完整链路
  4. 排序与分组:智能排序,分组显示提升体验
  5. 免打扰模式:灵活的规则配置,平衡打扰与提醒
  6. 监听器服务:强大的通知监听能力
  7. 限制策略:速率限制、数量限制、后台限制
  8. 调试与最佳实践:完善的调试工具和开发建议

关键设计思想

  • 用户至上:用户的设置优先级高于应用
  • 精细控制:渠道机制实现类型级别的管理
  • 隐私保护:锁屏通知可隐藏敏感内容
  • 智能排序:根据重要性、时间、用户习惯排序

下一篇预告:《JobScheduler与WorkManager:任务调度机制》将分析Android的后台任务调度系统,理解JobScheduler和WorkManager的工作原理与最佳实践。


参考资料


系列导航

本文基于Android 15 (API Level 35)源码分析,不同厂商的定制ROM可能存在差异。 欢迎来我中的个人主页找到更多有用的知识和有趣的产品