Android 12.0 通知发送过程源码分析-Framework
以下NotificationManagerService简称 NMS
1. 通知的发送: NotificationManager.notify(int id, Notification notification) 开始.
源码路径: /frameworks/base/core/java/android/app/NotificationManager.java/***发布通知以显示在状态栏中。 如果通知带有* 相同的 ID 已被您的应用程序发布且尚未被取消,它将被更新信息取代。** @param id 此通知的标识符在您的系统中是唯一的应用。* @param notification 描述向用户显示的内容。 一定不为空。* */public void notify(int id, Notification notification){notify(null, id, notification);}
这里继续调用 notify(), 其中 tag = null;
源码路径: /frameworks/base/core/java/android/app/NotificationManager.java
public void notify(String tag, int id, Notification notification){notifyAsUser(tag, id, notification, mContext.getUser());}/*** @hide*/@UnsupportedAppUsagepublic void notifyAsUser(String tag, int id, Notification notification, UserHandle user){INotificationManager service = getService();//获取binder对象,实现跨进程通信String pkg = mContext.getPackageName(); //获取发送通知应用的包名try {//跨进程调用,即调用NMS中的enqueueNotificationWithTag(),请看分析4//在这之前会先调用fixNotification()方法,提前做一些优化,请看分析2service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(),tag, id,fixNotification(notification), user.getIdentifier())} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}
2. 优化通知 , fixNotification(notification) 代码如下:
源码路径: /frameworks/base/core/java/android/app/NotificationManager.javaprivate Notification fixNotification(Notification notification) {String pkg = mContext.getPackageName();//这里把ApplicationInfo保存到Notificaiton.extras参数中, 请看2.(1)Notification.addFieldsFromContext(mContext, notification);//如果设置了通知铃声,这里获取铃声的uriif (notification.sound != null) {notification.sound = notification.sound.getCanonicalUri();if (StrictMode.vmFileUriExposureEnabled()) {notification.sound.checkFileUriExposed("Notification.sound");}}fixLegacySmallIcon(notification, pkg);//smallIcon版本兼容处理,请看2.(2)//Android 5.1 后要求必须设置setSmallIcon(),否则抛出异常if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {if (notification.getSmallIcon() == null) {throw new IllegalArgumentException("Invalid notification (no valid small icon): "+ notification);}}notification.reduceImageSizes(mContext);//按比例压缩图片,请看2.(3)return Builder.maybeCloneStrippedForDelivery(notification);}
(1) 保存ApplicationInfo 对象到通知中,addFieldsFromContext() ,源码如下:
源码路径: /frameworks/base/core/java/android/app/Notification.java
/*** @hide*/public static void addFieldsFromContext(Context context, Notification notification) {addFieldsFromContext(context.getApplicationInfo(), notification);}/*** @hide*/public static void addFieldsFromContext(ApplicationInfo ai, Notification notification) {//保存ApplicationInfo对象到通知中,属性名为EXTRA_BUILDER_APPLICATION_INFOnotification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, ai);}
(2) smallIcon版本兼容处理
老版本中定义的通知smallIcon为资源int型,新版本中换成Icon 类型,为了兼容旧版本,这里做了转换,即把 int 型转化成Icon型,并设置到通知中,fixLegacySmallIcon(notification, pkg)源码如下:
源码路径: /frameworks/base/core/java/android/app/NotificationManager.java
private void fixLegacySmallIcon(Notification n, String pkg) {if (n.getSmallIcon() == null && n.icon != 0) {//n.setSmallIcon(Icon icon), 而 n.icon 为 int 型,这里调用了createWithResource()转换n.setSmallIcon(Icon.createWithResource(pkg, n.icon));}}源码路径: frameworks/base/graphics/java/android/graphics/drawable/Icon.java/*** 创建Icon对象* @param resPackage 包名* @param resId 资源ID*/public static Icon createWithResource(String resPackage, @DrawableRes int resId) {if (resPackage == null) {throw new IllegalArgumentException("Resource package name must not be null.");}final Icon rep = new Icon(TYPE_RESOURCE);rep.mInt1 = resId;rep.mString1 = resPackage;return rep;}
(3) 压缩图片 reduceImageSizes(mContext)
源码如下:
源码路径: /frameworks/base/core/java/android/app/Notification.java
/*** 把图片缩小成给定的尺寸* @hide*/void reduceImageSizes(Context context) {if (extras.getBoolean(EXTRA_REDUCED_IMAGES)) {return;}boolean isLowRam = ActivityManager.isLowRamDeviceStatic();//判断设备是否为低内存if (mLargeIcon != null || largeIcon != null) {Resources resources = context.getResources();Class<? extends Style> style = getNotificationStyle();//不管是否为低内存,maxSize=48dp,源码定义在:/frameworks/base/core/res/res/values/dimens.xmlint maxSize = resources.getDimensionPixelSize(isLowRam? R.dimen.notification_right_icon_size_low_ram: R.dimen.notification_right_icon_size);if (mLargeIcon != null) {//压缩图片mLargeIcon.scaleDownIfNecessary(maxSize, maxSize);}if (largeIcon != null) {//压缩图片largeIcon = Icon.scaleDownIfNecessary(largeIcon, maxSize, maxSize);}}//对RemotView中的图片按规定的尺寸进行压缩reduceImageSizesForRemoteView(contentView, context, isLowRam);reduceImageSizesForRemoteView(headsUpContentView, context, isLowRam);reduceImageSizesForRemoteView(bigContentView, context, isLowRam);extras.putBoolean(EXTRA_REDUCED_IMAGES, true);}/***对RemotView中的图片按规定的尺寸进行压缩
*/private void reduceImageSizesForRemoteView(RemoteViews remoteView, Context context,boolean isLowRam) {if (remoteView != null) {Resources resources = context.getResources();int maxWidth = resources.getDimensionPixelSize(isLowRam? R.dimen.notification_custom_view_max_image_width_low_ram //294dp: R.dimen.notification_custom_view_max_image_width);//450dpint maxHeight = resources.getDimensionPixelSize(isLowRam? R.dimen.notification_custom_view_max_image_height_low_ram //208dp: R.dimen.notification_custom_view_max_image_height); //284dpremoteView.reduceImageSizes(maxWidth, maxHeight);}}源码路径: frameworks/base/graphics/java/android/graphics/drawable/Icon.java/*** 将位图缩小到给定的最大宽度和最大高度。 缩放将以统一的方式完成* @param bitmap 要缩小的位图* @param maxWidth 允许的最大宽度* @param maxHeight 允许的最大高度** @如果需要则返回缩放后的位图,如果不需要缩放则返回原始位图* @hide*/public static Bitmap scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight) {int bitmapWidth = bitmap.getWidth();int bitmapHeight = bitmap.getHeight();if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {float scale = Math.min((float) maxWidth / bitmapWidth,(float) maxHeight / bitmapHeight);bitmap = Bitmap.createScaledBitmap(bitmap,Math.max(1, (int) (scale * bitmapWidth)),Math.max(1, (int) (scale * bitmapHeight)),true /* filter */);}return bitmap;}
以上只是列举了压缩largeIcon 的例子,Notificaiton.java中,针对通知中的各种图片都做个指定尺寸的压缩.通知前期的优化完毕,继续看通知在NMS中的处理.
3. NMS 中保存通知的一些数据结构说明
源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java// 服务端维护的 已排序 的通知final ArrayList<NotificationRecord> mNotificationList = new ArrayList<>();// 服务端维护的 未排序 的通知final ArrayMap<String, NotificationRecord> mNotificationsByKey = new ArrayMap<>();// 入队通知: 保存所有入队的通知,当通知成功发送后则移除,即该列表记录的是所有入队成功且没有被发送出去的通知final ArrayList<NotificationRecord> mEnqueuedNotifications = new ArrayList<>();// 维护系统自动成组后的父通知final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries = new ArrayMap<>();// 服务端根据groupKey,维护着所有用户主动成组的父通知 final ArrayMap<String, NotificationRecord> mSummaryByGroupKey = new ArrayMap<>();
4. 通知到达enqueueNotificationWithTag()@NMS
enqueueNotificationWithTag()里调用了 enqueueNotificationInternal(),所以直接从enqueueNotificationInternal()开始学习,源码如下:
(1) enqueueNotificationInternal()
源码路径: frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.javavoid 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) {......checkRestrictedCategories(notification);//检查通知是否属于仅限系统使用的类别类型,// 优化通知,请看4.(2)try {fixNotification(notification, pkg, tag, id, userId);} catch (Exception e) {if (notification.isForegroundService()) {throw new SecurityException("Invalid FGS notification", e);}Slog.e(TAG, "Cannot fix notification", e);return;}// 检查setForegroundService()是否有FLAG_FOREGROUND_SERVICE权限final ServiceNotificationPolicy policy = mAmi.applyForegroundServiceNotification(notification, tag, id, pkg, userId);if (policy == ServiceNotificationPolicy.UPDATE_ONLY) {if (!isNotificationShownInternal(pkg, tag, id, userId)) {reportForegroundServiceUpdate(false, notification, id, pkg, userId);return;}}mUsageStats.registerEnqueuedByApp(pkg);//把通知封装成StatusBarNotification对象,即一条通知对应一个StatusBarNotification对象,主要面对App端final StatusBarNotification n = new StatusBarNotification(pkg, opPkg, id, tag, notificationUid, callingPid, notification,user, null, System.currentTimeMillis());// 创建channelId,String channelId = notification.getChannelId();if (mIsTelevision && (new Notification.TvExtender(notification)).getChannelId() != null) {channelId = (new Notification.TvExtender(notification)).getChannelId();}String shortcutId = n.getShortcutId();//Android8.0之后就需要为通知设置Channel,这里做了判断,否则无法发送通知final NotificationChannel channel = mPreferencesHelper.getConversationNotificationChannel(pkg, notificationUid, channelId, shortcutId,true /* parent ok */, false /* includeDeleted */);if (channel == null) {final String noChannelStr = "No Channel found for "+ "pkg=" + pkg+ ", channelId=" + channelId+ ", id=" + id+ ", tag=" + tag+ ", opPkg=" + opPkg+ ", callingUid=" + callingUid+ ", userId=" + userId+ ", incomingUserId=" + incomingUserId+ ", notificationUid=" + notificationUid+ ", notification=" + notification;Slog.e(TAG, noChannelStr);//获取通知的重要性boolean appNotificationsOff = mPreferencesHelper.getImportance(pkg, notificationUid)== NotificationManager.IMPORTANCE_NONE;if (!appNotificationsOff) {doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +"Failed to post notification on channel \"" + channelId + "\"\n" +"See log for more details");}return;}//把通知封装成NotificationRecord对象,即一条通知就是一个NotificationRecord对象,主要面对Service端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));if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {final boolean fgServiceShown = channel.isFgServiceShown();if (((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0|| !fgServiceShown)&& (r.getImportance() == IMPORTANCE_MIN|| r.getImportance() == IMPORTANCE_NONE)) {//提高通知的重要性if (TextUtils.isEmpty(channelId)|| NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) {r.setSystemImportance(IMPORTANCE_LOW);} else {channel.setImportance(IMPORTANCE_LOW);r.setSystemImportance(IMPORTANCE_LOW);if (!fgServiceShown) {channel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);channel.setFgServiceShown(true);}mPreferencesHelper.updateNotificationChannel(pkg, notificationUid, channel, false);r.updateNotificationChannel(channel);}} else if (!fgServiceShown && !TextUtils.isEmpty(channelId)&& !NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) {channel.setFgServiceShown(true);r.updateNotificationChannel(channel);}}ShortcutInfo info = mShortcutHelper != null? mShortcutHelper.getValidShortcutInfo(notification.getShortcutId(), pkg, user): null;if (notification.getShortcutId() != null && info == null) {Slog.w(TAG, "notification " + r.getKey() + " added an invalid shortcut");}r.setShortcutInfo(info);r.setHasSentValidMsg(mPreferencesHelper.hasSentValidMsg(pkg, notificationUid));r.userDemotedAppFromConvoSpace(mPreferencesHelper.hasUserDemotedInvalidMsgApp(pkg, notificationUid));//进一步过滤不符合规定的通知,限制通知速率和通知数量,请看4.(3)if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r,r.getSbn().getOverrideGroupKey() != null)) {return;}if (info != null) {// 缓存快捷方式mShortcutHelper.cacheShortcut(info, user);}// 暂时允许应用程序在启动待处理意图时执行额外的工作,if (notification.allPendingIntents != null) {final int intentCount = notification.allPendingIntents.size();if (intentCount > 0) {final long duration = LocalServices.getService(DeviceIdleInternal.class).getNotificationAllowlistDuration();for (int i = 0; i < intentCount; i++) {PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);if (pendingIntent != null) {mAmi.setPendingIntentAllowlistDuration(pendingIntent.getTarget(),ALLOWLIST_TOKEN, duration,TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,REASON_NOTIFICATION_SERVICE,"NotificationManagerService");mAmi.setPendingIntentAllowBgActivityStarts(pendingIntent.getTarget(),ALLOWLIST_TOKEN, (FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER| FLAG_SERVICE_SENDER));}}}}// 需要升级权限才能获得包重要性final long token = Binder.clearCallingIdentity();boolean isAppForeground;try {isAppForeground = mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND;} finally {Binder.restoreCallingIdentity(token);}//经过上面的一步一步过滤后,现在通知post到线程里,请看5分析mHandler.post(new EnqueueNotificationRunnable(userId, r, isAppForeground));}
(2) 第二次优化通知, fixNotification(notification, pkg, tag, id, userId)
源码路径: frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.javaprotected void fixNotification(Notification notification, String pkg, String tag, int id,int userId) throws NameNotFoundException {final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,(userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);//保存ApplicationInfo对象,请看分析2.(2)Notification.addFieldsFromContext(ai, notification);//检查权限,通知是否能着色,即通知中的 setColorized(boolean)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;}//检查全屏通知的权限,如果在Android Q(29)及以上给通知设置了fullScreenIntent,同时还//需要设置android.Manifest.permission.USE_FULL_SCREEN_INTENT权限,否则通知的//fullScreenIntent将被系统始终为null,即无效 if (notification.fullScreenIntent != null && ai.targetSdkVersion >= Build.VERSION_CODES.Q) {int fullscreenIntentPermission = mPackageManagerClient.checkPermission(android.Manifest.permission.USE_FULL_SCREEN_INTENT, pkg);if (fullscreenIntentPermission != PERMISSION_GRANTED) {//权限不足,该属性设置为nullnotification.fullScreenIntent = null;//fullScreenIntent无效日志Slog.w(TAG, "Package " + pkg +": Use of fullScreenIntent requires the USE_FULL_SCREEN_INTENT permission");}}// 检查 Style 样式中的action事件if (notification.isStyle(Notification.CallStyle.class)) {Notification.Builder builder =Notification.Builder.recoverBuilder(getContext(), notification);Notification.CallStyle style = (Notification.CallStyle) builder.getStyle();List<Notification.Action> actions = style.getActionsListWithSystemActions();notification.actions = new Notification.Action[actions.size()];actions.toArray(notification.actions);}// 检查RemoteView中的contentView,bigcontentView,headsUpContentView等是否超过checkRemoteViews(pkg, tag, id, notification);}/*** 检查RemouteView 的大小,是否超过了指定的大小
*/private boolean removeRemoteView(String pkg, String tag, int id, RemoteViews contentView) {if (contentView == null) {return false;}//获取当前RemoteView的大小final int contentViewSize = contentView.estimateMemoryUsage();//其中 mWarnRemoteViewsSizeBytes = 2000000 bytes , mStripRemoteViewsSizeBytes = 5000000 bytesif (contentViewSize > mWarnRemoteViewsSizeBytes&& contentViewSize < mStripRemoteViewsSizeBytes) {Slog.w(TAG, "RemoteViews too large on pkg: " + pkg + " tag: " + tag + " id: " + id+ " this might be stripped in a future release");}// contentViewSize >= 5000000 bytesif (contentViewSize >= mStripRemoteViewsSizeBytes) {mUsageStats.registerImageRemoved(pkg);Slog.w(TAG, "Removed too large RemoteViews (" + contentViewSize + " bytes) on pkg: "+ pkg + " tag: " + tag + " id: " + id);return true;}return false;}
(3) 限制通知速率和通知数量: checkDisqualifyingFeatures()
源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
涉及的源码路径:
速率的计算: /frameworks/base/services/core/java/com/android/server/notification/RateEstimator.java
保存不发送的通知:/frameworks/base/services/core/java/com/android/server/notification/NotificationUsageStats.java/*** 检查是否可以发布通知。 检查速率限制器、暂停助手和阻止。* 如果通知检查不合格,则返回 false,* 应用速率不能超过5000毫秒,通知总数不能超过50条*/boolean checkDisqualifyingFeatures(int userId, int uid, int id, String tag,NotificationRecord r, boolean isAutogroup) {Notification n = r.getNotification();final String pkg = r.getSbn().getPackageName();//是否为系统通知final boolean isSystemNotification =isUidSystemOrPhone(uid) || ("android".equals(pkg));//是否为通知监听器final boolean isNotificationFromListener = mListeners.isListenerPackage(pkg);// 限制除 android 之外的任何给定包的通知数量if (!isSystemNotification && !isNotificationFromListener) {final int callingUid = Binder.getCallingUid();if (mNotificationsByKey.get(r.getSbn().getKey()) == null&& isCallerInstantApp(callingUid, userId)) {// 临时应用程序对通知有一些特殊的限制。// 他们不被允许创建新的通知,但是他们被允许// 更新系统创建的通知(例如前台服务通知)。throw new SecurityException("Instant app " + pkg+ " cannot create notifications");}//限制更新未完成进度通知(即:进度条通知还在更新进度,当前速度还未达到最大值)的速率,if (mNotificationsByKey.get(r.getSbn().getKey()) != null&& !r.getNotification().hasCompletedProgress()&& !isAutogroup) {//算出这条通知距离上一个通知的时间差,然后算出速率,过程请看下面文字分析final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);//如果这个通知的速率大于规定的最大值,其中mMaxPackageEnqueueRate=5fif (appEnqueueRate > mMaxPackageEnqueueRate) {//把违规超速率的通知数量做好统计,保存在NotificationUsageStats.java中mUsageStats.registerOverRateQuota(pkg);final long now = SystemClock.elapsedRealtime();//这条通知的时间-上条通知的时间 > 5000 毫秒if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate+ ". Shedding " + r.getSbn().getKey() + ". package=" + pkg);mLastOverRateLogTime = now;}return false;//速率不合格,直接返回false}}// 限制应用程序可以拥有的非前台服务 未完成通知记录的数量if (!n.isForegroundService()) {//计算应用通知的总数,该总数:发送成功的通知+发送不成功的通知int count = getNotificationCount(pkg, userId, id, tag);// 应用总通知数 >= 50 条if (count >= MAX_PACKAGE_NOTIFICATIONS) {//把超出总数的通知保存在NotificationUsageStats.java中mUsageStats.registerOverCountQuota(pkg);Slog.e(TAG, "Package has already posted or enqueued " + count+ " notifications. Not showing more. package=" + pkg);return false;//通知总数不合格,直接返回false}}}// 气泡或内联回复是不可变的?if (n.getBubbleMetadata() != null&& n.getBubbleMetadata().getIntent() != null&& hasFlag(mAmi.getPendingIntentFlags(n.getBubbleMetadata().getIntent().getTarget()),PendingIntent.FLAG_IMMUTABLE)) {throw new IllegalArgumentException(r.getKey() + " Not posted."+ " PendingIntents attached to bubbles must be mutable");}if (n.actions != null) {for (Notification.Action action : n.actions) {if ((action.getRemoteInputs() != null || action.getDataOnlyRemoteInputs() != null)&& hasFlag(mAmi.getPendingIntentFlags(action.actionIntent.getTarget()),PendingIntent.FLAG_IMMUTABLE)) {throw new IllegalArgumentException(r.getKey() + " Not posted."+ " PendingIntents attached to actions with remote"+ " inputs must be mutable");}}}if (r.getSystemGeneratedSmartActions() != null) {for (Notification.Action action : r.getSystemGeneratedSmartActions()) {if ((action.getRemoteInputs() != null || action.getDataOnlyRemoteInputs() != null)&& hasFlag(mAmi.getPendingIntentFlags(action.actionIntent.getTarget()),PendingIntent.FLAG_IMMUTABLE)) {throw new IllegalArgumentException(r.getKey() + " Not posted."+ " PendingIntents attached to contextual actions with remote inputs"+ " must be mutable");}}}if (n.isStyle(Notification.CallStyle.class)) {boolean isForegroundService = (n.flags & FLAG_FOREGROUND_SERVICE) != 0;boolean hasFullScreenIntent = n.fullScreenIntent != null;if (!isForegroundService && !hasFullScreenIntent) {throw new IllegalArgumentException(r.getKey() + " Not posted."+ " CallStyle notifications must either be for a foreground Service or"+ " use a fullScreenIntent.");}}// 不发送snoozed类型的通知,当用户在设置中设置了不允许显示某个应用的通知(blocked)时,不再发送if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {MetricsLogger.action(r.getLogMaker().setType(MetricsProto.MetricsEvent.TYPE_UPDATE).setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED));mNotificationRecordLogger.log(NotificationRecordLogger.NotificationEvent.NOTIFICATION_NOT_POSTED_SNOOZED,r);if (DBG) {Slog.d(TAG, "Ignored enqueue for snoozed notification " + r.getKey());}mSnoozeHelper.update(userId, r);handleSavePolicyFile();return false;}// blocked appsif (isBlocked(r, mUsageStats)) {return false;}return true;}
5 . EnqueueNotificationRunnable@NMS
到此,通知经过优化后,最终进入到线程,下面是该线程的run() 方法 ,源码如下:
源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
public void run() {synchronized (mNotificationLock) {final Long snoozeAt =mSnoozeHelper.getSnoozeTimeForUnpostedNotification(r.getUser().getIdentifier(),r.getSbn().getPackageName(), r.getSbn().getKey());final long currentTime = System.currentTimeMillis();if (snoozeAt.longValue() > currentTime) {(new SnoozeNotificationRunnable(r.getSbn().getKey(),snoozeAt.longValue() - currentTime, null)).snoozeLocked(r);return;}final String contextId =mSnoozeHelper.getSnoozeContextForUnpostedNotification(r.getUser().getIdentifier(),r.getSbn().getPackageName(), r.getSbn().getKey());if (contextId != null) {(new SnoozeNotificationRunnable(r.getSbn().getKey(),0, contextId)).snoozeLocked(r);return;}//把通知添加到List中,即入队通知,指入队但未发送出去的通知,分析请看3mEnqueuedNotifications.add(r);scheduleTimeoutLocked(r);final StatusBarNotification n = r.getSbn();NotificationRecord old = mNotificationsByKey.get(n.getKey());//查看通知List中,是否已经存在该通知(通知的唯一标识为key),if (old != null) {// 保留以前记录的排名信息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();// 更新气泡通知updateNotificationBubbleFlags(r, isAppForeground);// 处理分组通知,详细介绍请看分析9handleGroupedNotificationLocked(r, old, callingUid, callingPid);if (n.isGroup() && notification.isGroupChild()) {mSnoozeHelper.repostGroupSummary(pkg, r.getUserId(), n.getGroupKey());}if (mAssistants.isEnabled()) {mAssistants.onNotificationEnqueuedLocked(r);//处理完之后,延迟post到PostNotificationRunnable线程,请看分析6mHandler.postDelayed(new PostNotificationRunnable(r.getKey()),DELAY_FOR_ASSISTANT_TIME);} else {//处理完之后,post到PostNotificationRunnable线程,请看分析6mHandler.post(new PostNotificationRunnable(r.getKey()));}}}}
上面是把通知添加到 ArrayList<NotificationRecord> mEnqueuedNotifications 列表中,该列表保存了所有待处理的通知,如果通知被取消、超时、处理完成后也会从该列表移除.
6. PostNotificationRunnable@NMS
(1) 继续分析 PostNotificationRunnable 的 run() 方法,该方法主要是通知发送前的一些处理,
源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.javapublic void run() {synchronized (mNotificationLock) {try {NotificationRecord r = null;int N = mEnqueuedNotifications.size();//遍历待处理通知列表,如果传递过来的key能在列表中存在,则把通知赋值给r,for (int i = 0; i < N; i++) {final NotificationRecord enqueued = mEnqueuedNotifications.get(i);if (Objects.equals(key, enqueued.getKey())) {r = enqueued;break;}}//如果列表中不存在该key通知,就returnif (r == null) {return;}//如果用户设置了不接收该通知,也return if (isBlocked(r)) {return;}//判断应用是否被系统限制了,即应用程序当前是否已暂停。final boolean isPackageSuspended =isPackagePausedOrSuspended(r.getSbn().getPackageName(), r.getUid());r.setHidden(isPackageSuspended);if (isPackageSuspended) {//统计被限制的通知的数量mUsageStats.registerSuspendedByAdmin(r);}NotificationRecord old = mNotificationsByKey.get(key);final StatusBarNotification n = r.getSbn();final Notification notification = n.getNotification();if (old == null || old.getSbn().getInstanceId() == null) {n.setInstanceId(mNotificationInstanceIdSequence.newInstanceId());} else {n.setInstanceId(old.getSbn().getInstanceId());}//判断通知是新的,还是已存在的通知,主要是通过遍历 待处理通知列表,如果存在则返回通知在列表的位置,如果是新的通知,则返回-1int index = indexOfNotificationLocked(n.getKey());if (index < 0) {//将新的通知添加到 mNotificaitonList 列表中,mNotificationList.add(r);mUsageStats.registerPostedByApp(r);r.setInterruptive(isVisuallyInterruptive(null, r));} else {//如果已存在该通知,则更新已存在的通知,即更新通知内容,key值不变,通知排序也不变old = mNotificationList.get(index); mNotificationList.set(index, r);mUsageStats.registerUpdatedByApp(r, old);//确保通知更新过程中前台服务标志丢失notification.flags |=old.getNotification().flags & FLAG_FOREGROUND_SERVICE;r.isUpdate = true;final boolean isInterruptive = isVisuallyInterruptive(old, r);r.setTextChanged(isInterruptive);r.setInterruptive(isInterruptive);}//把通知添加到 列表中,这个列表在后面有说明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;}mRankingHelper.extractSignals(r);mRankingHelper.sort(mNotificationList);final int position = mRankingHelper.indexOf(mNotificationList, r);int buzzBeepBlinkLoggingCode = 0;if (!r.isHidden()) {//处理通知的震动,音效和呼吸灯buzzBeepBlinkLoggingCode = buzzBeepBlinkLocked(r);}if (notification.getSmallIcon() != null) {StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;//*****发送通知,通知各个listeners,其中就包括了SystemUI,详情请看分析7mListeners.notifyPostedLocked(r, old);if ((oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup()))&& !isCritical(r)) {mHandler.post(new Runnable() {@Overridepublic void run() { //构建父通知mGroupHelper.onNotificationPosted(n, hasAutoGroupSummaryLocked(n));}});} else if (oldSbn != null) {final NotificationRecord finalRecord = r;mHandler.post(() -> mGroupHelper.onNotificationUpdated(finalRecord.getSbn(), hasAutoGroupSummaryLocked(n)));}} else {//由于没有设置smallIcon,通知无法发送,通知listeners移除该通知.if (old != null && !old.isCanceled) {mListeners.notifyRemovedLocked(r,NotificationListenerService.REASON_ERROR, r.getStats());mHandler.post(new Runnable() {@Overridepublic 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();for (int i = 0; i < N; i++) {final NotificationRecord enqueued = mEnqueuedNotifications.get(i);if (Objects.equals(key, enqueued.getKey())) {mEnqueuedNotifications.remove(i);break;}}}}}
这里从待处理通知 ArrayList<Notification> mEnqueuednotifications 取出通知,经过一些列步骤,之后把该通知添加到列表 ArrayMap<String,NotificationRecord> mNotificationsByKey 中, 该列表保存了服务端中未排序的所有通知,用于确定该通知是更新旧通知还是新类型的通知.最后, mListeners.notifyPostedLocked(r, old); 通知各个监听通知的listeners 通知更新了, 其中 mListeners 指 NotificationListeners, 它是NotificationManagerService的内部类,下面继续分析.
7. 通知监听者,通知发生变化: mListeners.notifyPostedLocked()
源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.javaprivate NotificationListeners mListeners; public class NotificationListeners extends ManagedServices {......private void notifyPostedLocked(NotificationRecord r, NotificationRecord old,boolean notifyAllListeners) {try {StatusBarNotification sbn = r.getSbn();StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;TrimCache trimCache = new TrimCache(sbn);//过滤部分listener,如:不可见用户,Android P 以下hidden类型的通知 for (final ManagedServiceInfo info : getServices()) {boolean sbnVisible = isVisibleToListener(sbn, r. getNotificationType(), info);boolean oldSbnVisible = (oldSbn != null)&& isVisibleToListener(oldSbn, old.getNotificationType(), info);//如果通知不可见,则忽略if (!oldSbnVisible && !sbnVisible) {continue;}if (r.isHidden() && info.targetSdkVersion < Build.VERSION_CODES.P) {continue;}//过滤不通知所有监听者,并且版本大于Android P if (!notifyAllListeners && info.targetSdkVersion >= Build.VERSION_CODES.P) {continue;}//构建通知映射表,分析请看分析8final NotificationRankingUpdate update = makeRankingUpdateLocked(info);// 移除旧以前可见,现在不可见的通知if (oldSbnVisible && !sbnVisible) {final StatusBarNotification oldSbnLightClone = oldSbn.cloneLight();mHandler.post(() -> notifyRemoved(info, oldSbnLightClone, update, null, REASON_USER_STOPPED));continue;}//授权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(() -> notifyPosted(info, sbnToPost, update));}} catch (Exception e) {Slog.e(TAG, "Could not notify listeners for " + r.getKey(), e);}}
到此, 通过 mHandler.post(() -> notifyPosted(info, sbnToPost, update)) 方法将通知传递到各个监听器,其中,在发送通知给监听器之前,会对通知进行排序,然后构建通知Map, SystemUI 会根据这个map 对通知进行排序.
8. 通知发送前对通知进行排序
/*** 仅对监听器可见的通知进行排序,构建通知map,* key = StatusBarNotification.getKey();* value = NotificationListenerService.Ranking*/@GuardedBy("mNotificationLock")NotificationRankingUpdate makeRankingUpdateLocked(ManagedServiceInfo info) {final int N = mNotificationList.size();final ArrayList<NotificationListenerService.Ranking> rankings = new ArrayList<>();for (int i = 0; i < N; i++) {NotificationRecord record = mNotificationList.get(i);if (isInLockDownMode(record.getUser().getIdentifier())) {continue;}//过滤掉当前用户不可见的通知if (!isVisibleToListener(record.getSbn(), record.getNotificationType(), info)) {continue;}//获取通知关键字keyfinal String key = record.getSbn().getKey();//根据每个关键字对应一个 NotificationListenerService.Ranking, 即构成通知ArrayMapfinal NotificationListenerService.Ranking ranking =new NotificationListenerService.Ranking();//将通知的关键信息添加到ranking中ranking.populate(key,rankings.size(),!record.isIntercepted(),record.getPackageVisibilityOverride(),record.getSuppressedVisualEffects(),record.getImportance(),record.getImportanceExplanation(),record.getSbn().getOverrideGroupKey(),record.getChannel(),record.getPeopleOverride(),record.getSnoozeCriteria(),record.canShowBadge(),record.getUserSentiment(),record.isHidden(),record.getLastAudiblyAlertedMs(),record.getSound() != null || record.getVibration() != null,record.getSystemGeneratedSmartActions(),record.getSmartReplies(),record.canBubble(),record.isTextChanged(),record.isConversation(),record.getShortcutInfo(),record.getRankingScore() == 0? RANKING_UNCHANGED: (record.getRankingScore() > 0 ? RANKING_PROMOTED : RANKING_DEMOTED),record.getNotification().isBubbleNotification(),record.getProposedImportance());rankings.add(ranking);}return new NotificationRankingUpdate(rankings.toArray(new NotificationListenerService.Ranking[0]));}
9. 通知的分组
通知组简介
继续分析 4标题中 handleGroupedNotificationLocked() 系统处理分组的源码如下:
/*** 确保分组通知得到特殊处理** 如果新通知导致组丢失其摘要,则取消组子项。** <p>Updates mSummaryByGroupKey.</p>*/@GuardedBy("mNotificationLock")private void handleGroupedNotificationLocked(NotificationRecord r, NotificationRecord old,int callingUid, int callingPid) {StatusBarNotification sbn = r.getSbn();Notification n = sbn.getNotification();if (n.isGroupSummary() && !sbn.isAppGroup()) {// 没有组的通知不应该是摘要,否则自动成组可能会导致错误,分析请看9.(1)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();//更新 mSummaryByGroupKey,分析请看3if (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);}FlagChecker childrenFlagChecker = (flags) -> {if ((flags & FLAG_FOREGROUND_SERVICE) != 0) {return false;}return true;};// 如果更新导致组摘要消失,则清除旧通知的组子项。当旧通知是摘要而新通知不是摘要时,// 或者当旧通知是摘要并且其groupKey发生更改时,则原来父通知下的所有子通知会被移除if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) {cancelGroupChildrenLocked(old, callingUid, callingPid, null, false /* sendDelete */,childrenFlagChecker, REASON_APP_CANCEL, SystemClock.elapsedRealtime());}}
(1) 如果 setGroupSummary(boolean isGroupSummary)
设置了Notification.FLAG_GROUP_SUMMARY
这个flag,但是没有调用setGroup(String groupKey)
设置对应的groupKey
, 则Notification.FLAG_GROUP_SUMMARY
这个flag会被去掉,否则会导致后续系统的自动成组导致出错。
10. 使用规则更新通知属性值(排序前更新)
源码路径:frameworks/base/services/core/java/com/android/server/notification/RankingConfig.javapublic interface RankingConfig {void setImportance(String packageName, int uid, int importance);int getImportance(String packageName, int uid);void setShowBadge(String packageName, int uid, boolean showBadge);boolean canShowBadge(String packageName, int uid);boolean badgingEnabled(UserHandle userHandle);int getBubblePreference(String packageName, int uid);boolean bubblesEnabled(UserHandle userHandle);boolean isMediaNotificationFilteringEnabled();boolean isGroupBlocked(String packageName, int uid, String groupId);boolean canShowNotificationsOnLockscreen(int userId);boolean canShowPrivateNotificationsOnLockScreen(int userId);Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg,int uid);void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,boolean fromTargetApp);ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty);boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel, boolean fromTargetApp, boolean hasDndAccess);void updateNotificationChannel(String pkg, int uid, NotificationChannel channel,boolean fromUser);NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,boolean includeDeleted);NotificationChannel getConversationNotificationChannel(String pkg, int uid, String channelId, String conversationId, boolean returnParentIfNoConversationChannel,boolean includeDeleted);boolean deleteNotificationChannel(String pkg, int uid, String channelId);void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId);void permanentlyDeleteNotificationChannels(String pkg, int uid);ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,boolean includeDeleted);
}
上面是规则接口类,下面分析该接口的实现类,举例通知圆点进行说明:
源码路径: frameworks/base/services/core/java/com/android/server/notification/PreferencesHelper.javapublic class PreferencesHelper implements RankingConfig {......@Overridepublic boolean canShowBadge(String packageName, int uid) {synchronized (mPackagePreferences) {return getOrCreatePackagePreferencesLocked(packageName, uid).showBadge;}}//设置某个应用的通知圆点开关,开启或者关闭@Overridepublic void setShowBadge(String packageName, int uid, boolean showBadge) {synchronized (mPackagePreferences) {getOrCreatePackagePreferencesLocked(packageName, uid).showBadge = showBadge;}updateConfig();//更新属性配置}......(1) 两个方法中都调用了同一个方法 getOrCreatePackagePreferencesLocked(),private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg,@UserIdInt int userId, int uid, int importance, int priority, int visibility,boolean showBadge, int bubblePreference) {final String key = packagePreferencesKey(pkg, uid);PackagePreferences r = (uid == UNKNOWN_UID)? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId)): mPackagePreferences.get(key);if (r == null) {r = new PackagePreferences();r.pkg = pkg;r.uid = uid;r.importance = importance;r.priority = priority;r.visibility = visibility;r.showBadge = showBadge;r.bubblePreference = bubblePreference;if (mOemLockedApps.containsKey(r.pkg)) {List<String> channels = mOemLockedApps.get(r.pkg);if (channels == null || channels.isEmpty()) {r.oemLockedImportance = true;} else {r.oemLockedChannels = channels;}}try {createDefaultChannelIfNeededLocked(r);} catch (PackageManager.NameNotFoundException e) {Slog.e(TAG, "createDefaultChannelIfNeededLocked - Exception: " + e);}if (r.uid == UNKNOWN_UID) {mRestoredWithoutUids.put(unrestoredPackageKey(pkg, userId), r);} else {mPackagePreferences.put(key, r);}}return r;}(2) 该方法返回 PackagePreferences 对象,它是PreferencesHelper.java的内部类,接着看下该对象有哪些属性:private static class PackagePreferences {String pkg;int uid = UNKNOWN_UID;int importance = DEFAULT_IMPORTANCE;//通知重要性int priority = DEFAULT_PRIORITY; //通知优先级int visibility = DEFAULT_VISIBILITY; //通知可见性boolean showBadge = DEFAULT_SHOW_BADGE; //通知原点int bubblePreference = DEFAULT_BUBBLE_PREFERENCE; //通知气泡int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS; boolean oemLockedImportance = DEFAULT_OEM_LOCKED_IMPORTANCE;List<String> oemLockedChannels = new ArrayList<>();boolean defaultAppLockedImportance = DEFAULT_APP_LOCKED_IMPORTANCE;boolean hasSentInvalidMessage = false;boolean hasSentValidMessage = false;boolean userDemotedMsgApp = false;Delegate delegate = null;ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();Map<String, NotificationChannelGroup> groups = new ConcurrentHashMap<>();public boolean isValidDelegate(String pkg, int uid) {return delegate != null && delegate.isAllowed(pkg, uid);}}(3)该内部类对象保存了通知的一些属性,是通知属性的封装类,如上面两个方法中,
都用到了getOrCreatePackagePreferencesLocked(packageName, uid).showBadge
来获取通知是否开启通知原点功能, 该方法相当于是通过 PackagePreferences.showBadge
获取属性值,之后便可以通过PreferencesHelper 来获取通知最新的属性.
通过 设置 或者 桌面快捷方式 可以打开通知圆点功能,请求会从 设置 跨进程发送到NotificationManagerService(NMS), NMS 会通过setShowBadge()@PreferencesHelper来更新属性,并把最新属性值保存到PreferencesHelper对象中.
相关文章:
Android 12.0 通知发送过程源码分析-Framework
以下NotificationManagerService简称 NMS 1. 通知的发送: NotificationManager.notify(int id, Notification notification) 开始. 源码路径: /frameworks/base/core/java/android/app/NotificationManager.java/***发布通知以显示在状态栏中。 如果通知带有* 相同的 ID 已被…...
提防远程攻击:了解正向 Shell 和反向 Shell 确保服务器安全
前言 在当今网络安全形势日益复杂的环境中,了解正向 Shell 和反向 Shell 的工作原理和使用场景,对于保护你的服务器免受远程攻击至关重要。本文不仅深入解析这两种常见的远程控制技术,还将提供有效的防护建议,帮助你提升服务器的…...
RabbitMQ中CorrelationData 与DeliveryTag的区别
在RabbitMQ中,CorrelationData是一个用于封装业务ID信息的类,它主要在消息确认机制中发挥作用。以下是关于CorrelationData在RabbitMQ中的详细作用: 封装业务ID信息: 当发送消息时,可以将业务ID信息封装在Correlation…...

数据恢复篇:如何在Android上恢复删除的短信
如果您不小心删除了Android设备上的短信并想要检索它们,则可以尝试以下方法: 如何在Android上恢复删除的短信 检查您的备份: 如果您之前备份了Android设备,则可以从备份中恢复已删除的短信。检查您设备的内部存储空间或 Google 云…...

花了大几万的踩坑经验!宠物空气净化器哪个牌子好:希喂、小米、有哈PK
我的闺蜜最近向我大吐苦水,自从家里养了猫之后,她发现家里的空气质量大不如前。宠物的浮毛和排泄物的气味在空气中飘散,让她非常怀念以前没有养猫时家里清新的呼吸环境。她觉得这些漂浮的毛发和异味大大降低了居家的舒适度。 还引起了身体上…...

查普曼大学团队使用惯性动捕系统制作动画短片
道奇电影和媒体艺术学院是查普曼大学的知名学院,同时也是美国首屈一指的电影学院之一,拥有一流电影制作工作室。 最近,道奇学院的一个学生制作团队接手了一个项目,该项目要求使用真人动作、视觉效果以及真人演员和CG角色之间的互动…...

vue 代理
一、常用的发送一个ajax请求: 1、xhr new XMLHttpRequest(),真正开发中不常用 2、jq,jq主要功能是获取dom,周边才是请求接口 3、axios(大名鼎鼎的) axios.get("url").then(response>{},error>{} )4、…...

[leetcode]24-game
. - 力扣(LeetCode) class Solution { public:static constexpr int TARGET 24;static constexpr double EPSILON 1e-6;static constexpr int ADD 0, MULTIPLY 1, SUBTRACT 2, DIVIDE 3;bool judgePoint24(vector<int> &nums) {vector&l…...
网络爬虫的原理
网络爬虫的原理 网络爬虫,作为信息检索和数据分析的重要工具,其原理的核心在于模拟人类浏览网页的行为,通过自动化的方式从互联网上收集所需的数据。在了解了网络爬虫的基本原理后,我们可以进一步探讨其在实际应用中的工作机制以…...

游戏AI的创造思路-技术基础-机器学习(2)
本篇存在大量的公式,数学不好的孩子们要开始恶补数学了,尤其是统计学和回归方程类的内容。 小伙伴们量力而行~~~~~ 游戏呢,其实最早就是数学家、元祖程序员编写的数学游戏,一脉相承传承至今,囊括了更多的设计师、美术…...
【深度学习】记录为什么没有调用GPU
排查CLIP为什么评测推理没有调用GPU,主要是这个代码:https://github.com/OFA-Sys/Chinese-CLIP/blob/master/cn_clip/eval/extract_features.py 第一次认为:因为model并没有to.cuda()。 但是又发现,model.cuda(args.gpu) # 已经加…...

vite 创建vue3项目 集成 ESLint、Prettier、Sass等
在网上找了一大堆vue3脚手架的东西,无非就是vite或者vue-cli,在vue2时代,vue-cli用的人挺多的,也很好用,然而vue3大多是和vite搭配搭建的,而且个人感觉vite这个脚手架并没有那么的好用,搭建项目时只能做两个…...

计算机系统基础知识(上)
目录 计算机系统的概述 计算机的硬件 处理器 存储器 总线 接口 外部设备 计算机的软件 操作系统 数据库 文件系统 计算机系统的概述 如图所示计算机系统分为软件和硬件:硬件包括:输入输出设备、存储器,处理器 软件则包括系统软件和…...

[深度学习]循环神经网络RNN
RNN(Recurrent Neural Network,即循环神经网络)是一类用于处理序列数据的神经网络,广泛应用于自然语言处理(NLP)、时间序列预测、语音识别等领域。与传统的前馈神经网络不同,RNN具有循环结构&am…...

【C++:list】
list概念 list是一个带头的双向循环链表,双向循环链表的特色:每一个节点拥有两 个指针进行维护,俩指针分别为prev和next,prev指该节点的前一个节点,next为该节点的后一个节点 list的底层实现中为什么对迭代器单独写一个结构体进行…...
解锁 Apple M1/M2 上的深度学习力量:安装 TensorFlow 完全指南
前言 随着 Apple M1 和 M2 芯片的问世,苹果重新定义了笔记本电脑和台式机的性能标准。这些强大的芯片不仅适用于日常任务,还能处理复杂的机器学习和深度学习工作负载。本文将详细介绍如何在 Apple M1 或 M2 芯片上安装和配置 TensorFlow,助你…...
Apache Iceberg:现代数据湖存储格式的未来
Apache Iceberg 是一个开源的表格式,用于在分布式数据湖中管理大规模数据集。它由 Netflix 开发,并捐赠给 Apache 基金会。Iceberg 的设计目标是解决传统数据湖存储格式(如 Apache Hive 和 Apache Parquet)在大规模数据管理中的一…...

【离散数学·图论】(复习)
一、基本概念 1.一些基本术语: 2.点u,v邻接(或相邻): 边e称为关联顶点u和v,or e连接u和v; 3.G(V,E)中,顶点v所有邻居的集合:N(v), 成为v的邻域。 4.度 : deg(v) 5.悬挂点:度为1的…...

【ONLYOFFICE震撼8.1】ONLYOFFICE8.1版本桌面编辑器测评
随着远程工作的普及和数字化办公的发展,越来越多的人开始寻找一款具有强大功能和便捷使用的办公软件。在这个时候,ONLYOFFICE 8.1应运而生,成为了许多用户的新选择。ONLYOFFICE 8.1是一种办公套件软件,它提供了文档处理、电子表格…...

Shell 脚本编程保姆级教程(上)
一、运行第一个 Shell 脚本 1.1 Shell 脚本 Shell 脚本(shell script),是一种为 shell 编写的脚本程序。 业界所说的 shell 通常都是指 shell 脚本,但读者朋友要知道,shell 和 shell script 是两个不同的概念。 由…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...

MMaDA: Multimodal Large Diffusion Language Models
CODE : https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA,它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...

P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...

PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...

Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...

用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...