本篇代码基于原生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的创建等等,偏视图类操作,本篇不作过多介绍了。
如有不对之处,欢迎大家指出,大家一起学习,一起进步!