Android的通知系统比较庞大复杂,牵扯到系统服务NotificationManagerService,SystemUI以及Launcher,本文将基于Android12L从源码角度分析一下NotificationManagerService流程,通知其他部分将分为多个章节展开。
一、常用发送通知方式
scss
// 创建一个NotificationManager实例
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// 创建一个NotificationCompat.Builder实例
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("通知标题")
.setContentText("通知内容")
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
// 发送通知
notificationManager.notify(NOTIFICATION_ID, builder.build());
NotificationManager最终调用了notifyAsUser方法。
scss
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user){
INotificationManager service = getService();
String pkg = mContext.getPackageName();
try {
if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
fixNotification(notification), user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
可以看到具体工作是交给了系统服务INotificationManager,其实现类就是NotificationManagerService 。
二、NotificationManagerService
NotificationManagerService 是在系统启动时在SystemService.startOtherServicess的时候通过mSystemServiceManager.startService(NotificationManagerService.class)方式启动的,并且加入到了系统服务的列表当中,在Android系统中可以通过ServiceManager.getService("notification")拿到这个服务,后面统称为NMS。
2.1-NotificationManagerService.enqueueNotificationInternal
前面讲到NotificationManager调用了NMS.enqueueNotificationWithTag方法,其实内部通过几次跳转最终走到了NMS.enqueueNotificationInternal方法。我们来看下具体实现。
[->NotificationManagerService.java]
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 incomingUserId, boolean postSilently) {
//权限检查
、、、
// Fix the notification as best we can.
try {
fixNotification(notification, pkg, tag, id, userId);
} catch (Exception e) {
return;
}
final StatusBarNotification n = new StatusBarNotification(
pkg, opPkg, id, tag, notificationUid, callingPid, notification,
user, null, System.currentTimeMillis());
String shortcutId = n.getShortcutId();
final NotificationChannel channel = mPreferencesHelper.getConversationNotificationChannel(
pkg, notificationUid, channelId, shortcutId,
true /* parent ok */, false /* includeDeleted */);
//必须要有channel
if (channel == null) {
、、、
return;
}
final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
r.setIsAppImportanceLocked(mPreferencesHelper.getIsAppImportanceLocked(pkg, callingUid));
r.setPostSilently(postSilently);
r.setFlagBubbleRemoved(false);
r.setPkgAllowedAsConvo(mMsgPkgsAllowedAsConvos.contains(pkg));
、、、
boolean isAppForeground;
try {
isAppForeground = mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND;
} finally {
Binder.restoreCallingIdentity(token);
}
mHandler.post(new EnqueueNotificationRunnable(userId, r, isAppForeground));
}
NMS.fixNotification
1.检查android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS权限有就
notification.flags |= Notification.FLAG_CAN_COLORIZE;无就清空对应标记
2.检查notification.fullScreenIntent,如果没有android.Manifest.permission.USE_FULL_SCREEN_INTENT权限将其置为空。
3.检查通知的Style样式
4.检查RemoteViews的contentView、bigContentView、headsUpContentView是否超出了空间大小限制,主要是通过bitmap所占空间大小判断。
我们看到方法最后通过Handler Post了一个Runable。
2.2-EnqueueNotificationRunnable
[->NotificationManagerService.java]
scss
protected class EnqueueNotificationRunnable implements Runnable {
@Override
public void run() {
synchronized (mNotificationLock) {
、、、
//存进集合,后续会用到
mEnqueuedNotifications.add(r);
// 当用户设置了 Builder.setTimeoutAfter(long durationMs) 则会在这里做处理
scheduleTimeoutLocked(r);
final StatusBarNotification n = r.getSbn();
if (DBG) Slog.d(TAG, "EnqueueNotificationRunnable.run for: " + n.getKey());
NotificationRecord old = mNotificationsByKey.get(n.getKey());
if (old != null) {
// Retain ranking information from previous record
r.copyRankingInformation(old);
}
//成组通知处理
handleGroupedNotificationLocked(r, old, callingUid, callingPid);
、、、
// tell the assistant service about the notification
if (mAssistants.isEnabled()) {
mAssistants.onNotificationEnqueuedLocked(r);
mHandler.postDelayed(new PostNotificationRunnable(r.getKey()),
DELAY_FOR_ASSISTANT_TIME);
} else {
mHandler.post(new PostNotificationRunnable(r.getKey()));
}
}
}
}
将通知存进了mEnqueuedNotifications队列,这个队列保存的是未处理的通知,当通知被取消、超时、处理完成后会从队列移除。还进行了超时逻辑的处理,分组处理,我们看下分组的处理。
2.3-handleGroupedNotificationLocked
[->NotificationManagerService.java]
java
private void handleGroupedNotificationLocked(NotificationRecord r, NotificationRecord old,
int callingUid, int callingPid) {
StatusBarNotification sbn = r.getSbn();
Notification n = sbn.getNotification();
//注释1
if (n.isGroupSummary() && !sbn.isAppGroup()) {
// notifications without a group shouldn't be a summary, otherwise autobundling can
// lead to bugs
n.flags &= ~Notification.FLAG_GROUP_SUMMARY;
}
String group = sbn.getGroupKey();
boolean isSummary = n.isGroupSummary();
Notification oldN = old != null ? old.getSbn().getNotification() : null;
String oldGroup = old != null ? old.getSbn().getGroupKey() : null;
boolean oldIsSummary = old != null && oldN.isGroupSummary();
//注释2
if (oldIsSummary) {
NotificationRecord removedSummary = mSummaryByGroupKey.remove(oldGroup);
if (removedSummary != old) {
String removedKey =
removedSummary != null ? removedSummary.getKey() : "<null>";
Slog.w(TAG, "Removed summary didn't match old notification: old=" + old.getKey() +
", removed=" + removedKey);
}
}
if (isSummary) {
mSummaryByGroupKey.put(group, r);
}
// Clear out group children of the old notification if the update
// causes the group summary to go away. This happens when the old
// notification was a summary and the new one isn't, or when the old
// notification was a summary and its group key changed.
//注释3
if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) {
cancelGroupChildrenLocked(old, callingUid, callingPid, null, false /* sendDelete */,
null, REASON_APP_CANCEL);
}
}
注释1:修正通知,通知是一个summary但是没有设置group,说明用户错误的设置了summary属性,去掉Notification.FLAG_GROUP_SUMMARY标志位。
注释2:如果必要的话更新集合mSummaryByGroupKey,旧的summary通知发生了变化。
注释3:如果旧通知是一条父通知,新通知变成了非父通知;或者旧通知新通知均是父通知,但是group key已经发生了变化,则原来父通知下的所有子通知会被移除
再回到EnqueueNotificationRunnable,在2.2最后我们看到EnqueueNotificationRunnable最后有Post了一个Runable。
2.4-PostNotificationRunnable
[->NotificationManagerService.java]
scss
protected class PostNotificationRunnable implements Runnable {
private final String key;
、、、
public void run() {
synchronized (mNotificationLock) {
try {
NotificationRecord r = null;
int N = mEnqueuedNotifications.size();
//从入队待处理的通知mEnqueuedNotifications中找到要处理的通知
for (int i = 0; i < N; i++) {
final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
if (Objects.equals(key, enqueued.getKey())) {
r = enqueued;
break;
}
}
//没找到,可能被取消了返回
if (r == null) {
return;
}
//检查是避免在中间处理过程中 blocked 属性发生了改变
if (isBlocked(r)) {
return;
}
//该应用是否被系统限制了,是的话hidden为true,
//这个属性在后面决定是否播放通知声音、震动等提醒的时候会用到
final boolean isPackageSuspended =
isPackagePausedOrSuspended(r.getSbn().getPackageName(), r.getUid());
r.setHidden(isPackageSuspended);
、、、
//从mNotificationList找到对应key的下标mNotificationList是存储 已排序 的通知,
//这里判断新来的通知是不是更新类型的,
//不是的话就直接add进mNotificationList,是的话则会将旧的通知替换掉,排序不变
int index = indexOfNotificationLocked(n.getKey());
if (index < 0) {
mNotificationList.add(r);
、、、
} else {
old = mNotificationList.get(index); // Potentially *changes* old
mNotificationList.set(index, r);
、、、
}
//将即将发送的通知存进集合mNotificationsByKey,
//这也是为什么前面我们可以通过mNotificationsByKey获取到某通知是否存在旧通知的原因
mNotificationsByKey.put(n.getKey(), r);
//前台服务通知是强制常驻通知面板的,不管你发送的时候是否设置了
//相关的常驻标志(FLAG_ONGOING_EVENT / FLAG_NO_CLEAR),系统都会帮你加上
if ((notification.flags & FLAG_FOREGROUND_SERVICE) != 0) {
notification.flags |= FLAG_ONGOING_EVENT
| FLAG_NO_CLEAR;
}
//注释1:
mRankingHelper.extractSignals(r);
mRankingHelper.sort(mNotificationList);
final int position = mRankingHelper.indexOf(mNotificationList, r);
int buzzBeepBlinkLoggingCode = 0;
if (!r.isHidden()) {
//震动、声音、LED提醒
buzzBeepBlinkLoggingCode = buzzBeepBlinkLocked(r);
}
//SmallIcon 不能为空,Android高版本规定必须要有小图标否则不展示
if (notification.getSmallIcon() != null) {
StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
mListeners.notifyPostedLocked(r, old);
、、、
} else {
if (old != null && !old.isCanceled) {
mListeners.notifyRemovedLocked(r,
NotificationListenerService.REASON_ERROR, r.getStats());
mHandler.post(new Runnable() {
@Override
public void run() {
mGroupHelper.onNotificationRemoved(n);
}
});
}
}
if (mShortcutHelper != null) {
mShortcutHelper.maybeListenForShortcutChangesForBubbles(r,
false /* isRemoved */,
mHandler);
}
maybeRecordInterruptionLocked(r);
maybeRegisterMessageSent(r);
maybeReportForegroundServiceUpdate(r, true);
} finally {
int N = mEnqueuedNotifications.size();
//处理完成了在这里将通知从mEnqueuedNotifications队列移除
for (int i = 0; i < N; i++) {
final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
if (Objects.equals(key, enqueued.getKey())) {
mEnqueuedNotifications.remove(i);
break;
}
}
}
}
}
}
这里主要是根据key找到要处理的通知,在2.3中PostNotificationRunnable的构造方法传入了key。找到要处理的通知之后进行一些状态判断处理。这里说明一下StatusBarNotification与NotificationRecord,两个都是对通知的封装,NotificationRecord是Service端使用的,另一个是给用户端使用的。
注释1:在这里将通知交给了多个NotificationSignalExtractor去处理,mRankingHelper是一个辅助类,内部维护了多个NotificationSignalExtractor的实现类,比如BubbleExtractor显示通知气泡,ImportanceExtractor确定通知重要性等,这些实现是在一个config.xml通过一个列表定义的。这里只是说明一下,后面会展开详细介绍。
高版本的Android系统规定通知必须有小图标,此时notification.getSmallIcon()不为空,调用mListeners.notifyPostedLocked。
2.5-notifyPostedLocked 通知监听者通知发生变更
[->NotificationManagerService.java]
java
public void notifyPostedLocked(NotificationRecord r, NotificationRecord old) {
notifyPostedLocked(r, old, true);
}
private void notifyPostedLocked(NotificationRecord r, NotificationRecord old,
boolean notifyAllListeners) {
try {
、、、
for (final ManagedServiceInfo info : getServices()) {
、、、
final StatusBarNotification sbnToPost = trimCache.ForListener(info);
mHandler.post(() -> notifyPosted(info, sbnToPost, update));
}
} catch (Exception e) {
Slog.e(TAG, "Could not notify listeners for " + r.getKey(), e);
}
}
private void notifyPosted(final ManagedServiceInfo info,
final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) {
final INotificationListener listener = (INotificationListener) info.service;
StatusBarNotificationHolder sbnHolder = new StatusBarNotificationHolder(sbn);
try {
listener.onNotificationPosted(sbnHolder, rankingUpdate);
} catch (RemoteException ex) {
Slog.e(TAG, "unable to notify listener (posted): " + info, ex);
}
}
通过调用notifyPostedLocked->notifyPostedLocked->notifyPosted,我们发现这里又有一个跨进程的接口调用INotificationListener。我们看到监听回调是在一个for循环中调用的,说明有多个模块注册了通知变化的监听,例如SystemUI中的通知栏,后面章节会讲到。到这里NMS已经处理完相关的工作,并且将事件通知了出去。
三、总结
前面NMS主干脉络绘制的流程调用结构图。
本文由mdnice多平台发布