Android 深入理解 Notification 機制

Android 深入理解 Notification 机制

博客:https://juejin.im/post/5c287e1ae51d453634703b15

筆者最近正在做一個項目,裡面需要用到 Android Notification 機制來實現某些特定需求。我正好通過這個機會研究一下 Android Notification 相關的發送邏輯和接收邏輯,以及整理相關的筆記。我研究 Notification 機制的目的是解決以下我在使用過程中所思考的問題:

  1. 我們創建的 Notification 實例最終以什麼樣的方式發送給系統?

  2. 系統是如何接收到 Notification 實例並顯示的?

  3. 我們是否能攔截其他 app 的 Notification 並獲取其中的信息?

什麼是 Android Notification 機制?

Notification,中文名翻譯為通知,每個 app 可以自定義通知的樣式和內容等,它會顯示在系統的通知欄等區域。用戶可以打開抽屜式通知欄查看通知的詳細信息。在實際生活中,Android Notification 機制有很廣泛的應用,例如 IM app 的新消息通知,資訊 app 的新聞推送等等。

源碼分析

本文的源碼基於 Android 7.0。

Notification 的發送邏輯

一般來說,如果我們自己的 app 想發送一條新的 Notification,可以參照以下代碼:

<code>NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.notification_icon)
.setWhen(System.currentTimeMillis)
.setContentTitle("Test Notification Title")
.setContentText("Test Notification Content!");
Intent resultIntent = new Intent(this, ResultActivity.class);

PendingIntent contentIntent =
PendingIntent.getActivity(
this,
0,
resultIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
mBuilder.setContentIntent(resultPendingIntent);
NotificationManager mNotificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// mId allows you to update the notification later on.
mNotificationManager.notify(mId, mBuilder.build);/<code>

可以看到,我們通過 NotificationCompat.Builder 新建了一個 Notification 對象,最後通過 NotificationManager#notify 方法將 Notification 發送出去。

NotificationManager#notify

<code>public void notify(int id, Notification notification)
{
notify(, id, notification);
}

// 省略部分註釋
public void notify(String tag, int id, Notification notification)
{
notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId));
}

/**
* @hide
*/
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
int idOut = new int[1];
INotificationManager service = getService;
String pkg = mContext.getPackageName;
// Fix the notification as best we can.
Notification.addFieldsFromContext(mContext, notification);
if (notification.sound != ) {
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) {
if (notification.getSmallIcon == ) {
throw new IllegalArgumentException("Invalid notification (no valid small icon): "
+ notification);
}
}
if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
try {
// !!!
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName, tag, id,
copy, idOut, user.getIdentifier);
if (localLOGV && id != idOut[0]) {
Log.v(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]);
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer;
}
}/<code>

我們可以看到,到最後會調用 service.enqueueNotificationWithTag 方法,這裡的是 service 是 INotificationManager 接口。如果熟悉 AIDL 等系統相關運行機制的話,就可以看出這裡是代理類調用了代理接口的方法,實際方法實現是在 NotificationManagerService 當中。

NotificationManagerService#enqueueNotificationWithTag

<code>@Override
public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id,
Notification notification, int[] idOut, int userId) throws RemoteException {
enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid,
Binder.getCallingPid, tag, id, notification, idOut, userId);
}

void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
final int callingPid, final String tag, final int id, final Notification notification,
int[] idOut, int incomingUserId) {
if (DBG) {
Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
+ " notification=" + notification);
}
checkCallerIsSystemOrSameApp(pkg);
final boolean isSystemNotification = isUidSystem(callingUid) || ("android".equals(pkg));
final boolean isNotificationFromListener = mListeners.isListenerPackage(pkg);

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

// Fix the notification as best we can.
try {
final ApplicationInfo ai = getContext.getPackageManager.getApplicationInfoAsUser(
pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
(userId == UserHandle.USER_ALL) ? UserHandle.USER_SYSTEM : userId);
Notification.addFieldsFromContext(ai, userId, notification);
} catch (NameNotFoundException e) {
Slog.e(TAG, "Cannot create a context for sending app", e);
return;
}

mUsageStats.registerEnqueuedByApp(pkg);

if (pkg == || notification == ) {
throw new IllegalArgumentException(" not allowed: pkg=" + pkg
+ " id=" + id + " notification=" + notification);
}
final StatusBarNotification n = new StatusBarNotification(
pkg, opPkg, id, tag, callingUid, callingPid, 0, notification,
user);

// Limit the number of notifications that any given package except the android
// package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks.

if (!isSystemNotification && !isNotificationFromListener) {
synchronized (mNotificationList) {
if(mNotificationsByKey.get(n.getKey) != ) {
// this is an update, rate limit updates only
final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
if (appEnqueueRate > mMaxPackageEnqueueRate) {
mUsageStats.registerOverRateQuota(pkg);
final long now = SystemClock.elapsedRealtime;
if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
+ ". Shedding events. package=" + pkg);
mLastOverRateLogTime = now;
}
return;
}
}

int count = 0;
final int N = mNotificationList.size;
for (int i=0; ifinal NotificationRecord r = mNotificationList.get(i);
if (r.sbn.getPackageName.equals(pkg) && r.sbn.getUserId == userId) {
if (r.sbn.getId == id && TextUtils.equals(r.sbn.getTag, tag)) {
break; // Allow updating existing notification
}
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
mUsageStats.registerOverCountQuota(pkg);
Slog.e(TAG, "Package has already posted " + count
+ " notifications. Not showing more. package=" + pkg);
return;
}
}
}
}
}

// Whitelist pending intents.
if (notification.allPendingIntents != ) {
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 != ) {
am.setPendingIntentWhitelistDuration(pendingIntent.getTarget, duration);
}

}
}
}

// Sanitize inputs
notification.priority = clamp(notification.priority, Notification.PRIORITY_MIN,
Notification.PRIORITY_MAX);

// setup local book-keeping
final NotificationRecord r = new NotificationRecord(getContext, n);
mHandler.post(new EnqueueNotificationRunnable(userId, r));

idOut[0] = id;
}
/<code>

這裡代碼比較多,但通過註釋可以清晰地理清整個邏輯:

  1. 首先檢查通知發起者是系統進程或者是查看發起者發送的是否是同個 app 的通知信息,否則拋出異常;

  2. 除了系統的通知和已註冊的監聽器允許入隊列外,其他 app 的通知都會限制通知數上限和通知頻率上限;

  3. 將 notification 的 PendingIntent 加入到白名單;

  4. 將之前的 notification 進一步封裝為 StatusBarNotification 和 NotificationRecord,最後封裝到一個異步線程 EnqueueNotificationRunnable 中

這裡有一個點,就是 mHandler,涉及到切換線程,我們先跟蹤一下 mHandler 是在哪個線程被創建。

mHandler 是 WorkerHandler 類的一個實例,在 NotificationManagerService#onStart 方法中被創建,而 NotificationManagerService 是系統 Service,所以 EnqueueNotificationRunnable 的 run 方法會運行在 system_server 的主線程。

NotificationManagerService.EnqueueNotificationRunnable#run

<code>@Override
public void run {
synchronized(mNotificationList) {
// 省略代碼
if (notification.getSmallIcon != ) {
StatusBarNotification oldSbn = (old != ) ? old.sbn : ;
mListeners.notifyPostedLocked(n, oldSbn);
} else {
Slog.e(TAG, "Not posting notification without small icon: " + notification);
if (old != && !old.isCanceled) {
mListeners.notifyRemovedLocked(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);
}
buzzBeepBlinkLocked(r);
}
}/<code>
  1. 省略的代碼主要的工作是提取 notification 相關的屬性,同時通知 notification ranking service,有新的 notification 進來,然後對所有 notification 進行重新排序;

  2. 然後到最後會調用 mListeners.notifyPostedLocked 方法。這裡 mListeners 是 NotificationListeners 類的一個實例。

NotificationManagerService.NotificationListeners#notifyPostedLocked

-> NotificationManagerService.NotificationListeners#notifyPosted

<code>public void notifyPostedLocked(StatusBarNotification sbn, StatusBarNotification oldSbn) {
// Lazily initialized snapshots of the notification.
TrimCache trimCache = new TrimCache(sbn);
for (final ManagedServiceInfo info: mServices) {
boolean sbnVisible = isVisibleToListener(sbn, info);
boolean oldSbnVisible = oldSbn != ? isVisibleToListener(oldSbn, info) : false;
// This notification hasn't been and still isn't visible -> ignore.
if (!oldSbnVisible && !sbnVisible) {
continue;
}
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);
}
});
continue;
}
final StatusBarNotification sbnToPost = trimCache.ForListener(info);
mHandler.post(new Runnable {
@Override
public void run {
notifyPosted(info, sbnToPost, update);
}
});
}
}

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);
}

}/<code>

調用到最後會執行 listener.onNotificationPosted 方法。通過全局搜索得知,listener 類型是 NotificationListenerService.NotificationListenerWrapper 的代理對象。

NotificationListenerService.NotificationListenerWrapper#onNotificationPosted

<code>public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update) {
StatusBarNotification sbn;
try {
sbn = sbnHolder.get;
} 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);
} catch (IllegalArgumentException e) {
// warn and drop corrupt notification
Log.w(TAG, "onNotificationPosted: can't rebuild notification from " + sbn.getPackageName);
sbn = ;
}
// protect subclass from concurrent modifications of (@link mNotificationKeys}.
synchronized(mLock) {
applyUpdateLocked(update);
if (sbn != ) {
SomeArgs args = SomeArgs.obtain;
args.arg1 = sbn;
args.arg2 = mRankingMap;
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;
}
}
}/<code>

這裡在一開始會從 sbnHolder 中獲取到 sbn 對象,sbn 隸屬於 StatusBarNotificationHolder 類,繼承於 IStatusBarNotificationHolder.Stub 對象。注意到這裡捕獲了一個 RemoteException,猜測涉及到跨進程調用,但我們不知道這段代碼是在哪個進程中執行的,所以在這裡暫停跟蹤代碼。

筆者之前是通過向系統發送通知的方式跟蹤源碼,發現走不通。故個人嘗試從另一個角度入手,即系統接收我們發過來的通知並顯示到通知欄這個方式入手跟蹤代碼。

系統如何顯示 Notification,即對於系統端來說,Notification 的接收邏輯

系統顯示 Notification 的過程,猜測是在 PhoneStatusBar.java 中,因為系統啟動的過程中,會啟動 SystemUI 進程,初始化整個 Android 顯示的界面,包括系統通知欄。

PhoneStatusBar#start -> BaseStatusBar#start

<code>public void start {
// 省略代碼
// Set up the initial notification state.
try {
mNotificationListener.registerAsSystemService(mContext,
new ComponentName(mContext.getPackageName, getClass.getCanonicalName),
UserHandle.USER_ALL);
} catch (RemoteException e) {
Log.e(TAG, "Unable to register notification listener", e);
}
// 省略代碼
}/<code>

這段代碼中,會調用 NotificationListenerService#registerAsSystemService 方法,涉及到我們之前跟蹤代碼的類。我們繼續跟進去看一下。

NotificationListenerService#registerAsSystemService

<code>public void registerAsSystemService(Context context, ComponentName componentName,
int currentUser) throws RemoteException {

if (mWrapper == ) {
mWrapper = new NotificationListenerWrapper;
}
mSystemContext = context;
INotificationManager noMan = getNotificationInterface;
mHandler = new MyHandler(context.getMainLooper);
mCurrentUser = currentUser;
noMan.registerListener(mWrapper, componentName, currentUser);
}/<code>

這裡會初始化一個 NotificationListenerWrapper 和 mHandler。由於這是在 SystemUI 進程中去調用此方法將 NotificationListenerService 註冊為系統服務,所以在前面分析的那裡:

NotificationListenerService.NotificationListenerWrapper#onNotificationPosted,這段代碼是運行在 SystemUI 進程,而 mHandler 則是運行在 SystemUI 主線程上的 Handler。所以,onNotificationPosted 是運行在 SystemUI 進程中,它通過 sbn 從 system_server 進程中獲取到 sbn 對象。下一步是通過 mHandler 處理消息,查看 NotificationListenerService.MyHandler#handleMessage 方法,得知當 message.what 為 MSG_ON_NOTIFICATION_POSTED 時,調用的是 onNotificationPosted 方法。

但是,NotificationListenerService 是一個抽象類,onNotificationPosted 為空方法,真正的實現是它的實例類。

觀察到之前 BaseStatusBar#start 中,是調用了 mNotificationListener.registerAsSystemService 方法。那麼,mNotificationListener 是在哪裡進行初始化呢?

BaseStatusBar.mNotificationListener#onNotificationPosted

<code>private final NotificationListenerService mNotificationListener = new NotificationListenerService {
// 省略代碼

@Override
public void onNotificationPosted(final StatusBarNotification sbn, final RankingMap rankingMap) {
if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn);
if (sbn != ) {
mHandler.post(new Runnable {
@Override
public void run {

processForRemoteInput(sbn.getNotification);
String key = sbn.getKey;
mKeysKeptForRemoteInput.remove(key);
boolean isUpdate = mNotificationData.get(key) != ;
// In case we don't allow child notifications, we ignore children of
// notifications that have a summary, since we're not going to show them
// anyway. This is true also when the summary is canceled,
// because children are automatically canceled by NoMan in that case.
if (!ENABLE_CHILD_NOTIFICATIONS && mGroupManager.isChildInGroupWithSummary(sbn)) {
if (DEBUG) {
Log.d(TAG, "Ignoring group child due to existing summary: " + sbn);
}
// Remove existing notification to avoid stale data.
if (isUpdate) {
removeNotification(key, rankingMap);
} else {
mNotificationData.updateRanking(rankingMap);
}
return;
}
if (isUpdate) {
updateNotification(sbn, rankingMap);
} else {
addNotification(sbn, rankingMap, /* oldEntry */ );
}
}
});
}
}
// 省略代碼
}/<code>
  1. 通過上述代碼,我們知道了在 BaseStatusBar.java 中,創建了 NotificationListenerService 的實例對象,實現了 onNotificationPost 這個抽象方法;

  2. 在 onNotificationPost 中,通過 handler 進行消息處理,最終調用 addNotification 方法

PhoneStatusBar#addNotification

<code>@Override
public void addNotification(StatusBarNotification notification, RankingMap ranking, Entry oldEntry) {
if (DEBUG) Log.d(TAG, "addNotification key=" + notification.getKey);
mNotificationData.updateRanking(ranking);
Entry shadeEntry = createNotificationViews(notification);
if (shadeEntry == ) {
return;
}
boolean isHeadsUped = shouldPeek(shadeEntry);
if (isHeadsUped) {
mHeadsUpManager.showNotification(shadeEntry);
// Mark as seen immediately
setNotificationShown(notification);
}
if (!isHeadsUped && notification.getNotification.fullScreenIntent != ) {
if (shouldSuppressFullScreenIntent(notification.getKey)) {
if (DEBUG) {
Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + notification.getKey);
}
} else if (mNotificationData.getImportance(notification.getKey) < NotificationListenerService.Ranking.IMPORTANCE_MAX) {
if (DEBUG) {
Log.d(TAG, "No Fullscreen intent: not important enough: " + notification.getKey);
}
} else {
// Stop screensaver if the notification has a full-screen intent.
// (like an incoming phone call)
awakenDreams;
// not immersive & a full-screen alert should be shown
if (DEBUG) Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent");
try {
EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, notification.getKey);
notification.getNotification.fullScreenIntent.send;
shadeEntry.notifyFullScreenIntentLaunched;
MetricsLogger.count(mContext, "note_fullscreen", 1);
} catch (PendingIntent.CanceledException e) {}
}
}
// !!!
addNotificationViews(shadeEntry, ranking);
// Recalculate the position of the sliding windows and the titles.
setAreThereNotifications;
}/<code>

在這個方法中,最關鍵的方法是最後的 addNotificationViews 方法。調用這個方法之後,你創建的 Notification 才會被添加到系統通知欄上。

總結

跟蹤完整個過程中,之前提到的問題也可以一一解決了:

  • Q:我們創建的 Notification 實例最終以什麼樣的方式發送給系統?

A:首先,我們在 app 進程創建 Notification 實例,通過跨進程調用,傳遞到 system_server 進程的 NotificationManagerService 中進行處理,經過兩次異步調用,最後傳遞給在 NotificationManagerService 中已經註冊的 NotificationListenerWrapper。而 android 系統在初始化 systemui 進程的時候,會往 NotificationManagerService 中註冊監聽器(這裡指的就是 NotificationListenerWrapper)。這種實現方法就是基於我們熟悉的一種設計模式:監聽者模式。

  • Q:系統是如何獲取到 Notification 實例並顯示的?

A:上面提到,由於初始化的時候已經往 NotificationManagerService 註冊監聽器,所以系統 SystemUI 進程會接收到 Notification 實例之後經過進一步解析,然後構造出 Notification Views 並最終顯示在系統通知欄上。

  • Q:我們是否能攔截 Notification 並獲取其中的信息?

A:通過上面的流程,我個人認為可以通過 Xposed 等框架去 hook 其中幾個重要的方法去捕獲 Notification 實例,例如 hook NotificationManager#notify 方法去獲取 Notification 實例。

近期文章:

  • 只有程序員才懂的16張趣圖

  • 談談面試完BAT後的感想

  • 聽說你Binder機制學的不錯,來面試下這幾個問題

今日問題:

使用Notification時,你遇到過什麼怪問題?

打卡格式:

打卡 X 天,答:xxx 。

Android 深入理解 Notification 机制


分享到:


相關文章: