Notification和NotificationManagerService原理解析

本篇代码基于原生Android P

一.通知的基本使用

通知栏消息是应用开发中十分常见和重要的一项功能。在Android O之后,增加了NotificationChannel的新特性,相同channel的通知拥有同样的特性,例如优先级,声音,振动,LED灯等等。这个特性使得相同属性的通知能够集中被管理,同时用户自己可以在应用的通知管理中手动修改channel的某项特性,这样给了用户一定的自主选择权。

发送通知的基本用法:

//定义一个channel
NotificationChannel channel1=new NotificationChannel("ch01","这是channel1",NotificationManager.IMPORTANCE_HIGH);
channel1.setDescription("这是channel1");
channel1.enableLights(true);
channel1.enableVibration(true);
channel1.setLightColor(Color.rgb(99,99,99));
//使用build模式构建一个Notification
Notification.Builder mBuilder=new Notification.Builder(MainActivity.this,"ch01")
                        .setSmallIcon(R.mipmap.ic_launcher_round)
                        .setWhen(System.currentTimeMillis())
                        .setContentTitle("通知标题")
                        .setContentText("通知内容")
                        .setAutoCancel(true);
//设置Intent跳转
Intent resultIntent=new Intent(MainActivity.this,ResultActivity.class);
PendingIntent contentIntent=PendingIntent.getActivity(MainActivity.this,0,resultIntent,PendingIntent.FLAG_UPDATE_CURRENT);
mBuilder.setContentIntent(contentIntent);
//使用NotificationManager创建channel和notification
NotificationManager notificationManager=(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel1);
notificationManager.notify(11223344,mBuilder.build());

demo的用法很简单,相信大家都会用了,不作过多的介绍。

二.通知发送源码解析

通知的显示主要参与角色有NotificationManager,NotificationManagerService,SystemUI。

1. NotificationManager调用(App进程):

    notificationManager.notify(11223344,mBuilder.build());最终会调用到notifyAsUser方法:

    public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
    {
         //tag默认为null,(tag,id)唯一确定了notification
        INotificationManager service = getService(); //获取Notification的代理对象
        String pkg = mContext.getPackageName();
        // Fix the notification as best we can.
        Notification.addFieldsFromContext(mContext, notification);

        if (notification.sound != null) {
            notification.sound = notification.sound.getCanonicalUri();
            if (StrictMode.vmFileUriExposureEnabled()) {
                notification.sound.checkFileUriExposed("Notification.sound");
            }

        }
        fixLegacySmallIcon(notification, pkg);
        if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) { //校验smallIcon,Android L之后不能为空
            if (notification.getSmallIcon() == null) {
                throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                        + notification);
            }
        }
        if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
        notification.reduceImageSizes(mContext);

        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
        boolean isLowRam = am.isLowRamDevice();
        final Notification copy = Builder.maybeCloneStrippedForDelivery(notification, isLowRam,
                mContext);
        try {
            //通过binder call调用NotificationManagerService的方法,进入system server进程
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    copy, user.getIdentifier());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

Notification实现了parcelable接口,便于跨进程通信。上述代码主要就是给Notification添加了一些应用的信息和user信息,还有一些常规校验等等。

2.NotificationManagerService(System Server进程,binder线程)

    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) {
        if (DBG) {
            Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
                    + " notification=" + notification);
        }
        checkCallerIsSystemOrSameApp(pkg);   //相关参数的检查

        final int userId = ActivityManager.handleIncomingUser(callingPid,
                callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
        final UserHandle user = new UserHandle(userId);

        if (pkg == null || notification == null) {  //相关校验
            throw new IllegalArgumentException("null not allowed: pkg=" + pkg
                    + " id=" + id + " notification=" + notification);
        }

        // The system can post notifications for any package, let us resolve that.
        final int notificationUid = resolveNotificationUid(opPkg, callingUid, userId);

        // Fix the notification as best we can.
        try {
            final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(
                    pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
                    (userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);
            Notification.addFieldsFromContext(ai, notification);

            int canColorize = mPackageManagerClient.checkPermission(
                    android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, pkg);
            if (canColorize == PERMISSION_GRANTED) {
                notification.flags |= Notification.FLAG_CAN_COLORIZE;
            } else {
                notification.flags &= ~Notification.FLAG_CAN_COLORIZE;
            }

        } catch (NameNotFoundException e) {
            Slog.e(TAG, "Cannot create a context for sending app", e);
            return;
        }

        mUsageStats.registerEnqueuedByApp(pkg);
        //获取channel信息,对channel进行规范性检查

        ......

        //Notification封装成StatusBarNotification,添加了一些应用信息,user信息等等,后续用于System UI交互
        final StatusBarNotification n = new StatusBarNotification(
                pkg, opPkg, id, tag, notificationUid, callingPid, notification,
                user, null, System.currentTimeMillis());
        //再次封装成NotificationRecord,带上了channel信息,后续会交给mHandler处理
        final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
        r.setIsAppImportanceLocked(mRankingHelper.getIsAppImportanceLocked(pkg, callingUid));

        //......

        //检查应用发送通知的速率,个数等等,决定是否允许继续发送通知
        if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r,
                r.sbn.getOverrideGroupKey() != null)) {
            return;
        }

        // Whitelist pending intents.
        if (notification.allPendingIntents != null) { //将通知的pendingIntent加入到白名单
            final int intentCount = notification.allPendingIntents.size();
            if (intentCount > 0) {
                final ActivityManagerInternal am = LocalServices
                        .getService(ActivityManagerInternal.class);
                final long duration = LocalServices.getService(
                        DeviceIdleController.LocalService.class).getNotificationWhitelistDuration();
                for (int i = 0; i < intentCount; i++) {
                    PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
                    if (pendingIntent != null) {
                        am.setPendingIntentWhitelistDuration(pendingIntent.getTarget(),
                                WHITELIST_TOKEN, duration);
                    }
                }
            }
        }
        //发送通知到主线程
        mHandler.post(new EnqueueNotificationRunnable(userId, r));
    }

上述代码主要是检查一些合法性,例如caller,参数等等,然后封装StatusBarNotification以及NotificationRecord,检查通知的速率,个数限制,最后发送通知。StatusBarNotification以及NotificationRecord作为核心数据结构,特别重要,大家可以这样理解,StatusBarNotification以StatusBar开头,顾名思义,后面肯定会用于通知栏的交互,NotificationRecord看名字就知道后面肯定用于framework层的使用。有了这样的印象后,后面的理解会更加容易一点。

其中,StatusBarNotification以key作为其唯一标识,定义如下:

例如:0|com.example.mi.xiaomiapp|11223344|null|10159,源码定义如下所示:

    private String key() {
        String sbnKey = user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid;
        if (overrideGroupKey != null && getNotification().isGroupSummary()) {
            sbnKey = sbnKey + "|" + overrideGroupKey;
        }
        return sbnKey;
    }

3. EnqueueNotificationRunnable(System Server进程,主线程)

    protected class EnqueueNotificationRunnable implements Runnable {
        private final NotificationRecord r;
        private final int userId;

        EnqueueNotificationRunnable(int userId, NotificationRecord r) {
            this.userId = userId;
            this.r = r;
        };

        @Override
        public void run() {
            synchronized (mNotificationLock) {
                //将通知加入队列,内部调用-> final ArrayList<NotificationRecord> mEnqueuedNotifications = new ArrayList<>();

                mEnqueuedNotifications.add(r);
                scheduleTimeoutLocked(r);

                final StatusBarNotification n = r.sbn;
                if (DBG) Slog.d(TAG, "EnqueueNotificationRunnable.run for: " + n.getKey());
                //根据key来查找是否已有相同的Record,有的话保持相同的排序信息
                //内部结构:final ArrayMap<String, NotificationRecord> mNotificationsByKey = new ArrayMap<>();

                NotificationRecord old = mNotificationsByKey.get(n.getKey());
                if (old != null) {
                    // Retain ranking information from previous record
                    r.copyRankingInformation(old);
                }

                final int callingUid = n.getUid();
                final int callingPid = n.getInitialPid();
                final Notification notification = n.getNotification();
                final String pkg = n.getPackageName();
                final int id = n.getId();
                final String tag = n.getTag();

                // Handle grouped notifications and bail out early if we
                // can to avoid extracting signals.
                //处理NotificationGroup的信息
                handleGroupedNotificationLocked(r, old, callingUid, callingPid);

                // if this is a group child, unsnooze parent summary
                if (n.isGroup() && notification.isGroupChild()) {
                    mSnoozeHelper.repostGroupSummary(pkg, r.getUserId(), n.getGroupKey());
                }

                // This conditional is a dirty hack to limit the logging done on
                //     behalf of the download manager without affecting other apps.
                if (!pkg.equals("com.android.providers.downloads")
                        || Log.isLoggable("DownloadManager", Log.VERBOSE)) {
                    int enqueueStatus = EVENTLOG_ENQUEUE_STATUS_NEW;
                    if (old != null) {
                        enqueueStatus = EVENTLOG_ENQUEUE_STATUS_UPDATE;
                    }
                    //event log写入notification_enqueue信息
                    EventLogTags.writeNotificationEnqueue(callingUid, callingPid,
                            pkg, id, tag, userId, notification.toString(),
                            enqueueStatus);
                }

                mRankingHelper.extractSignals(r);

                // tell the assistant service about the notification
                if (mAssistants.isEnabled()) {
                    mAssistants.onNotificationEnqueued(r);
                    //post到PostNotificationRunnable
                    mHandler.postDelayed(new PostNotificationRunnable(r.getKey()),
                            DELAY_FOR_ASSISTANT_TIME);
                } else {
                    mHandler.post(new PostNotificationRunnable(r.getKey()));
                }
            }
        }
    }

4.继续看:PostNotificationRunnable

    protected class PostNotificationRunnable implements Runnable {
        private final String key;

        PostNotificationRunnable(String key) {
            this.key = key;
        }

        @Override
        public void run() {
            synchronized (mNotificationLock) {
                try {
                    NotificationRecord r = null;
                    int N = mEnqueuedNotifications.size();
                    for (int i = 0; i < N; i++) {
                      //从队列中取出NotificationRecord
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            r = enqueued;
                            break;
                        }
                    }
                    if (r == null) {
                        Slog.i(TAG, "Cannot find enqueued record for key: " + key);
                        return;
                    }

                    r.setHidden(isPackageSuspendedLocked(r));
                    NotificationRecord old = mNotificationsByKey.get(key);
                    final StatusBarNotification n = r.sbn;
                    //根据key判断mNotificationList里面有没有此Record,有就更新,没有就新增
                    final Notification notification = n.getNotification();
                    //内部结构:final ArrayList<NotificationRecord> mNotificationList = new ArrayList<>();

                    int index = indexOfNotificationLocked(n.getKey());
                    if (index < 0) {
                        mNotificationList.add(r);
                        mUsageStats.registerPostedByApp(r);
                        r.setInterruptive(isVisuallyInterruptive(null, r));
                    } else {
                        old = mNotificationList.get(index);
                        mNotificationList.set(index, r);
                        mUsageStats.registerUpdatedByApp(r, old);
                        // Make sure we don't lose the foreground service state.
                        notification.flags |=
                                old.getNotification().flags & FLAG_FOREGROUND_SERVICE;
                        r.isUpdate = true;
                        r.setTextChanged(isVisuallyInterruptive(old, r));
                    }

                    mNotificationsByKey.put(n.getKey(), r);

                    // Ensure if this is a foreground service that the proper additional
                    // flags are set.
                    //如果是前台service,则继续添加FLAG_ONGOING_EVENT和FLAG_NO_CLEAR
                    if ((notification.flags & FLAG_FOREGROUND_SERVICE) != 0) {
                        notification.flags |= Notification.FLAG_ONGOING_EVENT
                                | Notification.FLAG_NO_CLEAR;
                    }

                    applyZenModeLocked(r); //评估静音和勿扰模式
                    mRankingHelper.sort(mNotificationList); //对notification进行排序

                    if (notification.getSmallIcon() != null) {
                        StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
                        //调用NotificationListeners的notifyPostedLocked方法
                        mListeners.notifyPostedLocked(r, old);
                        if (oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())) {
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    mGroupHelper.onNotificationPosted(
                                            n, hasAutoGroupSummaryLocked(n));
                                }
                            });
                        }
                    } else {
                        Slog.e(TAG, "Not posting notification without small icon: " + notification);
                        if (old != null && !old.isCanceled) {
                            mListeners.notifyRemovedLocked(r,
                                    NotificationListenerService.REASON_ERROR, null);
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    mGroupHelper.onNotificationRemoved(n);
                                }
                            });
                        }
                        // ATTENTION: in a future release we will bail out here
                        // so that we do not play sounds, show lights, etc. for invalid
                        // notifications
                        Slog.e(TAG, "WARNING: In a future release this will crash the app: "
                                + n.getPackageName());
                    }

                    if (!r.isHidden()) {
                        buzzBeepBlinkLocked(r);
                    }
                    maybeRecordInterruptionLocked(r);
                } finally {
                    int N = mEnqueuedNotifications.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            mEnqueuedNotifications.remove(i);
                            break;
                        }
                    }
                }
            }
        }
    }

上述过程主要是将notificationRecord插入或者更新到mNotificationList,静音勿扰模式评估,进行排序等等

5.继续查看NotificationListeners. notifyPostedLocked

        private void notifyPostedLocked(NotificationRecord r, NotificationRecord old,
                boolean notifyAllListeners) {
            // Lazily initialized snapshots of the notification.
            StatusBarNotification sbn = r.sbn;
            StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
            TrimCache trimCache = new TrimCache(sbn);

            for (final ManagedServiceInfo info : getServices()) {
                boolean sbnVisible = isVisibleToListener(sbn, info);
                boolean oldSbnVisible = oldSbn != null ? isVisibleToListener(oldSbn, info) : false;
                // This notification hasn't been and still isn't visible -> ignore.
                if (!oldSbnVisible && !sbnVisible) {
                    continue;
                }

                //...... //主要对oldSbn和sbn的可见性进行判断

                
                //提取之前更新好的排序信息,以便发送给System UI
                final NotificationRankingUpdate update = makeRankingUpdateLocked(info);

                // This notification became invisible -> remove the old one.
                if (oldSbnVisible && !sbnVisible) {
                    final StatusBarNotification oldSbnLightClone = oldSbn.cloneLight();
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            notifyRemoved(
                                    info, oldSbnLightClone, update, null, REASON_USER_STOPPED);
                        }
                    });
                    continue;
                }

                // Grant access before listener is notified
                final int targetUserId = (info.userid == UserHandle.USER_ALL)
                        ? UserHandle.USER_SYSTEM : info.userid;
                updateUriPermissions(r, old, info.component.getPackageName(), targetUserId);

                final StatusBarNotification sbnToPost = trimCache.ForListener(info);
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        notifyPosted(info, sbnToPost, update);
                    }
                });
            }
        }

StatusBarNotification会发送给上述中的所有注册了NotificationListenerService的client,包括system UI等。对于小米手机而言,我们可以在设置里面的“通知使用权”里面去显示设置,里面能够看到注册了NLS的应用。例如,上述的ManagedServiceInfo可以有system UI,手机管家以及用户自己实现NLS的任意APP。

6.继续查看notifyPosted

        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) {
                Log.e(TAG, "unable to notify listener (posted): " + listener, ex);
            }
        }


        /**
     * Wrapper for a StatusBarNotification object that allows transfer across a oneway
     * binder without sending large amounts of data over a oneway transaction.
     */
    private static final class StatusBarNotificationHolder
            extends IStatusBarNotificationHolder.Stub {
        private StatusBarNotification mValue;

        public StatusBarNotificationHolder(StatusBarNotification value) {
            mValue = value;
        }

        /** Get the held value and clear it. This function should only be called once per holder */
        @Override
        public StatusBarNotification get() {
            StatusBarNotification value = mValue;
            mValue = null;
            return value;
        }
    }

重点来了,上面两段代码大家仔细看,onNotificationPosted传入的参数是sbnHolder而不是sbn对象。尼玛,逗我呢?sbnHolder类我也贴出来了,大家可以看到有个get()方法,返回的是真正的sbn对象。聪明的你肯定明白了,APP收到sbnHolder后需要再次调用binder call才能获得sbn对象。为啥呢?看Google大大的注释啊,sbnHolder的注释写道:Wrapper for a StatusBarNotification object that allows transfer across a oneway binder without sending large amounts of data over a oneway transaction.

大体意思清楚了,就是说对sbn对象进行封装,通过一次one way的binder call,避免传输大量数据。哦,我明白了,原来是为了避免高强度劳动啊,StatusBarNotification对象确实过于庞大,人家这样设计也是有道理的啊。来来来,比喻小达人我又来了,NotificationManagerService好比快递小哥(尼玛又是快递的比喻,我的BroadcastReceiver那篇博客也用的快递作比喻,哈哈),原先人家送货上门,现在人家发短信告诉你地址了(sbnHolder),你自己来取快递(sbnHolder.get()),这样确实避免了高强度劳动,快递小哥也不容易啊,致敬!

三.通知的接收

1.接着上节内容,我们首先就来看看APP收到sbnHolder的地方:

        public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
                NotificationRankingUpdate update) {
            StatusBarNotification sbn;
            try {
                sbn = sbnHolder.get(); //取出真正的sbn
            } catch (RemoteException e) {
                Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e);
                return;
            }

            try {
                // convert icon metadata to legacy format for older clients
                createLegacyIconExtras(sbn.getNotification());
                maybePopulateRemoteViews(sbn.getNotification());
                maybePopulatePeople(sbn.getNotification());
            } catch (IllegalArgumentException e) {
                // warn and drop corrupt notification
                Log.w(TAG, "onNotificationPosted: can't rebuild notification from " +
                        sbn.getPackageName());
                sbn = null;
            }

            // protect subclass from concurrent modifications of (@link mNotificationKeys}.
            synchronized (mLock) {
                applyUpdateLocked(update);
                if (sbn != null) {
                    SomeArgs args = SomeArgs.obtain();
                    args.arg1 = sbn;
                    args.arg2 = mRankingMap;
                    //交由NLS的handler处理
                    mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_POSTED,
                            args).sendToTarget();
                } else {
                    // still pass along the ranking map, it may contain other information
                    mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_RANKING_UPDATE,
                            mRankingMap).sendToTarget();
                }
            }

        }


//......

        public void handleMessage(Message msg) {
            if (!isConnected) {
                return;
            }
            switch (msg.what) {
                case MSG_ON_NOTIFICATION_POSTED: {
                    SomeArgs args = (SomeArgs) msg.obj;
                    StatusBarNotification sbn = (StatusBarNotification) args.arg1;
                    RankingMap rankingMap = (RankingMap) args.arg2;
                    args.recycle();
                    onNotificationPosted(sbn, rankingMap); //注册NLS的app收到sbn
                } break;

后面可以对sbn对象进行处理啦。

四.总结

1.Notification的整个过程其实宏观上来说也不复杂,重点还是对于StatusBarNotification和NotificationRecord的理解。

2.关于NotificationListenerService的理解,大家需要强化,虽然本篇并未展开讲,但是大家可以随着源码自己去跟踪。关于NLS的用途,大家肯定也很清楚,最主要的就是抢红包软件了,通过监听系统通知,获取sbn对象,判断是不是红包,然后弹出抢红包提示等等。

3.System UI的展示后面更多的是SystemUI对于notification的存储,以及view的创建等等,偏视图类操作,本篇不作过多介绍了。

如有不对之处,欢迎大家指出,大家一起学习,一起进步!

发布了10 篇原创文章 · 获赞 89 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/cbzcbzcbzcbz/article/details/104068389