Android通知之NotificationManagerService 源码分析

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多平台发布

相关推荐
zhangphil8 分钟前
Android简洁缩放Matrix实现图像马赛克,Kotlin
android·kotlin
m0_512744648 分钟前
极客大挑战2024-web-wp(详细)
android·前端
lw向北.25 分钟前
Qt For Android之环境搭建(Qt 5.12.11 Qt下载SDK的处理方案)
android·开发语言·qt
不爱学习的啊Biao33 分钟前
【13】MySQL如何选择合适的索引?
android·数据库·mysql
Clockwiseee1 小时前
PHP伪协议总结
android·开发语言·php
mmsx8 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
众拾达人10 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌11 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley13 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei14 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576