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 是两个不同的概念。 由…...
凸优化相关文章汇总
深度学习/机器学习入门基础数学知识整理(三):凸优化,Hessian,牛顿法_深度学习和凸优化-CSDN博客 深度学习/机器学习入门基础数学知识整理(四):拟牛顿法、BFGS、L-BFGS、DFP、共轭梯…...
Java鲜花下单预约系统源码小程序源码
让美好触手可及 🌸一、开启鲜花新篇章 在繁忙的都市生活中,我们总是渴望那一抹清新与美好。鲜花,作为大自然的馈赠,总能给我们带来无尽的惊喜与愉悦。但你是否曾因为工作繁忙、时间紧张而错过了亲自挑选鲜花的机会?今…...
网络变压器和RJ45接线的方法
网络变压器在以太网硬件电路设计中扮演着重要的角色,它主要用于信号电平耦合、隔离外部干扰、实现阻抗匹配以及增加传输距离。而RJ45接口则是以太网连接的标准化接口,它提供了与网络电缆的连接点。 网络变压器与RJ45的接线方法通常遵循以下步骤…...
Matlab/simulink三段式电流保护
电流1段仿真波形如下所示 电流2段仿真波形如下所示 电流3段仿真波形如下所示...
OOXML入门学习
进入-飞入 <par> <!-- 这是一个并行动画序列的开始。"par"代表并行,意味着在这个标签内的所有动画将同时开始。 --><cTn id"5" presetID"2" presetClass"entr" presetSubtype"4" fill"hold&…...
k8s集群node节点加入失败
出现这种情况: [preflight] FYI: You can look at this config file with kubectl -n kube-system get cm kubeadm-config -o yaml [kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml" [kubelet-start] Writing kub…...
layui+jsp项目中实现table单元格嵌入下拉选择框功能,下拉选择框可手动输入内容或选择默认值,修改后数据正常回显。
需求 table列表中的数据实现下拉框修改数据,当默认的下拉框不符合要求时,可手动输入内容保存。内容修改后表格显示修改后的值同时表格不刷新。 实现 layui框架下拉框组件只能选择存在的数据,不支持将输入的内容显示在input中的功能&#x…...
2024年客户体验的几个预测
数字化转型、以客户为中心的理念、数字技术的发展和产品的不断创新,都为客户体验带来了巨大的改变。 目前,我们看到很多公司都在致力于塑造一种以客户为中心的商业模式。企业开始用更多技术、更多数据和更多产品来强化自己在客户体验方面的能力。 那么&a…...
【C++】动态内存管理new和delete
文章目录 一、C的内存管理方式二、new和delete的用法1.操作内置类型2.操作自定义内置类型 三、new和delete的底层实现1.operator new和operator delete函数2.new和delete的实现原理 四、定位new表达式五、malloc/free和new/delete的区别 一、C的内存管理方式 之前在C语言的动态…...
Java面向对象特性
Java继承: 继承的概念: 在Java中,继承(inheritance)是面向对象编程的一个重要概念,它允许一个类(子类)继承另一个类(父类)的属性和方法。通过继承,…...
17网站一起做网店图片工具/大众点评seo关键词优化
2019独角兽企业重金招聘Python工程师标准>>> 【Java线程】volatile的适用场景 博客分类: java http://www.ibm.com/developerworks/cn/java/j-jtp06197.html 把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子…...
怎样做恶搞网站/上海推广外包
1. collections模块collections模块主要封装了⼀些关于集合类的相关操作. 比如, 我们学过的Iterable,Iterator等等. 除了这些以外, collections还提供了⼀些除了基本数据类型以外的数据集合类型. Counter, deque, OrderedDict, defaultdict以及namedtuple class Animal:passfr…...
网站流量做那些好/seo外包顾问
Windows XP是一款经典的操作系统,同时也是一款很老的操作系统,不过尽管如此,还是有一批用户在使用XP系统,所以发行一些软件的时候还是要测试在XP系统中能否运行,这时候我们就可以借助VirtualBox虚拟机安装一个XP系统来…...
做橙光游戏的网站/web成品网站源码免费
一、集合操作 1.UNION:并集运算。 语法结构: SQL>select 表1的列1, 表1的列2 from 表1 union select表2的列1, 表2的列2 from表2; 其中表1的列1和表1的列2是来自于表1的两列,表2的列1和表2的列2是来自于表2的两列,需要注意…...
网站设计与制作的流程/重庆 seo
问题描述:为了实现前后端的彻底分离,我们彻底放弃使用.jsp的方式在前端显示页面中穿插java代码,但是带来的问题也比较明显,就是前端向后台发出请求的时候可能会出现跨域的问题,浏览器为了安全会阻止跨域请求。目前有一…...
萝岗免费网站建设/如何开发一款app软件
转载来自:http://blog.csdn.net/xiaowei_cqu/article/details/7586847 前一天把系统整个重写了一遍,脉络清晰了很多,也终于解决了以前很多崩溃,异常退出的问题。这里小小总结一下自己遇到的麻烦。 1、内存泄露 内存泄露是说没有释…...