android APT技术

1,背景

        对于注解的使用,想必大家都不陌生,它出现在我们的源码中,以及大部分框架中,比如ButterKnifeArouterRetrofit,但它们是有区别的,其中前2个是编译时注解,最后一个是运行时注解。我们都知道用注解可以简化代码,但大多数同学对其中的原理和实现方式还是很陌生的,下面就带大家入门编译时注解。



2,结合实例讲解

以仿写ButterKnife为例,内容如下:

  • 创建自定义注解@interface
  • 创建并注册注解处理器AbstractProcesso,生成处理注解逻辑的.java文件;
  • 封装一个供外部调用的API,用的是反射技术,具体来说就是调用第二步中生成的代码中的方法;
  • 在项目中使用,比如ActivityFragmentAdapter

APT实现方案

这里按功能职责,分多个module来处理,具体如下:

  • annotation:自定义注解(java lib)
  • processor:注解处理器AbstractProcessor (java lib)
  • inject_api: 供外部调用的API (android lib)
  • app:项目使用 android lib)
步骤一:创建自定义注解

创建一个Java Library,名称为annotation,作用是保存所有注解。

步骤二:实现注解处理器AbstractProcessor

创建一个Java Library,名称为processor,作用是扫描、解析、处理注解。

processor的Gradle配置如下:

创建自己的Processor来实现注解处理器:

//通过 AutoService 将 Processor 声明到 META-INF 中
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {/*** 生成文件的工具类*/private Filer filer;/*** 打印信息*/private Messager messager;/*** 元素相关*/private Elements elementUtils;private Types typeUtils;/*** 存放被注解标记的所有变量,按类来划分*/private Map<String, ProxyInfo> proxyInfoMap = new HashMap<>();/*** 一些初始化操作,获取一些有用的系统工具类** @param processingEnv*/@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);filer = processingEnv.getFiler();messager = processingEnv.getMessager();elementUtils = processingEnv.getElementUtils();typeUtils = processingEnv.getTypeUtils();}/*** 设置支持的版本** @return 这里用最新的就好*/@Overridepublic SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();}/*** 设置支持的注解类型** @return*/@Overridepublic Set<String> getSupportedAnnotationTypes() {//添加支持的注解HashSet<String> set = new HashSet<>();set.add(BindView.class.getCanonicalName());return set;}/*** 注解内部逻辑的实现* <p>* Element代表程序的一个元素,可以是package, class, interface, method.只在编译期存在* TypeElement:变量;TypeElement:类或者接口** @param annotations* @param roundEnv* @return*/@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {messager.printMessage(Diagnostic.Kind.NOTE, "annotations size--->" + annotations.size());//1、获取要处理的注解的元素的集合Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//process()方法会调用3次,只有第一次有效,第2,3次调用的话生成.java文件会发生异常if (elements == null || elements.size() < 1) {return true;}//2、按类来划分注解元素,因为每个使用注解的类都会生成相应的代理类for (Element element : elements) {checkAnnotationValid(element, BindView.class);//获取被注解的成员变量//这里被注解的类型只能是变量,所以可以直接强转VariableElement variableElement = (VariableElement) element;//获取该元素的父元素,这里是父类TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//获取全类名String className = typeElement.getQualifiedName().toString();//获取被注解元素的包名String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();//获取注解的参数int resourceId = element.getAnnotation(BindView.class).value();//生成ProxyInfo对象//一个类里面的注解都在一个ProxyInfo中处理ProxyInfo proxyInfo = proxyInfoMap.get(className);if (proxyInfo == null) {proxyInfo = new ProxyInfo(typeElement, packageName);proxyInfoMap.put(className, proxyInfo);}proxyInfo.viewVariableElement.put(resourceId, variableElement);}//3、生成注解逻辑处理类for (String key : proxyInfoMap.keySet()) {ProxyInfo proxyInfo = proxyInfoMap.get(key);JavaFile javaFile = JavaFile.builder(proxyInfo.packageName, proxyInfo.generateProxyClass())//在文件头部添加注释.addFileComment("auto generateProxyClass code,can not modify").build();try {javaFile.writeTo(filer);} catch (IOException e) {e.printStackTrace();}}return true;}/*** 检查注解是否可用** @param annotatedElement* @param clazz* @return*/private boolean checkAnnotationValid(Element annotatedElement, Class clazz) {if (annotatedElement.getKind() != ElementKind.FIELD) {messager.printMessage(Diagnostic.Kind.NOTE, "%s must be declared on field.", annotatedElement);return false;}if (annotatedElement.getModifiers().contains(PRIVATE)) {messager.printMessage(Diagnostic.Kind.NOTE, "%s() can not be private.", annotatedElement);return false;}if (!isView(annotatedElement.asType())) {return false;}return true;}//递归判断android.view.View是不是其父类private boolean isView(TypeMirror type) {List<? extends TypeMirror> supers = typeUtils.directSupertypes(type);if (supers.size() == 0) {return false;}for (TypeMirror superType : supers) {if (superType.toString().equals("android.view.View") || isView(superType)) {return true;}}return false;}
}

下面开始这个Processor类的详解:

  • 继承AbstractProcessor,需要重写process()方法,这里是处理注解内部逻辑的,也是本文的关键点之一;

  • 此外还需要实现几个简单的方法init ()getSupportedSourceVersion()getSupportedAnnotationTypes()

  • getSupportedSourceVersion():设置支持的版本,一般用最新的就好;

  • getSupportedAnnotationTypes():添加支持的注解类型,可以是单个/多个,用Set存储;

  • init ():一些初始化操作,获取一些有用的系统工具类,比如生成文件、打印信息、处理元素等;

        

   @AutoService(Processor.class),生成 META-INF 信息;这里使用 Google 的 AutoService 来创建 META-INF,并将被注解的文件声明到 META-INF 中,这里是ButterKnifeProcessor
下面是META-INF 的文件目录:

文件内容com.zx.processor.ButterKnifeProcessor,这就是我们自定义的注解处理器ButterKnifeProcessor的路径。

接下来讲解最关键的process()方法

它的工作内容是扫描处理注解,最终生成.java文件,这个文件里面就是注解的业务逻辑。

这里主要分为3步:

  • 1、通过getElementsAnnotatedWith()获取要处理的注解的元素的集合,换句话说,找到所有Class中被@BindView注解标记的变量;
  • 2、遍历第一步中的元素集合,由于这个注解可能会在多个类中使用,所以我们以类名为单元划分注解。具体说,新建一个ProxyInfo对象去保存一个类里面的所有被注解的元素;用proxyInfoMap去保存所有的ProxyInfo;大概是这个样子Map<String, ProxyInfo> proxyInfoMap = new HashMap<>();
  • 3、在ProxyInfo中为每个使用了@BindView注解的类生成一个代理类;
  • 4、遍历proxyInfoMap,通过ProxyInfoJavaFile生成具体的代理类文件
 /*** 注解内部逻辑的实现* <p>* Element代表程序的一个元素,可以是package, class, interface, method.只在编译期存在* TypeElement:变量;TypeElement:类或者接口** @param annotations* @param roundEnv* @return*/@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {messager.printMessage(Diagnostic.Kind.NOTE, "annotations size--->" + annotations.size());//1、获取要处理的注解的元素的集合Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//process()方法会调用3次,只有第一次有效,第2,3次调用的话生成.java文件会发生异常if (elements == null || elements.size() < 1) {return true;}//2、按类来划分注解元素,因为每个使用注解的类都会生成相应的代理类for (Element element : elements) {checkAnnotationValid(element, BindView.class);//获取被注解的成员变量//这里被注解的类型只能是变量,所以可以直接强转VariableElement variableElement = (VariableElement) element;//获取该元素的父元素,这里是父类TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//获取全类名String className = typeElement.getQualifiedName().toString();//获取被注解元素的包名String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();//获取注解的参数int resourceId = element.getAnnotation(BindView.class).value();//生成ProxyInfo对象//一个类里面的注解都在一个ProxyInfo中处理ProxyInfo proxyInfo = proxyInfoMap.get(className);if (proxyInfo == null) {proxyInfo = new ProxyInfo(typeElement, packageName);proxyInfoMap.put(className, proxyInfo);}proxyInfo.viewVariableElement.put(resourceId, variableElement);}//3、生成注解逻辑处理类for (String key : proxyInfoMap.keySet()) {ProxyInfo proxyInfo = proxyInfoMap.get(key);JavaFile javaFile = JavaFile.builder(proxyInfo.packageName, proxyInfo.generateProxyClass()).addFileComment("auto generateProxyClass code,can not modify").build();try {javaFile.writeTo(filer);} catch (IOException e) {e.printStackTrace();}}return true;}

具体来说,可以按如下分类:

  • PackageElement 一般代表Package
  • TypeElement 一般代表代表类
  • VariableElement 一般代表成员变量
  • ExecutableElement 一般代表类中的方法

通过javapoet生成注解逻辑代码的具体步骤如下:

 /*** 通过javapoet API生成代理类* @return*/public TypeSpec generateProxyClass() {//代理类实现的接口ClassName viewInjector = ClassName.get("com.zx.inject_api", "IViewInjector");//原始的注解类ClassName className = ClassName.get(typeElement);//  泛型接口,implements IViewInjector<MainActivity>ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(viewInjector, className);//生成接口的实现方法inject()MethodSpec.Builder bindBuilder = MethodSpec.methodBuilder("inject").addModifiers(Modifier.PUBLIC).addAnnotation(Override.class) //添加方法注解.addParameter(className, "target").addParameter(Object.class, "source");for (int id : viewVariableElement.keySet()) {VariableElement element = viewVariableElement.get(id);String fieldName = element.getSimpleName().toString();bindBuilder.addStatement(" if (source instanceof android.app.Activity){target.$L = ((android.app.Activity) source).findViewById( $L);}" +"else{target.$L = ((android.view.View)source).findViewById($L);}", fieldName, id, fieldName, id);}MethodSpec bindMethodSpec = bindBuilder.build();//创建类TypeSpec typeSpec = TypeSpec.classBuilder(proxyClassName).addModifiers(Modifier.PUBLIC).addSuperinterface(parameterizedTypeName) //实现接口.addMethod(bindMethodSpec).build();return typeSpec;}

        这里通过javapoet的API去生成代理类的代码,大概就是TypeSpec生成类,MethodSpec生成方法。这里以MainActivity举例,具体生成处理注解的代理类如下:

package com.zx.compileannotation;import com.zx.inject_api.IViewInjector;import java.lang.Object;
import java.lang.Override;public class MainActivity_ViewBinding implements IViewInjector<MainActivity> {@Overridepublic void inject(MainActivity target, Object source) {if (source instanceof android.app.Activity) {target.textView2 = ((android.app.Activity) source).findViewById(2131230920);} else {target.textView2 = ((android.view.View) source).findViewById(2131230920);};if (source instanceof android.app.Activity) {target.recyclerView = ((android.app.Activity) source).findViewById(2131230849);} else {target.recyclerView = ((android.view.View) source).findViewById(2131230849);};if (source instanceof android.app.Activity) {target.textView = ((android.app.Activity) source).findViewById(2131230919);} else {target.textView = ((android.view.View) source).findViewById(2131230919);};}
}

注意:这里只是以MainActivity的代理类,实际上每个用到注解的类都会生成一个类似的代理类

到这一步就会为每一个使用注解的类生成对应的代理类,生成的代理类路径如下,

步骤三:封装一个供外部调用的API

我们知道在processor中已经生成了处理注解逻辑的代理类,那接下来就是调用了。首先我们要知道代理类是在编译器动态生成的,而且会有多个,所以我们只能通过反射找到这个类,然后调用它的方法

/*** 根据使用注解的类和约定的命名规则,通过反射找到动态生成的代理类(处理注解逻辑)* @param object 调用类对象*/private static IViewInjector findProxyActivity(Object object) {String proxyClassName = object.getClass().getName() + PROXY;Log.e(TAG, "findProxyActivity: "+proxyClassName);Class<?> proxyClass = null;try {proxyClass = Class.forName(proxyClassName);
//            Constructor<?> constructor = proxyClass.getConstructor(object.getClass());return (IViewInjector) proxyClass.newInstance();} catch (Exception e) {e.printStackTrace();}return null;}

我们都知道在使用findViewById()的时候分为2种情况,在Activity中可以直接使用;而在Fragment、Adapter中则是view. findViewById(),所以在下面的接口方法中多设计了一个Object参数,如果是Activity传入它本身;如果是Fragment、Adapter则传入View,

public interface IViewInjector<T> {/*** 通过source.findViewById()** @param target 泛型参数,调用类 activity、fragment等* @param source Activity、View*/void inject(T target, Object source);
}

最后提供2个供外部activity、fragment等调用的方法,

 /*** Activity调用*/public static void bind(Activity activity) {findProxyActivity(activity).inject(activity, activity);}/*** Fragment、Adapter调用** @param object* @param view*/public static void bind(Object object, View view) {findProxyActivity(object).inject(object, view);}

而运行时的入口为,

public class MainActivity extends AppCompatActivity {@BindView(R.id.tv1)TextView textView;@BindView(R.id.tv2)TextView textView2;@BindView(R.id.rv)RecyclerView recyclerView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//运行时入口ButterKnife.bind(this);textView2.setText("this is Activity  compile annotation");addFragment();initRv();}

而ButterKnife.java的代码如下,

package com.zx.inject_api;import android.app.Activity;
import android.util.Log;
import android.view.View;/*** @author 周旭* @company 伊柯夫* @e-mail 374952705@qq.com* @time 2019/11/18* @descripe*/public class ButterKnife {private static final String TAG = "ButterKnife";public static final String PROXY = "_ViewBinding";/*** Activity调用*/public static void bind(Activity activity) {findProxyActivity(activity).inject(activity, activity);}/*** Fragment、Adapter调用** @param object* @param view*/public static void bind(Object object, View view) {//先找到对应的类,再调用对应的接口,和我们平时写代码无异,只是类时编译时生成的findProxyActivity(object).inject(object, view);}/*** 根据使用注解的类和约定的命名规则,通过反射找到动态生成的代理类(处理注解逻辑)** @param object 调用类对象*/private static IViewInjector findProxyActivity(Object object) {String proxyClassName = object.getClass().getName() + PROXY;Log.e(TAG, "findProxyActivity: " + proxyClassName);Class<?> proxyClass = null;try {proxyClass = Class.forName(proxyClassName);
//            Constructor<?> constructor = proxyClass.getConstructor(object.getClass());return (IViewInjector) proxyClass.newInstance();} catch (Exception e) {e.printStackTrace();}return null;}
}

通过上面注解,调用到注解生成的文件MainActivity_ViewBinding.java中,内容如下,

// auto generateProxyClass code,can not modify
package com.zx.compileannotation;import com.zx.inject_api.IViewInjector;
import java.lang.Object;
import java.lang.Override;public class MainActivity_ViewBinding implements IViewInjector<MainActivity> {@Overridepublic void inject(MainActivity target, Object source) {if (source instanceof android.app.Activity){target.textView2 = ((android.app.Activity) source).findViewById( 2131230920);}else{target.textView2 = ((android.view.View)source).findViewById(2131230920);};if (source instanceof android.app.Activity){target.recyclerView = ((android.app.Activity) source).findViewById( 2131230849);}else{target.recyclerView = ((android.view.View)source).findViewById(2131230849);};if (source instanceof android.app.Activity){target.textView = ((android.app.Activity) source).findViewById( 2131230919);}else{target.textView = ((android.view.View)source).findViewById(2131230919);};}
}

代码就是刚process()写进入的,

    /*** 通过javapoet API生成代理类* @return*/public TypeSpec generateProxyClass() {//代理类实现的接口ClassName viewInjector = ClassName.get("com.zx.inject_api", "IViewInjector");//类
//        ClassName className = ClassName.bestGuess(simpleClassName);ClassName className = ClassName.get(typeElement);// 类型变量
//        TypeVariableName tTypeVariable = TypeVariableName.get("T");//  泛型接口,implements IViewInjector<MainActivity>ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(viewInjector, className);//生成构造方法MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameter(className, "target").addStatement("this.target = target");//生成接口的实现方法inject()MethodSpec.Builder bindBuilder = MethodSpec.methodBuilder("inject").addModifiers(Modifier.PUBLIC).addAnnotation(Override.class) //添加方法注解.addParameter(className, "target").addParameter(Object.class, "source");for (int id : viewVariableElement.keySet()) {VariableElement element = viewVariableElement.get(id);String fieldName = element.getSimpleName().toString();bindBuilder.addStatement(" if (source instanceof android.app.Activity){target.$L = ((android.app.Activity) source).findViewById( $L);}" +"else{target.$L = ((android.view.View)source).findViewById($L);}", fieldName, id, fieldName, id);}MethodSpec bindMethodSpec = bindBuilder.build();
//        MethodSpec constructorMethodSpec = constructorBuilder.build();//创建类TypeSpec typeSpec = TypeSpec.classBuilder(proxyClassName).addModifiers(Modifier.PUBLIC).addSuperinterface(parameterizedTypeName) //实现接口
//                .addTypeVariable(tTypeVariable)
//                .addMethod(constructorMethodSpec).addMethod(bindMethodSpec) //添加类中的方法
//                .addField(className, "target", Modifier.PRIVATE).build();return typeSpec;}

本质就是运行时调用编译时生成的文件,而编译生成的文件是通过 

@AutoService(Processor.class) 

的技术来实现的。

完毕

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

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

相关文章

MySQL 和 PostgreSQL综合比对分析汇总

面对大数据项目或其它类型项目中&#xff0c;面对关系型数据库选择一直是很总要的一点&#xff0c;本文针对MySQL 和 PostgreSQL进行综合比对分析汇总&#xff0c;内容仅供参考。MySQL 和 PostgreSQL 是两款主流的开源关系型数据库&#xff08;RDBMS&#xff09;&#xff0c;但…

Linux---make和makefile

一、基本概念1.是什么make是一条命令&#xff0c;makefile是一个文件2.对应在vs中按一下f5就能运行代码&#xff0c;在Linux中make就相当于f5&#xff0c;使用makefile来封装从而实现我&#xff0c; 想要的功能3.使用①创建makefile文件②编辑makefile解释&#xff1a;test.exe…

【DAB收音机】DAB收音机协议及其他资料汇总

目录[ETSI DAB标准协议文档](https://www.etsi.org/standards)Other DAB资料DAB收音机相关的专利DAB收音机相关的期刊及学位论文DAB开源项目代码仓库qt-dab工具welle.io工具dablin工具【eti广播工具】⚙️ 项目对比与选型建议Other 收音机资料Other资料ETSI DAB标准协议文档 官…

RabbitMQ的特点和消息可靠性保障

掌握RabbitMQ的核心知识&#xff0c;需从其特点和消息可靠性保障&#xff08;尤其是消息丢失解决方案&#xff09;两方面入手&#xff0c;以下是详细说明&#xff1a; 一、RabbitMQ的核心特点 RabbitMQ是基于AMQP&#xff08;Advanced Message Queuing Protocol&#xff09;协议…

项目升级啦

公司要新做一个医疗行业的业务&#xff0c;经过业务端和产品端的评估该业务与公司已有的产品线关联不大&#xff0c;用户后续也不想在老系统那台老爷车上继续使用&#xff0c;话说老系统到现在差不多10年了&#xff0c;中间经历过的前后端开发者形形色色&#xff0c;维护者换了…

Android中页面生命周期变化

一、Activity切换的生命周期变化&#xff08;A启动B&#xff09;1. 标准流程&#xff08;B完全覆盖A&#xff09;完整生命周期路径&#xff1a;Activity A&#xff1a;onPause()&#xff1a;失去焦点&#xff0c;仍部分可见onStop()&#xff1a;完全不可见&#xff08;当B完全覆…

自动驾驶控制算法——PID算法

自动驾驶控制算法——PID算法 文章目录自动驾驶控制算法——PID算法一、PID 是什么&#xff1f;二、PID 原理2.1 **比例环节&#xff08;P&#xff09;**2.2 **积分环节&#xff08;I&#xff09;**2.3 **微分环节&#xff08;D&#xff09;**2.4 特点总结2.5 案例分析 —— 小…

Spring Boot 异步执行方式全解析:@Async、CompletableFuture 与 TaskExecutor 对比

在 Spring Boot 开发中&#xff0c;异步执行是提升系统性能的重要手段&#xff0c;尤其适用于处理耗时操作&#xff08;如日志记录、邮件发送、数据同步等&#xff09;。本文将深入对比 Spring Boot 中三种主流的异步实现方式 ——Async注解、手动CompletableFuture和直接使用T…

高效微调2:Prompt-Tuning原理与实战

高效微调2:Prompt-Tuning原理与实战 Prompt-Tuning原理介绍 代码 Prompt-Tuning原理介绍 Prompt-Tuning Prompt-Tuning的思想:冻结主模型全部参数,在训练数据前加入一小段Prompt,只训练Prompt的表示层,即一个Embedding模块。其中,Prompt.又存在两种形式,一种是hard promp…

使用BART模型和T5模型实现文本改写

BART模型BART&#xff08;Bidirectional and Auto-Regressive Transformers&#xff09;是由 Facebook AI Research&#xff08;FAIR&#xff09;在 2019 年提出的序列到序列&#xff08;seq2seq&#xff09;预训练模型&#xff0c;论文发表于《BART: Denoising Sequence-to-Se…

电商前端Nginx访问日志收集分析实战

使用FileBeatLogstashES实现分布式日志收集 在大型项目中 &#xff0c;往往服务都是分布在非常多不同的机器上 &#xff0c;每个机器都会打印自己的log日志 但是 &#xff0c;这样分散的日志 &#xff0c;本来就无法进行整体分析。再加上微服务的负载均衡体系 &#xff0c;甚至…

TwinCAT3示例项目1

目录一、需求分析二、程序编写1.实现1盏灯的自控&#xff08;IF、TOF&#xff09;2. 添加模式控制&#xff08;Case、枚举&#xff09;3. 添加多盏灯&#xff08;FOR、数组&#xff09;4. 添加多组灯&#xff08;二维数组&#xff09;END项目结合了&#xff0c;FB&#xff0c;I…

如何在 VMware Workstation 虚拟机中利用 Nvidia 显卡的硬件加速功能

这篇文章详细介绍了如何在 VMware Workstation 虚拟机中利用 Nvidia 显卡的硬件加速功能&#xff0c;通过 PCI 设备直通&#xff08;Pass-Through&#xff09;技术将显卡分配给虚拟机使用&#xff1a; 在 VMware Workstation 虚拟机中利用 Nvidia 显卡的硬件加速功能 1. 检查…

设计模式(二十二)行为型:策略模式详解

设计模式&#xff08;二十二&#xff09;行为型&#xff1a;策略模式详解策略模式&#xff08;Strategy Pattern&#xff09;是 GoF 23 种设计模式中最具实用性和广泛影响力的行为型模式之一&#xff0c;其核心价值在于定义一系列算法或行为&#xff0c;并将每个算法封装到独立…

AI+向量化

要理解 Java 如何结合 AI 与向量化&#xff0c;我们需要从向量化的核心概念、AI 中向量化的作用、Java 生态中的实现工具以及具体实践案例四个维度展开。以下是详细解析&#xff1a;一、核心概念&#xff1a;向量化与 AI 的关系向量化&#xff08;Vectorization&#xff09;是将…

Bootstap Vue 之b-form-radio-group 不显示选中状态问题

代码类似&#xff1a;<b-form-radio-groupclass"mt-2"required:disabled"dfrmDisabled"v-model"childDikeForm.SafetyAppraisalRank":options"[一, 二, 三, 四]"name"rankradioopt"></b-form-radio-group>经过测…

Shell 脚本实战:基于 for 循环的批量操作三例(账户创建、网络检测与密码管理)

一、编写脚本for1.sh,使用for循环创建20账户&#xff0c;账户名前缀由用户从键盘输入&#xff0c;账户初始密码由用户输入&#xff0c;例如:test1、test2、test3、......、test10实现思路通过read命令获取用户输入的账户前缀和初始密码&#xff1b;加入非空校验&#xff1a;若前…

PBR技术

一 、PBR的概述1.定义策略路由&#xff1a; PBR 是一种覆盖路由器默认路由决策机制的技术。它允许管理员根据策略&#xff08;而不仅仅是目标地址&#xff09;来设置数据包的下一跳 IP 地址、出站接口、IP 优先级/DSCP 值等。路由策略&#xff1a;是指在路由器或三层设备上&…

STM32-ESP8266Wi-Fi模块使用USART实现通信/创建AP和STA模式配置教程(寄存器版)

本章思维导图&#xff1a;ESP8266WIFI模块简介ESP8266 是一款由乐鑫科技推出的低成本、高性能 Wi-Fi 模块&#xff0c;广泛应用于物联网和嵌入式开发领域。WIFI的频段5G和2.4G2.4G Wi-Fi与5G Wi-Fi最本质的区别即工作频段&#xff08;无线电波的频率&#xff09;不一样&#xf…

算法26. 删除有序数组中的重复项

给你一个 非严格递增排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 考虑 nums 的唯一元素的数量为 k &#xff0c;你…