Android Gradle开发、应用、插件发布(六)—实现打包自动复制文件插件

1. 前言

项目中遇到了一个问题 :

其中一个模块MyLibrary的assets文件夹中,需要存放很多文件(每个文件对应一个功能)。

这样导致的问题是MyLibrary打出的这个aar包体积特别大。

如果把MyLibrary严谨地拆解成若干个Module又比较费时,对于现在业务现状来说也显得没那么必要。

那么能不能在上传MyLibrary这个aar的时候,自动复制相应的文件到assets目录下,打出不同功能的aar呢 ?

这就需要自己开发一个Gradle插件来完成这个功能了。

本文环境

  • Android Studio 版本 : Android Studio Hedgehog | 2023.1.1
  • Gradle版本 : gradle-8.2
  • AGP版本 : 8.2.0
  • 项目结构 : 项目有app模块和MyLibrary模块,使用build.gradle (Groovy语言),app的assets目录下,有test1.so、test2.so、test3.so这三个文件

2. 配置上传Maven仓库

首先我们把MyLibrary配置上传Maven的插件,也就是maven-publish。

对于这部分功能不了解的同学可以先看我的这篇博客 : Android Module上传到Maven仓库 及 实现同时上传到多个Maven仓库

下面我们简单讲述一下

复制maven_upload.gradle到项目根目录下

apply plugin: 'maven-publish'//TODO 这里填你Maven仓库的地址
def RELEASE_REPOSITORY_URL = "https://devops-maven.xxxxx.com/repository/yyyyy/"
//TODO 这里填你Maven仓库的账号
def NEXUS_USERNAME = "*********"
//TODO 这里填你Maven仓库的密码
def NEXUS_PASSWORD = "*********"afterEvaluate {publishing {repositories {maven {name("ReleaseMaven")url = RELEASE_REPOSITORY_URLcredentials {username = NEXUS_USERNAMEpassword = NEXUS_PASSWORD}}}publications {Production(MavenPublication) {from components.releasegroupId = rootProject.ext.GROUPartifactId = rootProject.ext.POM_ARTIFACT_IDversion = rootProject.ext.VERSION_NAME}}}
}

在MyLibrary中依赖

rootProject.ext.GROUP = "com.heiko.mytest"
rootProject.ext.POM_ARTIFACT_ID = "mylibrary"
rootProject.ext.VERSION_NAME = "1.0.0"
apply from: "${project.rootDir}/maven_upload.gradle"

Sync一下,可以看到gradle中多了publishing这个文件夹,里面的publishProductionPublicationToReleaseMavenRepository就是用来将MyLibrary打包并上传到Maven仓库的Gradle命令了。

3. Gradle相关操作

3.1 复制文件

from是原目录,into是目标目录,include可以指定需要复制的文件,onlyIf可以用来判断是否执行复制任务。

task copyFiles(type: Copy) {from 'src/main/assets'into 'build/outputs/assets'//include 'test1.txt','test2.txt' //指定文件名include '**/*.txt' //根据*匹配符合要求的文件onlyIf {true}
}
3.2 删除文件
task myDeleteFilesInDir(type: Delete) {//delete 'src/main/assets/test1.txt'  //删除test1.txt//delete 'src/main/assets/test1.txt' //删除文件夹下所有的文件,assets文件也会被删除delete fileTree('src/main/assets') //删除文件夹下所有的文件,assets文件不会被删除
}

3.3 dependsOn

dependsOn表示一个任务需要另一个任务先完成,可以理解为依赖于或需要先做,这意味着在执行这个任务之前,它所依赖的任务必须首先执行。

例如,如果你有一个任务叫做compile,它需要在clean任务之后执行,就可以像这样声明依赖关系:

task clearTask {doLast {println("执行 clear.doLast")}
}task compileTask {doLast {println("执行 compile.dolast")}
}compileTask.dependsOn(clearTask)

这样,每当你运行compile任务时,Gradle会首先运行clean任务。


执行compile任务的日志如下

> Task :app:clearTask
执行 clear.doLast

> Task :app:compileTask
执行 compile.dolast

BUILD SUCCESSFUL in 510ms
 

更通俗的理解 :
dependsOn就像是做饭的顺序:你首先需要准备食材,然后才能开始烹饪。同样地,如果你有一个任务依赖于另一个任务,那么你需要在开始当前任务之前先完成那个依赖任务。如果没有这种依赖关系,那么任务可能会在错误的时机执行,导致结果不正确或者出现错误。

//准备食材任务
task prepareFood(){}//做饭任务
task cooking(){}//做饭任务 依赖于 准备食材任务
cooking.dependsOn(prepareFood)
3.4 finalizedBy

finalizedBy用于指定一个任务另一个任务完成之后执行,可以理解为在完成后执行

Gradle中,finalizedBy用于指定一个或多个任务,这些任务将在关联任务执行完毕后执行,无论关联任务是否成功。可以把这个理解为一种清理或收尾的工作。

举个例子,我们有一个任务A,它被finalizedBy任务B,那就意味着在任务A执行完之后,无论任务A是否成功,任务B都会被执行。

这就好比一个厨师在做完一道菜(任务A)之后,无论这道菜是否做得成功,他都需要清理厨房(任务B),那么清理厨房这个步骤就是做菜这个任务的finalizedBy

3.5 mustRunAfter和shouldRunAfter

Gradle中,mustRunAfter 是用来定义任务执行的顺序的。如果你有两个任务,比如说任务A任务B,你希望无论何时,只要这两个任务都被执行任务A都必须在任务B之后执行,那么你就可以在任务A上调用 mustRunAfter 方法并传入任务B

举个例子,如下代码:

taskA.mustRunAfter taskB

这段代码的意思就是,如果这两个任务都在执行队列中,那么无论何时,任务A都必须在任务B之后执行。

需要注意的是,mustRunAfter 不会强制执行任务B。如果任务B没有被加入到执行队列中,那么任务A也可以独立执行。同样,如果任务A没有被加入到执行队列中,那么任务B也可以独立执行。

这与 dependsOn 不同。dependsOn 会创建一个强制的依赖关系,也就是说,任务A依赖于任务B,那么只要任务A需要执行,任务B就必须要先执行。而 mustRunAfter 只是定义了一个任务的执行顺序,并不会创建一个依赖关系。

shouldRunAftermustRunAfter是一样的,只不过mustRunAfter如果导致了循环依赖,Gradle 会抛出一个错误,并导致构建失败。而shouldRunAfter不会抛出异常,Gradle会忽略这个顺序要求,不会导致构建失败。

4. 新建插件

4.1 创建插件类

MyLibrarybuild.gradle中,增加如下代码,创建插件类 : MyPlugin

class MyPlugin implements Plugin<Project> {@Overridevoid apply(Project project) {println "---- MyPlugin.apply ----"project.afterEvaluate { //在Gradle项目配置阶段完成后会被调用//后续代码放在这里...}}
}
4.2 应用这个插件

MyLibrarybuild.gradle中添加这行代码,表示应用这个插件

apply plugin : MyPlugin

 在Sync一下,可以看到会打印出日志,表明我们配置这个插件成功了

---- MyPlugin.apply ----

5. 实现初步的依赖

5.1 在app的assets中添加文件

appassets目录下,添加test1.sotest2.sotest3.so这三个文件

5.2 MyLibrary新建assets目录

MyLibrary下新建assets文件夹

5.3 在MyPlugin中实现复制文件的Task

注意 : 后面的这些操作,代码都放在MyPluginapply方法的project.afterEvaluate {}

def myCopyFiles = project.task("myPublishCopyTask", type: Copy) {from project.fileTree('../app/src/main/assets')include 'test1.so', 'test2.so'into 'src/main/assets'
}
myCopyFiles.group = "publishingV2"

Sync一下项目,可以在Android StudioGradle Tab中,看到publishingV2文件夹,里面有myPublishCopyTask这个Task。


我们点击这个Task,可以发现,appassets目录下的test1.sotest2.so已经被复制到MyLibraryassets目录下了

5.4 在MyPlugin中实现删除文件的Task
//原本task名叫做myPublishDeleteTask,但是后面关联了其他Task后,这个task的名称起做publishAutoCopyTask是比较合适的
def myDeleteFilesInDir = project.task("publishAutoCopyTask", type: Delete) {delete project.fileTree('src/main/assets')
}
myDeleteFilesInDir.group = "publishingV2"

Sync一下项目,可以在Android StudioGradle Tab中,看到publishingV2文件夹,里面有publishAutoCopy这个Task
我们点击publishAutoCopy这个Task,可以发现,MyLibraryassets目录下的test1.sotest2.so已经被删除了

5.5 关联Task

这里,我们调用packageReleaseAssets.mustRunAfter(myCopyFiles),表明调用packageReleaseAssets之前,必定会调用myCopyFiles,否则就会抛出异常。
然后调用myDeleteFilesInDir.dependsOn(myCopyFiles, publishTask),表示运行myDeleteFilesInDir的时候,会先去执行myCopyFilespublishTask

def packageReleaseAssets = project.tasks.findByName("packageReleaseAssets")
def publishTask = "publishProductionPublicationToReleaseMavenRepository"
packageReleaseAssets.mustRunAfter(myCopyFiles)
myDeleteFilesInDir.dependsOn(myCopyFiles, publishTask)
5.6 来看一下整体的代码

这部分代码都在MyLibrarybuild.gradle

rootProject.ext.GROUP = "com.heiko.mytest"
rootProject.ext.POM_ARTIFACT_ID = "mylibrary"
rootProject.ext.VERSION_NAME = "1.0.0"
apply from: "${project.rootDir}/maven_upload.gradle"class MyPlugin implements Plugin<Project> {@Overridevoid apply(Project project) {println "---- MyPlugin.apply ----"project.afterEvaluate {def myCopyFiles = project.task("myPublishCopyTask", type: Copy) {from project.fileTree('../app/src/main/assets')include 'test1.so', 'test2.so'into 'src/main/assets'}myCopyFiles.group = "publishingV2"//原本task名叫做myPublishDeleteTask,但是后面关联了其他Task后,这个task的名称起做publishAutoCopy是比较合适的def myDeleteFilesInDir = project.task("publishAutoCopyTask", type: Delete) {delete project.fileTree('src/main/assets')}myDeleteFilesInDir.group = "publishingV2"def packageReleaseAssets = project.tasks.findByName("packageReleaseAssets")def publishTask = "publishProductionPublicationToReleaseMavenRepository"packageReleaseAssets.mustRunAfter(myCopyFiles)myDeleteFilesInDir.dependsOn(myCopyFiles, publishTask)}}
}apply plugin: MyPlugin
5.7 调用myPublishDeleteTask

这个时候,我们在Gradle中执行myPublishDeleteTask这个任务,会发现 自动复制文件 -> 打包 -> 上传到Maven仓库 -> 删除文件这个链路,都自动执行了。

5.7.1 查看Maven仓库

我们来查看下Maven仓库,可以看到已经上传到maven仓库了
在这里插入图片描述

5.7.2 下载aar文件

下载这个版本的aar文件

接着拖到Android Studio中,就可以看到assets里已经有test1.sotest2.so

6. 实现动态配置

接着我们就可以来实现动态的配置了。
要实现动态配置,就需要在appbuild.gradle中配置名称和要复制的文件名称,像下面这样 :

apply plugin: MyPluginpublishAutoCopy {//要复制的源文件目录sourcePath = '../app/src/main/assets'//要复制的目标文件目录targetPath = 'src/main/assets'publishItem {//名称name = 'weather'//要复制的文件名sourceFiles = ['test1.so', 'test2.so']}publishItem {//名称name = 'ocr'//要复制的文件名sourceFiles = ['test3.so']}
}

 那么需要怎么将这些配置传递到MyPlugin插件中呢 ?

6.1 编写传参类
class PublishAutoCopyItem {String nameArrayList<String> sourceFiles
}class PublishAutoCopyExtension {private Project projectString targetPathString sourcePathList<PublishAutoCopyItem> items = []PublishAutoCopyItem publishItem(Closure closure) {PublishAutoCopyItem myItem = new PublishAutoCopyItem()project.configure(myItem, closure)items << myItemreturn myItem}PublishAutoCopyExtension(Project project) {this.project = project}
}
6.2 在Plugin中获取传参
class MyPlugin implements Plugin<Project> {@Overridevoid apply(Project project) {def extension = project.extensions.create("publishAutoCopy", PublishAutoCopyExtension)project.afterEvaluate {def sourcePath = extension.sourcePath ?: ""def targetPath = extension.targetPath ?: ""def items = extension.itemsprintln("sourcePath:" + sourcePath + " targetPath:" + targetPath)for (final def item in items) {println("item.name:" + item.name+" sourceFiles:"+item.sourceFiles)}//省略了之前写的代码...}}
}
6.3 在Build.gradle中配置

这个配置在MyLibrarybuild.gradle中即可

publishAutoCopy {//要复制的源文件目录sourcePath = '../app/src/main/assets'//要复制的目标文件目录targetPath = 'src/main/assets'publishItem {//名称name = 'weather'//要复制的文件名sourceFiles = ['test1.so', 'test2.so']}publishItem {//名称name = 'ocr'//要复制的文件名sourceFiles = ['test3.so']}//更多的配置可以在这里增加...
}
6.4 Sync下项目

然后我们Sync下项目,可以看到Gralde打印出了如下的日志

> Configure project :NcnnLibrary
sourcePath:../app/src/main/assets targetPath:src/main/assets
item.name:weather sourceFiles:[test1.so, test2.so]
item.name:ocr sourceFiles:[test3.so]

这样,我们就将配置传递给我们自定义的MyPlugin插件了

6.5 将参数传递给Task

将这些参数传递给Task,最终代码如下

project.afterEvaluate {def names = new ArrayList<>()def sourcePath = extension.sourcePath ?: ""def targetPath = extension.targetPath ?: ""for (final def item in extension.items) {def name = item.name ?: ""def sourceFiles = item.sourceFiles ?: [""]if (name.isEmpty()) returnnames.add(name)def nameCapitalize = name.capitalize()def myCopyFiles = project.task("myPublish${nameCapitalize}CopyTask", type: Copy) {from project.fileTree(sourcePath)include sourceFilesinto targetPath}def myDeleteFilesInDir = project.task("publish${nameCapitalize}AutoCopy", type: Delete) {delete project.fileTree(targetPath)}myDeleteFilesInDir.group = "publishingv2"def packageReleaseAssets = project.tasks.findByName("packageReleaseAssets")def publishTask = "publish${nameCapitalize}PublicationToReleaseMavenRepository"packageReleaseAssets.mustRunAfter(myCopyFiles)myDeleteFilesInDir.dependsOn(myCopyFiles, publishTask)}project.rootProject.ext["${project.name}_NAMES"] = names
}

Sync项目后,可以发现有publishOcrAutoCopypublishWeatherAutoCopy两个Task

在这里插入图片描述

6.6 修改 maven_upload.gradle

myArtifactId现在通过rootProject.ext.POM_ARTIFACT_ID进行传递,所以我们现在需要修改maven_upload.gradle

apply plugin: 'maven-publish'//TODO 这里填你Maven仓库的地址
def RELEASE_REPOSITORY_URL = "https://devops-maven.xxxxx.com/repository/yyyyy/"
//TODO 这里填你Maven仓库的账号
def NEXUS_USERNAME = "*********"
//TODO 这里填你Maven仓库的密码
def NEXUS_PASSWORD = "*********"afterEvaluate {publishing {repositories {maven {name("ReleaseMaven")url = RELEASE_REPOSITORY_URLcredentials {username = NEXUS_USERNAMEpassword = NEXUS_PASSWORD}}}publications {List<String> namesdef namesKey = "${project.name}_NAMES"if (rootProject.ext.has(namesKey)) {names = rootProject.ext[namesKey]} else {names = new ArrayList()names.add("")}for (final def itemName in names) {def publicationName = itemName.capitalize()create(publicationName, MavenPublication) {def myArtifactIdif (name.isEmpty()) myArtifactId = rootProject.ext.POM_ARTIFACT_IDelse myArtifactId = rootProject.ext.POM_ARTIFACT_ID + "-" + itemNamefrom components.releasegroupId = rootProject.ext.GROUPartifactId = myArtifactIdversion = rootProject.ext.VERSION_NAME}}}}
}
6.7 动态配置完成

到这里,我们就完成动态配置了 : 我们想要打包带某些文件的aar,就点击对应的Task进行打包就行了

  • 想打带test1.sotest2.so的包,那么就点击publishWeatherAutoCopy
  • 想打带test3.so的包,那么就点击publishOcrAutoCopy

7. 将MyPlugin移到独立的gradle文件中

我们还可以将MyPlugin相关的代码移到一个单独的gradle文件中,比如publish_auto_copy.gradle,具体方式和maven_upload.gradle类似。

这样,我们就可以在build.gradle中,使用apply from: "${project.rootProject.rootDir}/publish_auto_copy.gradle"这一句代码,就应用MyPlugin插件了。接着,在build.gradle中配置好MyPlugin的参数就行了。

apply from: "${project.rootProject.rootDir}/publish_auto_copy.gradle"publishAutoCopy {//要复制的源文件目录sourcePath = '../app/src/main/assets'//要复制的目标文件目录targetPath = 'src/main/assets'publishItem {//名称name = 'weather'//要复制的文件名sourceFiles = ['test1.so', 'test2.so']}publishItem {//名称name = 'ocr'//要复制的文件名sourceFiles = ['test3.so']}//更多的配置可以在这里增加...
}

至此,我们就完成了打包自动复制文件插件的全部功能了。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue3实战笔记(42)—Vue + ECharts:流量数据可视化的强大组合

文章目录 前言vue3使用echarts标准demo&#xff1a;总结 前言 在前端开发中&#xff0c;数据可视化已经成为了一个不可或缺的部分。Vue.js作为一个轻量级且易于上手的渐进式JavaScript框架&#xff0c;与ECharts这个强大的数据可视化库的结合&#xff0c;使得在Vue应用中构建交…

Mysql插入中文内容报错解决及其Mysql常用的存储引擎说明

一、问题描述 我们在Mysql数据库的表中插入带有中文内容时报错,提示【1366 - Incorrect string value: \xE5\x8C\x97\xE4\xBA\xAC... for column UserDealer at row 1】,如下图所示: 二、问题分析 一般来说插入中文内容有问题我们首先想到的就是编码问题;我们可以查看该表使…

文科论文,使用AI写作时能够提供实证数据吗?

人工智能时代&#xff0c;为了撰写论文提供思路及高效&#xff0c;利用AI撰写论文已是常态&#xff0c;可撰写文科论文通常研究中都需要实证数据&#xff0c;而AI撰写论文时能够提供这样的数据吗&#xff1f; 一、什么是实证数据 实证数据是指从研究报告、财务报表、新闻报道…

栈和队列的经典例题,LeetCode 括号匹配问题;栈实现队列;队列实现栈;队列带环问题

1.前序 又有很久没有更新文章了&#xff0c;这次带你们手撕几道基础题&#xff1b;真的就和康纳吃饭一样简单&#xff01;&#xff01;&#xff01; 如果还不会队列和栈的可以去看看之前写的博客&#xff1b; 栈的实现 队列概念以及实现 <- 快速传送 目录 1.前序 …

Jmeter例题分析-作业一

作业 作业1概要 本文档是关于执行软件性能测试的详细指南&#xff0c;包括使用JMeter工具进行测试的步骤和要求。 文档分为两个主要部分&#xff1a;性能测试的执行和性能测试报告的编写。 在第一部分中&#xff0c;详细描述了如何使用 JMeter进行性能测试。这包括设置测试环…

【机器学习】大模型在机器学习中的应用:从深度学习到生成式人工智能的演进

&#x1f512;文章目录&#xff1a; &#x1f4a5;1.引言 ☔2.大模型概述 &#x1f6b2;3.大模型在深度学习中的应用 &#x1f6f4;4.大模型在生成式人工智能中的应用 &#x1f44a;5.大模型的挑战与未来展望 &#x1f4a5;1.引言 随着数据量的爆炸性增长和计算能力的提…

LeetCode //C - 119. Pascal‘s Triangle II

119. Pascal’s Triangle II Given an integer rowIndex, return the rowIndexth (0-indexed) row of the Pascal’s triangle. In Pascal’s triangle, each number is the sum of the two numbers directly above it as shown: Example 1: Input: rowIndex 3 Output: …

Autodesk 3DS Max v2025 解锁版安装教程 (3D 建模软件)

前言 Autodesk 3ds Max 是一款功能强大的 3D 建模和动画解决方案&#xff0c;游戏开发人员、视觉效果艺术家和平面设计师使用它来创建庞大的世界、令人惊叹的场景和引人入胜的虚拟现实 (VR) 体验。 Autodesk 3DS MAX是业界使用最广泛的3D建模和动画软件程序之一&#xff0c;它…

6.小程序页面布局 - 账单明细

文章目录 1. 6.小程序页面布局 - 账单明细1.1. 竞品1.2. 布局分析1.3. 布局demo1.4. 页面实现-头部1.5. 账单明细1.5.1. 账单明细-竞品分析1.5.2. 账单明细-实现1.5.2.1. 账单明细-实现-mock数据1.5.2.2. 每日收支数据的聚合整理1.5.2.3. 页面scroll-view 1.6. TODO 1. 6.小程序…

力扣HOT100 - 72. 编辑距离

解题思路&#xff1a; 动态规划 class Solution {public int minDistance(String word1, String word2) {int n1 word1.length();int n2 word2.length();int[][] dp new int[n1 1][n2 1];for (int j 1; j < n2; j) dp[0][j] dp[0][j - 1] 1;for (int i 1; i < …

OA界面这么香吗?总有老铁私信,让我多发点,他好参考。

OA的确是B端系统应用最为广泛的一种&#xff0c;这次再给大家分享十来个页面&#xff0c;希望对他们的界面提升有所帮助。 举报 评论 3

【数据结构与算法】之堆的应用——堆排序及Top_K问题!

目录 1、堆排序 2、Top_K问题 3、完结散花 个人主页&#xff1a;秋风起&#xff0c;再归来~ 数据结构与算法 个人格言&#xff1a;悟已往之不谏&#xff0c;知来者犹可追 克心守己&#xff0c;律己则安&#xff01; 1、堆排序 对一个无序的数组…

焦化超低排平台选哪家好?(已解答)

在环保政策日益严格的背景下&#xff0c;焦化行业的超低排放改造成为企业转型升级的必经之路。朗观视觉小编建议&#xff0c;选择合适的焦化超低排平台对于确保改造效果和实现可持续发展具有重要意义。本文将从多个维度为您提供一份全面的评估与选择指南&#xff0c;帮助您在众…

【计算机毕业设计】030英语学习交流平台微信小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

Volatile的内存语义

1、volatile的特性 可见性&#xff1a;对一个volatile变量的读&#xff0c;总能够看到任意一个线程对这个volatile变量的写入。 原子性&#xff1a;对任意单个volatile变量的读/写具有原子性&#xff0c;但类似于volatile这种复合操作不具有原子性。 接下来我们用程序验证。…

Spring Cloud 系列之Gateway:(9)初识网关

传送门 Spring Cloud Alibaba系列之nacos&#xff1a;(1)安装 Spring Cloud Alibaba系列之nacos&#xff1a;(2)单机模式支持mysql Spring Cloud Alibaba系列之nacos&#xff1a;(3)服务注册发现 Spring Cloud 系列之OpenFeign&#xff1a;(4)集成OpenFeign Spring Cloud …

【2024软考】史上最全!软考刷题+解析大合集(9万字全手工打,货真价实)

计算机基础知识 1.中断向量表用来保存各个中断源的中断服务程序的入口地址。当外设发出中断请求信号&#xff08;INTR&#xff09;以后&#xff0c;由中断控制器&#xff08;INTC&#xff09;确定其中断号&#xff0c;并根据中断号查找中断向量表来取得其中断服务程序的入口地…

89.网络游戏逆向分析与漏洞攻防-游戏技能系统分析-游戏中使用的哈希算法逆向分析

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果&#xff0c;代码看不懂是正常的&#xff0c;只要会抄就行&#xff0c;抄着抄着就能懂了 内容…

HMI设计:再谈上位机与下位机,附海量案例图

上期回顾&#xff1a;HMI界面之&#xff1a;上位机界面设计&#xff0c;一文扫盲 一、上位机负责控制和决策&#xff0c;下位机负责采集和执行 上位机和下位机是两个概念&#xff0c;通常用于描述计算机系统中不同层次的设备或组件。 上位机&#xff08;Host Computer&#x…

kubernetes之prometheus kube-controller-manager。 scheduler报错问题

项目场景&#xff1a; prometheus scheduler及kube-controller-manager监控报错 问题描述 kubeadm搭建完kube-prometheus 会有这个报错 原因分析&#xff1a; rootmaster2:~# kubectl describe servicemonitor -n kube-system kube-controller-manager通过以上图片我们发现 k…