Android 14 NotificationChannels与Notification的加载流程

前言

这部分我觉得三方应用使用的较多,分析的时候也是源码与三方应用结合分析的。

一. NotificationChannel 的创建

在源码中,我看到了一个很怪的类:NotificationChannels.java。这个类继承了 CoreStartable。

注:CoreStartable 就是 SystemUI,只是我这的源码的命名不一样,下面为了便于他人阅读,就以 SystemUI 来叫。

NotificationChannels.java 就百十行代码,很简单,一起看看这个类:
frameworks/base/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java

public class NotificationChannels extends CoreStartable {
    // ...
    // 省略代码
    public static void createAll(Context context) {
        final NotificationManager nm = context.getSystemService(NotificationManager.class);
        // 创建通道
        final NotificationChannel batteryChannel = new NotificationChannel(BATTERY,
                context.getString(R.string.notification_channel_battery),
                NotificationManager.IMPORTANCE_MAX);
        final String soundPath = Settings.Global.getString(context.getContentResolver(),
                Settings.Global.LOW_BATTERY_SOUND);
        batteryChannel.setSound(Uri.parse("file://" + soundPath), new AudioAttributes.Builder()
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
                .build());
        batteryChannel.setBlockable(true);
        // 创建通道
        final NotificationChannel alerts = new NotificationChannel(
                ALERTS,
                context.getString(R.string.notification_channel_alerts),
                NotificationManager.IMPORTANCE_HIGH);
        // 创建通道
        final NotificationChannel general = new NotificationChannel(
                GENERAL,
                context.getString(R.string.notification_channel_general),
                NotificationManager.IMPORTANCE_MIN);
        // 创建通道
        final NotificationChannel storage = new NotificationChannel(
                STORAGE,
                context.getString(R.string.notification_channel_storage),
                isTv(context)
                        ? NotificationManager.IMPORTANCE_DEFAULT
                        : NotificationManager.IMPORTANCE_LOW);
        // 创建通道
        final NotificationChannel hint = new NotificationChannel(
                HINTS,
                context.getString(R.string.notification_channel_hints),
                NotificationManager.IMPORTANCE_DEFAULT);
        // No need to bypass DND.
        // 注册通道
        nm.createNotificationChannels(Arrays.asList(
                alerts,
                general,
                storage,
                createScreenshotChannel(
                        context.getString(R.string.notification_channel_screenshot),
                        nm.getNotificationChannel(SCREENSHOTS_LEGACY)),
                batteryChannel,
                hint
        ));
        // Delete older SS channel if present.
        // Screenshots promoted to heads-up in P, this cleans up the lower priority channel from O.
        // This line can be deleted in Q.
        nm.deleteNotificationChannel(SCREENSHOTS_LEGACY);
        
        if (isTv(context)) {
            // TV specific notification channel for TV PIP controls.
            // Importance should be {@link NotificationManager#IMPORTANCE_MAX} to have the highest
            // priority, so it can be shown in all times.
            // 注册通道
            nm.createNotificationChannel(new NotificationChannel(
                    TVPIP,
                    context.getString(R.string.notification_channel_tv_pip),
                    NotificationManager.IMPORTANCE_MAX));
        }
    }
    /**
     * Set up screenshot channel, respecting any previously committed user settings on legacy
     * channel.
     * @return
     */
    @VisibleForTesting static NotificationChannel createScreenshotChannel(
            String name, NotificationChannel legacySS) {
        NotificationChannel screenshotChannel = new NotificationChannel(SCREENSHOTS_HEADSUP,
                name, NotificationManager.IMPORTANCE_HIGH); // pop on screen
        screenshotChannel.setSound(null, // silent
                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
        screenshotChannel.setBlockable(true);
        if (legacySS != null) {
            // Respect any user modified fields from the old channel.
            int userlock = legacySS.getUserLockedFields();
            if ((userlock & NotificationChannel.USER_LOCKED_IMPORTANCE) != 0) {
                screenshotChannel.setImportance(legacySS.getImportance());
            }
            if ((userlock & NotificationChannel.USER_LOCKED_SOUND) != 0)  {
                screenshotChannel.setSound(legacySS.getSound(), legacySS.getAudioAttributes());
            }
            if ((userlock & NotificationChannel.USER_LOCKED_VIBRATION) != 0)  {
               screenshotChannel.setVibrationPattern(legacySS.getVibrationPattern());
            }
            if ((userlock & NotificationChannel.USER_LOCKED_LIGHTS) != 0)  {
                screenshotChannel.setLightColor(legacySS.getLightColor());
            }
            // skip show_badge, irrelevant for system channel
        } 
        return screenshotChannel;
    }
    @Override
    public void start() {
        createAll(mContext);
    }
    private static boolean isTv(Context context) {
        PackageManager packageManager = context.getPackageManager();
        return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
    }
}

NotificationChannels 扩展自 SystemUI 并重写了 start() 方法,它执行了 createAll() 方法,创建了通知通道有 batteryChannel(电池)、alerts(提醒)、storage(存储空间)、screenshot(屏幕截图)、hint (提示)、general(常规消息)。
此外,如果是 TV 设备的话还会创建画中画通知通道。

  • 怪在哪呢:为什么在这个类去创建注册那些通知通道,而且并没有提示消息什么的,意义在哪?
  • 注:下面我把该类当作三方应用。

下面围绕 NotificationChannels 一步一步的分析,上面调用 new NotificationChannel() 创建通知通道,然后 调用 nm.createNotificationChannels() 方法注册通道。
nm 其实是 NotificationManager 的对象,这样就转到了 NotificationManager 中。这里我作了一个流程图:

从 NotificationManager.createNotificationChannel() 到 NotificationManagerService.createNotificationChannelsImpl() 都是正常流程,也好理解。创建的关键代码在 mPreferencesHelper.createNotificationChannel() 中,具体如下:
frameworks/base/services/core/java/com/android/server/notification/PreferencesHelper.java

    @Override
    public boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel,
            boolean fromTargetApp, boolean hasDndAccess) {
        Objects.requireNonNull(pkg);
        Objects.requireNonNull(channel);
        Objects.requireNonNull(channel.getId());
        Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName()));
        boolean needsPolicyFileChange = false, wasUndeleted = false, needsDndChange = false;
        synchronized (mPackagePreferences) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
            if (r == null) {
                throw new IllegalArgumentException("Invalid package");
            }
            if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
                throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
            }
            if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
                throw new IllegalArgumentException("Reserved id");
            }
            // 前面是各种条件检查,下面这行是关键点,先检索这个 channel 是否已经存在,以 channel id 为标志位。
            NotificationChannel existing = r.channels.get(channel.getId());
            // 如果通道已经存在就更新通道
            //  更新通道保留大部分已存在的设置,只更新了 name,description 等几项
            if (existing != null && fromTargetApp) {
                 // 省略部分代码......
            } else {
                 // 省略部分代码......
                // channel 未创建过,把用户创建的 channel 加入到系统的 cache 里
                r.channels.put(channel.getId(), channel);
                if (channel.canBypassDnd() != mAreChannelsBypassingDnd) {
                    needsDndChange = true;
                }
                MetricsLogger.action(getChannelLog(channel, pkg).setType(
                        com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN));
                mNotificationChannelLogger.logNotificationChannelCreated(channel, uid, pkg);
            }
        }
        if (needsDndChange) {
            updateChannelsBypassingDnd();
        }
        return needsPolicyFileChange;
    }

至此,一个通知完整的创建完成。
其实通过mPreferencesHelper.createNotificationChannel() 方法还能看出 NotificationChannel 一旦创建,那么能更改的东西就很少了(只有名字,描述,blocksystem,以及优先级),而 blocksystem 属性只有在系统源码里面才能使用(hide);

NotificationChannel 不会重复创建。

Android官方是这么解释这个设计的:NotificationChannel 就像是开发者送给用户的一个精美礼物,一旦送出去,控制权就在用户那里了。即使用户把通知铃声设置成《江南style》,你可以知道,但不可以更改。

二. Notification 的显示过程

这里代码有点多,我制作了一个通知传递的方法调用流程图:

上述流程图中,我们可能更比较关注 NotificationManagerService 是怎么与 SystemUI 交互的。
其实SystemUI向 NotificationManagerService 注册一个"服务"(一个Binder)。这个"服务"就相当于客户端 SystemUI 在服务端 NotificationManagerService 注册的一个回调。当有通知来临的时候,就会通过这个"服务"通知SystemUI,这个注册是在 StatusBar#setUpPresenter() 中完成的:
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java

private void setUpPresenter() {
    // 省略部分代码......  
    // 这位置调用了NotificationsControllerImpl#initialize()的方法
    mNotificationsController.initialize(
            mPresenter,
            mNotifListContainer,
            mStackScrollerController.getNotifStackController(),
            mNotificationActivityStarter,
            mCentralSurfacesComponent.getBindRowCallback());
}

NotificationsControllerImpl#initialize() 中进行注册:
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt

override fun initialize(
    presenter: NotificationPresenter,
    listContainer: NotificationListContainer,
    stackController: NotifStackController,
    notificationActivityStarter: NotificationActivityStarter,
    bindRowCallback: NotificationRowBinderImpl.BindRowCallback
) {
    // 注册回调
    notificationListener.registerAsSystemService()
}

上述注册了之后,每当有通知来时就会回调到:NotificationListener#onNotificationPosted() 中,接着就会到 NotificationEntryManager中。
下面分析通知视图的加载,这里就直接从 NotificationEntryManager#onNotificationPosted() 开始。
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescer.java

    private final NotificationHandler mNotifListener = new NotificationHandler() {
        @Override
        public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
            maybeEmitBatch(sbn);
            applyRanking(rankingMap);

            final boolean shouldCoalesce = handleNotificationPosted(sbn, rankingMap);
            // 通过key值进行判断,通知是否已存在
            if (shouldCoalesce) {
                mLogger.logEventCoalesced(sbn.getKey());
                mHandler.onNotificationRankingUpdate(rankingMap);
            } else {
                mHandler.onNotificationPosted(sbn, rankingMap);
            }
        }
    }

通过上述源码可以知道,通知到后,首先会进行判断该通知是否存在,存在则刷新,不存在则添加;这里以添加为例去分析。

先看两张图,可以知道下面分析的方向:

SystemUI组件思维导图:

SystemUI 关键布局图

根布局:super_status_bar.xml,
顶上状态栏: status_bar.xml, 通过CollapsedStatusBarFragment.java加载;PhoneStatusBarView(FrameLayout,)是里面的父控件; 对应 R.id.status_bar_container 。
下拉状态栏:(包括通知为status_bar_expanded.xml),最外层布局NotificationPanelView;qs_frame.xml 为下拉后的状态栏部分(用QSFragment管理,布局控件为QSContainerImpl),其高度更新在QSContainerImpl.java中;
NotificationStackScrollLayout: 用于下拉的通知的相关问题(占满全屏,包括导航栏,会处理点击状态栏空白区的逻辑)。

NotificationStackScrollLayout:是一个滑动布局,里面嵌套着 ExpandableNotificationRow ,即通知。

接着上面分析:上面我们只关注 addNotification(sbn, rankingMap) ,而它内部时调用 addNotificationInternal() 方法实现的。
NotificationEntryManager#addNotificationInternal()

// NotificationEntryManager.java
private void addNotificationInternal(
        StatusBarNotification notification,
        RankingMap rankingMap) throws InflationException {
    
    // 省略部分代码 ...
    NotificationEntry entry = mPendingNotifications.get(key);
    // 省略部分代码 ...
    // 构造视图
    if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
        // NotificationRowBinderImpl 为 NotificationRowBinder 的实现类
        mNotificationRowBinderLazy.get().inflateViews(entry, null, mInflationCallback);
    }
    // 省略部分代码 ...
}

首先为通知创建一个 NotificationEntry 通知实例,然后再通过 NotificationRowBinderImpl 中的 inflateViews() 加载通知视图,绑定通知信息,并在通知栏添加通知视图,以及在状态栏添加通知图标。
NotificationRowBinderImpl#inflateViews()
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java

@Override
public void inflateViews(
        NotificationEntry entry,
        NotifInflater.Params params,
        NotificationRowContentBinder.InflationCallback callback)
        throws InflationException {
    if (params == null) {
        // weak assert that the params should always be passed in the new pipeline
        mNotifPipelineFlags.checkLegacyPipelineEnabled();
    }
    // 获取查看父布局
    ViewGroup parent = mListContainer.getViewParentForNotification(entry);
    // 通知是否存在
    if (entry.rowExists()) {
        mIconManager.updateIcons(entry);
        ExpandableNotificationRow row = entry.getRow();
        row.reset();
        updateRow(entry, row);
        inflateContentViews(entry, params, row, callback);
    } else {
        // 创建图标
        mIconManager.createIcons(entry);
        mRowInflaterTaskProvider.get().inflate(mContext, parent, entry,
                row -> {
                    // 为视图设置控制器.
                    ExpandableNotificationRowComponent component =
                            mExpandableNotificationRowComponentBuilder
                                    .expandableNotificationRow(row)
                                    .notificationEntry(entry)
                                    .onExpandClickListener(mPresenter)
                                    .listContainer(mListContainer)
                                    .build();
                    ExpandableNotificationRowController rowController =
                            component.getExpandableNotificationRowController();
                    rowController.init(entry);
                    entry.setRowController(rowController);
                    bindRow(entry, row);
                    updateRow(entry, row);
                    inflateContentViews(entry, params, row, callback);
                });
    }
}

上面无论走哪个分支,最后进入到inflateContentViews(entry, row, callback);这是一个回调:
NotificationRowBinderImpl#inflateContentViews()
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java

// 加载该行的基本内容视图
private void inflateContentViews(
        NotificationEntry entry,
        NotifInflater.Params inflaterParams,
        ExpandableNotificationRow row,
        @Nullable NotificationRowContentBinder.InflationCallback inflationCallback) {
    
        // 省略部分代码......
    params.rebindAllContentViews();
    mRowContentBindStage.requestRebind(entry, en -> {
        row.setUsesIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
        row.setIsLowPriority(isLowPriority);
        if (inflationCallback != null) {
            inflationCallback.onAsyncInflationFinished(en);
        }
    });
}

inflationCallback 是 NotificationRowContentBinder 的一个内部接口;在 NotificationEntryManager 中被实现,所以将回调到 NotificationEntryManager#onAsyncInflationFinished() 中。

// NotificationEntryManager.java
@Override
public void onAsyncInflationFinished(NotificationEntry entry) {
    Trace.beginSection("NotificationEntryManager.onAsyncInflationFinished");
    mPendingNotifications.remove(entry.getKey());
    // If there was an async task started after the removal, we don't want to add it back to
    // the list, otherwise we might get leaks.
    if (!entry.isRowRemoved()) {
        boolean isNew = getActiveNotificationUnfiltered(entry.getKey()) == null;
            // 省略部分代码......
        if (isNew) {
            // 省略部分代码......
            // 添加一个notification会走到这里、
            // 包括一开机就显示出来的那些notification
            addActiveNotification(entry);
            // 更新视图
            updateNotifications("onAsyncInflationFinished");
            // 省略部分代码......
        } else {
            // 省略部分代码......
        }
    }
    Trace.endSection();
}

这里直接看 updateNotifications(“onAsyncInflationFinished”) 方法;
NotificationEntryManager#updateNotification()

// NotificationEntryManager.java
public void updateNotifications(String reason) {
    // 省略部分代码......
    if (mPresenter != null) {
        // 更新视图
        mPresenter.updateNotificationViews(reason);
    }
    // 省略部分代码......
}

mPresenter 的实现类是 StatusBarNotificationPresenter,所以接着看其里面的 updateNotificationViews() 方法。
StatusBarNotificationPresenter#updateNotificationViews()
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java

@Override
public void updateNotificationViews(final String reason) {
    if (!mNotifPipelineFlags.checkLegacyPipelineEnabled()) {
        return;
    }
    // The function updateRowStates depends on both of these being non-null, so check them here.
    // We may be called before they are set from DeviceProvisionedController's callback.
    if (mScrimController == null) return;
    // 不要在折叠期间修改通知。.
    if (isCollapsing()) {
        mShadeController.addPostCollapseAction(() -> updateNotificationViews(reason));
        return;
    }
    // 把通知视图添加到通知面版的通知栏中
    mViewHierarchyManager.updateNotificationViews();
    // 这里不仅仅更新了通知面版的通知视图,也更新了状态栏的通知图标
    mNotificationPanel.updateNotificationViews(reason);
}

我们这里看通知面板更新,即 mNotificationPanel.updateNotificationViews(reason) 方法。mNotificationPanel 为 NotificationPanelViewController 的对象。
NotificationPanelViewController#updateNotificationViews(reason)
frameworks/base/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java

// 更新通知视图的部分和状态栏图标。每当显示的基础通知数据发生更改时,
// 这由 NotificationPresenter 触发。
public void updateNotificationViews(String reason) {
    // 更新NotificationStackScrollLayout 这个视图类的各种信息
    // updateSectionBoundaries() 这个方法还没弄明白,但我估计是添加/删除视图后布局重新定位,以及一个
    mNotificationStackScrollLayoutController.updateSectionBoundaries(reason);
    // Footer 其实就是通知面板底部的两个按钮:“管理”、“全部清除”。
    mNotificationStackScrollLayoutController.updateFooter();
    // 更新状态栏的通知图标
    mNotificationIconAreaController.updateNotificationIcons(createVisibleEntriesList());
}

至此通知面板的视图完成添加、更新。
下面接着看下状态栏的通知图标更新:
NotificationIconAreaController#updateNotificationIcons()
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java

public void updateNotificationIcons(List<ListEntry> entries) {
    mNotificationEntries = entries;
    updateNotificationIcons();
}
private void updateNotificationIcons() {
    Trace.beginSection("NotificationIconAreaController.updateNotificationIcons");
        // 更新状态栏图标
    updateStatusBarIcons();
    updateShelfIcons();
    // 更新 Aod 通知图标
    updateAodNotificationIcons();
    // 应用通知图标色调
    applyNotificationIconsTint();
    Trace.endSection();
}

下面都是调用 update XXX Icons() 这种类似的方法,接着调用 updateIconsForLayout() 方法,我们直接分析
NotificationIconAreaController#updateIconsForLayout():
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java

private void updateIconsForLayout(Function<NotificationEntry, StatusBarIconView> function,
        NotificationIconContainer hostLayout, boolean showAmbient, boolean showLowPriority,
        boolean hideDismissed, boolean hideRepliedMessages, boolean hideCurrentMedia,
        boolean hideCenteredIcon) {
    // toShow保存即将显示的图标
    ArrayList<StatusBarIconView> toShow = new ArrayList<>(
            mNotificationScrollLayout.getChildCount());
    // 过滤通知,并保存需要显示的通知图标
    for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) {
        // 获取一个通知视图
        View view = mNotificationScrollLayout.getChildAt(i);
        if (view instanceof ExpandableNotificationRow) {
            NotificationEntry ent = ((ExpandableNotificationRow) view).getEntry();
            if (shouldShowNotificationIcon(ent, showAmbient, showLowPriority, hideDismissed,
                    hideRepliedMessages, hideCurrentMedia, hideCenteredIcon)) {
                // 获取图标
                StatusBarIconView iconView = function.apply(ent);
                if (iconView != null) {
                    toShow.add(iconView);
                }
            }
        }
    }
    // ...
    // 把需要显示的图标添加到hostLayout中
    final FrameLayout.LayoutParams params = generateIconLayoutParams();
    for (int i = 0; i < toShow.size(); i++) {
        StatusBarIconView v = toShow.get(i);
        // 如果刚刚删除并再次添加,视图可能仍会暂时添加
        hostLayout.removeTransientView(v);
        if (v.getParent() == null) {
            if (hideDismissed) {
                v.setOnDismissListener(mUpdateStatusBarIcons);
            }
            // 执行到最后是 NotificationIconContainer.addView 添加视图
            // NotificationIconContainer本身没有addView、removeView方法,
            // 最终走的是其多层下去的父类ViewGroup的方法
            hostLayout.addView(v, i, params);
        }
    }
    // ...
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/525099.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

mysqldump: Got error: 1049: Unknown database ‘root‘ when selecting the datab

1.问题描述 MySQL版本号&#xff1a;MySQL Server 8.3MySQL持久化到处数据库结构及数据 mysqldump: Got error: 1049: Unknown database root when selecting the datab2.问题解决 cmd 切换本地路径 cd /d D:\Program Files\MySQL\MySQL Server 8.3\bin执行数据库备份命令 …

uniapp vue2 时钟 循环定时器

效果展示&#xff1a; 时钟 写在前面&#xff1a;vue2有this指向&#xff0c;没有箭头函数 实验操作&#xff1a;封装一个时钟组件 uniapp vue2 封装一个时钟组件 核心代码&#xff1a; this指向的错误代码&#xff0c;在下&#xff1a; start() { this.myTimer setInterval(…

复习知识点整理

零碎语法 1.导入某个文件夹的index文件&#xff0c;index可以省略&#xff08;这里导入的是router和store文件下的index.js文件&#xff09; 2.路由懒加载 this 1.在vue文件中使用router\store对象时 this&#xff1a;普通函数的this指向vue实例对象(在没有明确指向的时候…

大语言模型上下文窗口初探(下)

由于篇幅原因&#xff0c;本文分为上下两篇&#xff0c;上篇主要讲解上下文窗口的概念、在LLM中的重要性&#xff0c;下篇主要讲解长文本能否成为LLM的护城河、国外大厂对长文本的态度。 3、长文本是护城河吗&#xff1f; 毫无疑问&#xff0c;Kimi从一开始就用“长文本”占领…

加载infercnv报错“../JAGS/modules-4/basemod.so”

library(infercnv) Error: package or namespace load failed for ‘infercnv’:.onLoad failed in loadNamespace() for rjags, details:call: load.module("basemod", quiet TRUE)error: File not found: /opt/R/4.3.2/lib64/R/../JAGS/modules-4/basemod.so 一、…

揭开Spring Bean生命周期的神秘面纱

目录 一、Spring IOC 1.1 Spring IOC 的加载过程 二、Spring Bean 生命周期 2.1 实例化前置 2.2 实例化后置 2.3 属性赋值 2.4 初始化前置 2.5 初始化 2.6 初始化后置 2.7 Bean 销毁 Spring 是一个开源的企业级Java应用程序框架&#xff0c;它简化了企业级应用程序开…

k8s资源监控_bitnami metrics-server v0(1),2024一位Linux运维中级程序员的跳槽面经

错误3 也有可能会遇到以下错误&#xff0c;按照下面提示解决 Error from server (ServiceUnavailable): the server is currently unable to handle the request (get nodes.metrics.k8s.io) 如果metrics-server正常启动&#xff0c;没有错误&#xff0c;应该就是网络问题。修改…

基于SpringBoot的“自习室预订系统”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“自习室预订系统”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 管理员登录界面 座位预订管理界面图 自习室管理…

13 Python进阶:pip及其他常用模块

pip 是 Python 包管理工具&#xff0c;它提供了对 Python 包的查找、下载、安装、卸载的功能。 包地址&#xff1a; https://pypi.org/ 最新的 Python 版本已经预装了 pip。 pip 各种命令 查看是否已经安装 pip 可以使用以下命令&#xff1a; pip --version下载安装包使用…

Leetcode 581. 最短无序连续子数组

心路历程&#xff1a; 本以为这道题要用动态规划求解&#xff0c;因为题目中这几个关键字与动态规划太匹配了&#xff0c;结果想了半天也没发现dp(i)和dp(i-1)的递推关系。 这道题本意考察双指针的做法&#xff0c;也可以用排序后做比较的方式来做。 注意的点&#xff1a; 1…

【Redis 知识储备】冷热分离架构 -- 分布系统的演进(5)

冷热分离架构 简介出现原因架构工作原理技术案例架构优缺点 简介 引入缓存, 实行冷热分离, 将热点数据放到缓存中快速响应 (如存储到 Redis中) 出现原因 海量的请求导致数据库负载过高, 站点响应再读变慢 架构工作原理 多了缓存服务器, 对于热点数据全部到缓存中, 不常用数…

Android10系统ROM定制之Frida逆向分析实战

CSDN在线课程地址: https://edu.csdn.net/course/detail/37881 推荐阅读 2024培训课程 2024技术交流群 Android14系统安全 Android10系统ROM定制之Frida逆向分析实战

ctfshow web入门 文件包含 web151--web161

web151 打算用bp改文件形式(可能没操作好)我重新试了一下抓不到 文件上传不成功 改网页前端 鼠标右键&#xff08;检查&#xff09;&#xff0c;把png改为php访问&#xff0c;执行命令 我上传的马是<?php eval($_POST[a]);?> 查看 web152 上传马 把Content-Type改为…

在linux环境下如何进行stm32的开发?

在Linux环境下进行STM32开发确实需要一些配置和工具。我这里有一套嵌入式入门教程&#xff0c;不仅包含了详细的视频讲解&#xff0c;项目实战。如果你渴望学习嵌入式&#xff0c;不妨点个关注&#xff0c;给个评论222&#xff0c;私信22&#xff0c;我在后台发给你。 选择开发…

一起学习python——基础篇(7)

今天讲一下python的函数。 函数是什么&#xff1f;函数是一段独立的代码块&#xff0c;这块代码是为了实现一些功能&#xff0c;而这个代码块只有在被调用时才能运行。 在 Python 中&#xff0c;使用 def 关键字定义函数&#xff1a; 函数的固定结构就是 def(关键字)函数名字…

Redis单线程 VS 多线程

一、Redis 为什么选择单线程&#xff1f; 这种说法其实并不严谨&#xff0c;为什么这么说呢&#xff1f; Redis的版本有很多 3.x、4.x、6.x&#xff0c;版本不同架构也不同的&#xff0c;不限定版本问是否单线程也是不太严谨。 版本3.x&#xff0c;最早版本&#xff0c;也就…

第十二届蓝桥杯大赛软件赛省赛C/C++大学B组

第十二届蓝桥杯大赛软件赛省赛C/C 大学 B 组 文章目录 第十二届蓝桥杯大赛软件赛省赛C/C 大学 B 组1、空间2、卡片3、直线4、货物摆放5、路径6、时间显示7、砝码称重8、杨辉三角形9、双向排序10、括号序列 1、空间 1MB 1024KB 1KB 1024byte 1byte8bit // cout<<"2…

python基于opencv实现数籽粒

千粒重是一个重要的农艺性状&#xff0c;通过对其的测量和研究&#xff0c;我们可以更好地理解作物的生长状况&#xff0c;优化农业生产&#xff0c;提高作物产量和品质。但数籽粒数目是一个很繁琐和痛苦的过程&#xff0c;我们现在用一个简单的python程序来数水稻籽粒。代码的…

有限的边界-DDD领域

从广义上讲&#xff0c;领域&#xff08;Domain&#xff09;即是一个组织所做的事情以及其中所包含的一切。商业机构通常会确定一个市场&#xff0c;然后在这个市场中销售产品和服务。每个组织都有它自己的业务范围和做事方式。这个业务范围以及在其中所进行的活动便是领域。当…

Linux云计算之Linux基础3——Linux系统基础2

1、终端 终端(terminal)&#xff1a;人和系统交互的必要设备&#xff0c;人机交互最后一个界面&#xff08;包含独立的输入输出设备&#xff09; 物理终端(console)&#xff1a;直接接入本机器的键盘设备和显示器虚拟终端(tty)&#xff1a;通过软件方式虚拟实现的终端。它可以…