
Android 通知系统是平台最复杂的子系统之一。应用调用一次notify(),会触发一连串权限校验、通道查找、信号提取、优先级排序、勿扰模式过滤、监听器分发,最终在 SystemUI 内完成 UI 渲染。在 Android 17 中,核心服务NotificationManagerService.java代码量就超过 16500 行,协同 70 余个辅助类完成工作。本章将完整追踪通知的生命周期:从公开 API,历经服务端处理流水线、排序引擎、提醒效果,直至 SystemUI 通知面板;最后介绍 Android 17 新增的通知界面:富常驻通知、系统管理通知规则与上下文模式、礼貌通知、AI 摘要。28.1 通知架构28.1.1 高层概览Android 通知子系统分为四层:应用程序 | v NotificationManager (android.app.NotificationManager) -- 公开SDK API | (Binder IPC) v NotificationManagerService (system_server) -- 策略引擎 | +--- RankingHelper (信号提取与排序) +--- PreferencesHelper (通道、分组、应用私有配置) +--- ZenModeHelper (勿扰模式) +--- NotificationAttentionHelper (声音、振动、LED) +--- GroupHelper (自动分组) +--- SnoozeHelper (定时暂存) +--- ShortcutHelper (会话快捷方式) +--- NotificationListeners (监听器分发) +--- NotificationAssistants (NAS调整) | v SystemUI (通知面板、悬浮通知、气泡、状态栏图标)连接应用与服务的 Binder 接口定义在INotificationManager.aidl。NotificationManager的每一次notify()、cancel()调用都会通过 Binder 跨进程进入运行在system_server中的NotificationManagerService。源码文件:组件路径NotificationManagerframeworks/base/core/java/android/app/NotificationManager.javaINotificationManagerframeworks/base/core/java/android/app/INotificationManager.aidlNotificationManagerServiceframeworks/base/services/core/java/com/android/server/notification/NotificationManagerService.javaNotificationframeworks/base/core/java/android/app/Notification.javaNotificationChannelframeworks/base/core/java/android/app/NotificationChannel.javaStatusBarNotificationframeworks/base/core/java/android/service/notification/StatusBarNotification.java28.1.2 Notification 对象Notification对象是核心数据结构,包含:小图标(必填)—— 显示在状态栏以及通知面板头部内容标题与文本 —— 主要文本内容通道 ID(API 26 起必填)—— 管控通知行为的通道PendingIntent—— 点击动作(contentIntent),可选deleteIntent删除意图操作按钮 —— 最多三个按钮(包含行内回复)样式 ——BigTextStyle、BigPictureStyle、InboxStyle、MessagingStyle、MediaStyle、CallStyle、DecoratedCustomViewStyle、DecoratedMediaCustomViewStyle之一附加数据 ——Bundle,存放任意键值对BubbleMetadata—— 气泡展示的可选元数据标志位 —— 位掩码,例如FLAG_FOREGROUND_SERVICE、FLAG_AUTO_CANCEL、FLAG_GROUP_SUMMARY、FLAG_BUBBLE、FLAG_ONGOING_EVENT、FLAG_NO_CLEAR28.1.3 关键常量与限制来自NotificationManagerService:// frameworks/base/services/core/java/com/android/server/notification/ // NotificationManagerService.java (lines 493-494) static final int MAX_PACKAGE_NOTIFICATIONS = 50; static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5f;单个应用包最多允许 50 条活跃通知。入队速率限制为每秒 5 条,防止通知洪泛。PreferencesHelper额外施加的限制:// frameworks/base/services/core/java/com/android/server/notification/ // PreferencesHelper.java (lines 133-135) static final int NOTIFICATION_CHANNEL_COUNT_LIMIT = 5000; static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 6000;28.1.4 架构图28.1.5 线程模型NotificationManagerService绝大多数操作使用独立工作线程。应用发来的 Binder 调用运行在 Binder 线程池,实际业务处理投递到 Handler 线程:// NotificationManagerService.java (lines 500-506) // message codes static final int MESSAGE_DURATION_REACHED = 2; static final int MESSAGE_SEND_RANKING_UPDATE = 4; static final int MESSAGE_LISTENER_HINTS_CHANGED = 5; static final int MESSAGE_LISTENER_NOTIFICATION_FILTER_CHANGED = 6; static final int MESSAGE_FINISH_TOKEN_TIMEOUT = 7; static final int MESSAGE_ON_PACKAGE_CHANGED = 8;另有独立排序线程:// NotificationManagerService.java (lines 511-512) // ranking thread messages private static final int MESSAGE_RECONSIDER_RANKING = 1000; private static final int MESSAGE_RANKING_SORT = 1001;核心同步锁mNotificationLock:对mNotificationList、mNotificationsByKey、mSummaryByGroupKey、mEnqueuedNotifications的全部读写操作,必须持有mNotificationLock锁。28.1.6 信号提取器流水线排序流水线由一系列NotificationSignalExtractor实现类驱动,通过 XML 配置反射加载:!-- frameworks/base/core/res/res/values/config.xml (line 3845) -- string-array itemcom.android.server.notification.NotificationChannelExtractor/item itemcom.android.server.notification.NotificationAdjustmentExtractor/item itemcom.android.server.notification.BubbleExtractor/item itemcom.android.server.notification.ValidateNotificationPeople/item itemcom.android.server.notification.PriorityExtractor/item itemcom.android.server.notification.ZenModeExtractor/item itemcom.android.server.notification.ImportanceExtractor/item itemcom.android.server.notification.VisibilityExtractor/item itemcom.android.server.notification.BadgeExtractor/item itemcom.android.server.notification.CriticalNotificationExtractor/item /string-array接口定义十分精简:// frameworks/base/services/core/java/com/android/server/notification/ // NotificationSignalExtractor.java public interface NotificationSignalExtractor { public void initialize(Context context, NotificationUsageStats usageStats); public RankingReconsideration process(NotificationRecord notification); void setConfig(RankingConfig config); void setZenHelper(ZenModeHelper helper); }每个提取器按顺序对每条通知执行。如果提取器返回非空RankingReconsideration,任务投递到排序线程做延迟重新评估(ValidateNotificationPeople用于异步联系人查找)。处理链路:NotificationRecord→ChannelExtractor→AdjustmentExtractor→BubbleExtractor→ValidateNotificationPeople→PriorityExtractor→ZenModeExtractor→ImportanceExtractor→VisibilityExtractor→BadgeExtractor→CriticalNotificationExtractor→ 已排序通知列表28.1.7 NotificationRecordNotificationRecord是服务端对StatusBarNotification的包装类,保存信号提取器产出的全部中间状态。// frameworks/base/services/core/java/com/android/server/notification/ // NotificationRecord.java (lines 108-248, summarized) public final class NotificationRecord { private final StatusBarNotification sbn; private float mContactAffinity; private boolean mIntercept; // 被勿扰模式拦截 private long mRankingTimeMs; private int mImportance; private int mSystemImportance; private int mAssistantImportance; private float mRankingScore; private int mCriticality; private NotificationChannel mChannel; private ShortcutInfo mShortcutInfo; private boolean mAllowBubble; private boolean mShowBadge; private int mSuppressedVisualEffects; private ArrayListNotification.Action mSystemGeneratedSmartActions; private ArrayListCharSequence mSmartReplies; private int mUserSentiment; private boolean mIsInterruptive; // ...更多字段 }类注释给出关键线程约束:修改该类对象必须持有NotificationManagerService.mNotificationLock锁;修改完成后必须重新排序列表。28.2 NotificationManagerService:入队、发布、取消28.2.1 通知生命周期概览通知在服务端经历三个阶段:入队 (Enqueue):校验、通道查找、速率限制、创建NotificationRecord发布 (Post):信号提取、排序、提醒效果、监听器分发取消 (Cancel):从列表移除、通知监听器、归档历史流程时序:28.2.2 阶段 1:入队Binder 调用入口为内部类INotificationManager.Stub:// NotificationManagerService.java (line 4616) public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id, Notification notification, int userId) throws RemoteException { enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid(), Binder.getCallingPid(), tag, id, notification, userId, /* byForegroundService= */ false, /* isAppProvided= */ true); }私有方法enqueueNotificationInternal()(从 9346 行开始)执行如下步骤:步骤 1 — 参数校验if (pkg == null || notification == null) { throw new IllegalArgumentException("null not allowed: pkg=" + pkg + " notification=" + notification); }步骤 2 — 用户 ID 解析final int userId = ActivityManager.handleIncomingUser(callingPid, callingUid, incomingUserId, true, false, "enqueueNotification", pkg);步骤 3 — UID 解析与安全校验notificationUid = resolveNotificationUid(opPkg, pkg, callingUid, userId); if (notificationUid == INVALID_UID) { throw new SecurityException("Caller " + opPkg + ":" + callingUid + " trying to post for invalid pkg " + pkg); }步骤 4 — 前台服务策略校验final ServiceNotificationPolicy policy = mAmi.applyForegroundServiceNotification( notification, tag, id, pkg, userId);步骤 5 — 修正通知内容:fixNotification()做内容净化:移除非法操作按钮强制文本最大长度前台服务通知设置FLAG_NO_CLEAR确保通知具备合法通道 ID步骤 6 — 查找通知通道getNotificationChannelRestoreDeleted()定义在 9571 行,enqueueNotificationInternal()大约 9447 行调用final NotificationChannel channel = getNotificationChannelRestoreDeleted( pkg, callingUid, notificationUid, channelId, shortcutId); if (channel == null) { // ...弹出Toast警告,返回false }步骤 7 — 创建 NotificationRecordfinal NotificationRecord r = new NotificationRecord(getContext(), n, channel); r.setIsAppImportanceLocked(mPermissionHelper.isPermissionUserSet(pkg, userId)); r.setPostSilently(postSilently); r.setFlagBubbleRemoved(false); r.setPkgAllowedAsConvo(mMsgPkgsAllowedAsConvos.contains(pkg));步骤 8 — 前台服务重要性下限:如果通知归属前台服务或者用户发起任务,通道重要性为MIN或NONE,则提升至LOWif (notification.isFgsOrUij()) { if (r.getImportance() == IMPORTANCE_MIN || r.getImportance() == IMPORTANCE_NONE) { channel.setImportance(IMPORTANCE_LOW); r.setSystemImportance(IMPORTANCE_LOW); } }步骤 9 — 获取唤醒锁,调度 EnqueueNotificationRunnablePostNotificationTracker tracker = acquireWakeLockForPost(pkg, callingUid);唤醒锁超时 30 秒(POST_WAKE_LOCK_TIMEOUT),保证设备保持唤醒直至通知发布完成。28.2.3 EnqueueNotificationRunnable源码:NotificationManagerService.java,10715 行。该 Runnable 运行在 Handler 线程,执行时持有mNotificationLock锁。protected class EnqueueNotificationRunnable implements Runnable { private final NotificationRecord r; private final int userId; // ...核心操作:暂存状态检查:如果该通知 key 曾经被暂存,且暂存时间未到期,则立刻重新暂存final long snoozeAt = mSnoozeHelper.getSnoozeTimeForUnpostedNotification( r.getUser().getIdentifier(), r.getSbn().getPackageName(), r.getSbn().getKey()); if (snoozeAt currentTime) { (new SnoozeNotificationRunnable(r.getSbn().getKey(), snoozeAt - currentTime, null)).snoozeLocked(r); return false; }拷贝旧通知排序信息:如果是更新已有通知,保留排序相关信息NotificationRecord old = mNotificationsByKey.get(n.getKey()); if (old != null) { r.copyRankingInformation(old); }加入入队列表mEnqueuedNotifications.add(r); mTtlHelper.scheduleTimeoutLocked(r, SystemClock.elapsedRealtime());更新气泡标志位updateNotificationBubbleFlags(r, isAppForeground);分组通知处理handleGroupedNotificationLocked(r, old, callingUid, callingPid);调度 PostNotificationRunnable:NAS 启用则延迟 200ms,否则立即执行if (mAssistants.isEnabled()) { mAssistants.onNotificationEnqueuedLocked(r); mHandler.postDelayed( new PostNotificationRunnable(r.getKey(), ...), DELAY_FOR_ASSISTANT_TIME); // 200ms } else { mHandler.post(new PostNotificationRunnable(r.getKey(), ...)); }28.2.4 PostNotificationRunnable¶源码:NotificationManagerService.java,10885 行。通知变为可见的逻辑,运行持有mNotificationLock锁。步骤 1 — 在入队列表查找记录NotificationRecord r = findNotificationByListLocked(mEnqueuedNotifications, key); if (r == null) { Slog.i(TAG, "Cannot find enqueued record for key: " + key); return false; }步骤 2 — 屏蔽校验媒体通知、通话样式通知跳过屏蔽校验;其余通知,若应用被禁用或通道被屏蔽,则直接丢弃if (!(notification.isMediaNotification() || isCallNotificationAndCorrectStyle) (appBanned || isRecordBlockedLocked(r))) { mUsageStats.registerBlocked(r); return false; }步骤 3 — 添加至主通知列表int index = indexOfNotificationLocked(n.getKey()); if (index 0) { mNotificationList.add(r); // 新建通知 mUsageStats.registerPostedByApp(r); } else { old = mNotificationList.get(index); // 更新已有通知 mNotificationList.set(index, r); mUsageStats.registerUpdatedByApp(r, old); } mNotificationsByKey.put(n.getKey(), r);步骤 4 — 自动分组:GroupHelper.onNotificationPosted()判断是否自动分组boolean willBeAutogrouped = mGroupHelper.onNotificationPosted( r, hasAutoGroupSummaryLocked(r)); if (willBeAutogrouped) { addAutogroupKeyLocked(key, autogroupName, /*requestSort=*/false); }步骤 5 — 信号提取与排序mRankingHelper.extractSignals(r); mRankingHelper.sort(mNotificationList);步骤 6 — 提醒效果(蜂鸣、振动、闪烁 LED)buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r, new NotificationAttentionHelper.Signals( mUserProfiles.isCurrentProfile(r.getUserId()), mListenerHints));步骤 7 — 分发监听器回调通知必须携带小图标才允许发布;没有小图标直接拒绝if (notification.getSmallIcon() != null) { notifyListenersPostedAndLogLocked(r, old, mTracker, maybeReport); posted = true; } else { Slog.e(TAG, "Not posting notification without small icon: " + notification); }步骤 8 — 清理入队列表for (int i = 0; i N; i++) { final NotificationRecord enqueued = mEnqueuedNotifications.get(i); if (Objects.equals(key, enqueued.getKey())) { mEnqueuedNotifications.remove(i); break; } }28.2.5 阶段 3:取消取消通知有多种来源:取消原因常量触发条件REASON_CLICK1用户点击通知REASON_CANCEL2用户侧滑清除通知REASON_CANCEL_ALL3用户点击 “全部清除”REASON_APP_CANCEL8应用调用 cancel ()REASON_APP_CANCEL_ALL9应用调用 cancelAll ()REASON_LISTENER_CANCEL10NotificationListenerService.cancelNotification()REASON_PACKAGE_BANNED7应用通知整体被禁用REASON_CHANNEL_BANNED17通道重要性设置为 NONEREASON_SNOOZED18用户暂存通知REASON_TIMEOUT19TTL 超时(默认 3 天)REASON_GROUP_OPTIMIZATION13通知重新分组原因常量定义:frameworks/base/core/java/android/service/notification/NotificationListenerService.java234‑291 行,完整表格见 28.17 节。取消逻辑运行在 Handler 线程,CancelNotificationRunnable(10530 行),由各类cancelNotification()入口调度。代表性 Binder 入口:// NotificationManagerService.java (line 8890) public void cancelNotification(String pkg, String opPkg, int callingUid, int callingPid, String tag, int id, int userId) { // ... 持有mNotificationLock调用cancelNotificationLocked(...) }私有方法cancelNotificationLocked()(12037 行)执行:发送 deleteIntent(如果通知配置)if (sendDelete) { sendDeleteIntent(r.getNotification().deleteIntent, r.getSbn().getPackageName()); }通知监听器通知已移除mListeners.notifyRemovedLocked(r, reason, r.getStats());通知 GroupHelper 处理分组变更mGroupHelper.onNotificationRemoved(r, mNotificationList, sendDelete);清除提醒效果mAttentionHelper.clearEffectsLocked(canceledKey);更新分组汇总跟踪if (groupSummary != null groupSummary.getKey().equals(canceledKey)) { mSummaryByGroupKey.remove(groupKey); }归档历史if (reason != REASON_CHANNEL_REMOVED) { mArchive.record(getSbnForArchive(r, reason), reason); }28.2.6 TTL 与通知超时通知默认存活时间 3 天:// NotificationManagerService.java (line 682) static final long NOTIFICATION_TTL = Duration.ofDays(3).toMillis();发布时,创建时间超过 14 天的通知直接拒绝:// NotificationManagerService.java (line 684) static final long NOTIFICATION_MAX_AGE_AT_POST = Duration.ofDays(14).toMillis();TimeToLiveHelper调度闹钟,TTL 到期自动取消通知,避免僵尸通知永久驻留。28.2.7 速率限制服务对每个应用包做速率限制,防止通知风暴:static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5f;Toast 通知拥有更严格限流:// NotificationManagerService.java (lines 559-563) private static final MultiRateLimiter.RateLimit[] TOAST_RATE_LIMITS = { MultiRateLimiter.RateLimit.create(3, Duration.ofSeconds(20)), MultiRateLimiter.RateLimit.create(5, Duration.ofSeconds(42)), MultiRateLimiter.RateLimit.create(6, Duration.ofSeconds(68)), };28.2.8 完整发布流程图28.3 通知通道与分组28.3.1 通道简介Android 8.0(API 26)起,每一条通知必须归属一个NotificationChannel。通道让用户细粒度控制通知行为:重要等级、声音、振动、LED、角标显示,全部按通道配置。通道一旦创建,行为配置归用户所有,应用无法通过程序修改。源码:frameworks/base/core/java/android/app/NotificationChannel.java28.3.2 通道属性属性方法说明IDgetId()不可变字符串标识名称getName()用户可见名称描述getDescription()用户可见描述重要等级getImportance()控制打扰等级提