Android开发中的服务发现技术

news2024/12/25 16:21:42

我们的日常开发中充满了Interface+Registry这种模式的代码,其中:

  • Interface为定义的服务接口,可能是业务功能服务也可能是日志服务、数据解析服务、特定功能引擎等各种抽象层(abstract layer);
  • Registry为特定服务注册中心(或管理中心Manager),通常是一个单例形式用于管理对应的服务进行统一调度;

其常规开发流程如下:
在这里插入图片描述

这种常规模式存在两个可改进点:

  1. 服务的数量
    在服务种类和实现的数量不多的情况下问题影响不大,但随着业务发展,会伴生越来越多的服务。极端地设想我们现在有50个服务,每个服务有10个实现类,意味着我们不进要需要对应的50个Registry类,还需要手动new出50×10个服务实例,并添加到注册中心。

  2. 注册中心与服务实现耦合
    从上面开发流程可以看到,在调用服务之前有一个手动初始化服务实例和注册的过程,意味着注册中心是要显式引用每个服务实现类的,当服务实现类位于不同的module下时意味着要显式地依赖这些module,造成依赖复杂性增大和灵活性下降。

针对上述问题,一种更为解耦和可扩展的实现形式为:
在这里插入图片描述
可以看到在这种模式下,针对第一个问题省去了手动初始化实例和注册的工作,针对第二个问题通过一个全局服务注册中心省去了创建特定服务注册中心的工作,此外最大的一个收益是解耦了接口与实现之间以及模块之间的依赖使我们可以从全局的视角去处理和调度服务。由此引申出了相关的实现技术。

技术方案

可以根据是否使用反射把这类技术分为有反射和无反射两大类(或动态和静态):

  • 反射技术:通过某种方式获取对应服务的class对象,实例化并注册到全局注册中心。
    • 优点:可动态加载
    • 缺点:性能比静态差,混淆时需要添加keep规则
  • 无反射技术:通过编译期修改代码的方式实例化服务并注册到全局注册中心。
    • 优点:性能好
    • 缺点:无法动态加载,编译隔离场景下引入额外的复杂度

反射技术

通过某种方式获取对应服务的class对象,然后初始化实例进行服务注册等相关操作。比较典型的如Java的SPI机制和Android上特有的dex遍历查找。

SPI机制

SPI(Service Provider Interface)是Java自带的一种服务发现机制,其核心类是ServiceLoader,使用也比较简单:

  1. 定义服务类,可以是接口或抽象类;
  2. 实现服务接口或抽象类;
  3. META-INF/services下创建以服务类的完整类名为名的配置文件,文件需utf-8编码,内容为第2步中实现类的完整类名(每行一个);
  4. 创建ServiceLoader获取服务实例;
private static final Map<Class<?>, ServiceLoader<?>> sLoaders = new ConcurrentHashMap<>();

public static <T> List<T> getServiceBySPI(Class<T> clz){
    List<T> services = new ArrayList<>();
    ServiceLoader<T> loader = (ServiceLoader<T>) sLoaders.get(clz);
    if (loader == null) {
        loader = ServiceLoader.load(clz, clz.getClassLoader());
        if (loader == null) {
            throw new NullPointerException();
        }
        sLoaders.put(clz, loader);
    }
    for (T service : loader) {
        Log.d(TAG, "getServiceBySPI: ==> " + service.getClass().getName());
        services.add(service);
    }
    return Collections.unmodifiableList(services);
}

SPI的核心加载流程如下:
在这里插入图片描述

特点总结如下:

优点缺点
1. 原生支持,使用简单;
2. 加载时间与包体大小没有线性正相关关系;
3. 支持模块化开发(jdk编译器会自动merge多模块配置文件);
4. 支持服务懒加载和provider缓存,在迭代器中实例化服务对象;
1. 没有针对Android平台进行专门性能优化;
2. 使用了反射实例化,需要对服务接口及其实现类进行混淆keep;

Dex扫描

在安卓中class被优化为dex文件以提高其在虚拟机中的执行效率,dex扫描顾名思义就是把应用中所有的dex文件都找到,然后加载这些dex并逐个遍历其中的文件找出符合条件的class文件。在使用流程与SPI也是大同小异:

  1. 定义服务类,可以是接口或抽象类;
  2. 实现服务接口或抽象类;
  3. 使用ClassUtils找到符合规则的class全类名;
  4. 初始化Class并实例化;
public static <T> List<T> getServiceByDex(Class<T> c){
    List<T> services = new ArrayList<>();
    try {
        // 示例中使用报名作为全类名过滤条件,实际开发时可以灵活定制
        for (String s : ClassUtils.getFileNameByPackageName(Utils.getApp(), c.getPackage().getName())) {
            if (s.contains("$")) continue;
            try {
                Class<?> clz = Class.forName(s);
                if (clz == c) continue;
                if (!c.isAssignableFrom(clz)) continue;
                Log.d(TAG, "getServiceByDex: ==> " + clz.getName());
                services.add((T) clz.newInstance());
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return Collections.unmodifiableList(services);
}

DEX扫描的核心加载流程如下:
在这里插入图片描述
特点总结如下:

优点缺点
1. 动态特性,使用简单;1. 不支持Apply Changes等hot load技术,导致新增服务实现类时需要卸载重新安装apk才能被检测到;
2. 性能不如SPI,并且加载时间与包体增大而劣化;

其他方案

从上面两个方案的对比中不难发现反射方案的核心和区别点在于服务实现类全名的搜集方式不同:

  • SPI是借助resource资源文件和Java的resource相关API;
  • DEX扫描则是采用运行期全局dex文件扫描的方式

两者的IO操作工作量相差巨大,我们不难推测基于SPI的方案性能要犹豫DEX扫描,而实验数据也可以进一步验证这个结论:
实验条件:

  • 包体大小:apk:4.2MB,dex:3.7MB
  • 服务1:该服务有3个实现,分别位于两个不同的module中
public interface IService {
    String doJob();
}
  • 服务2:该服务有2个实现,分别位于两个不同的module中
public abstract class AbsService {
    protected abstract void doJob();
}

实验结果:

测试caseSPIDEX扫描
加载3个服务耗时3ms17ms
加载2个服务耗时1ms13ms

实验数据显示,基于配置文件的SPI方案比基于运行期dex扫描的方案快了近一个数量级,可见服务列表的搜集方案对整体性能影响的占比是占了很大比例的。

有没有更好的搜集服务实现类的方法呢?这是一个开放性的问题,有一些比较典型的实践供我们参考:

apt经典案例之Glide

Glide作为谷歌官方维护的图片加载框架,除了其轻量级、功能多、性能强等优势外,一些代码设计的思想也非常值得我们借鉴。这里从服务发现的角度看看它是如何实现的。

Glide对用户提供了全局配置服务GlideModule,具体分为LibraryGlideModuleAppGlideModule,前者用于组件后者用于宿主。

  • LibraryGlideModule示例
    我们在library模块中创建如下的LibraryGlideModule
@GlideModule
public class GModule1 extends LibraryGlideModule {
    private static final String TAG = "GModule1";

    @Override
    public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
        Log.d(TAG, "registerComponents: ");
    }
}

然后make以下module,Glide的compiler将为我们生成如下的代码:

package com.bumptech.glide.annotation.compiler;

@Index(
    modules = "<package_name>.GModule1"
)
public class GlideIndexer_GlideModule_<package_name>_GModule1 {
}
  • AppGlideModule示例
    我们在application模块中创建如下的AppGlideModule
@GlideModule
public class GApp extends AppGlideModule {
    private static final String TAG = "GApp";
    @Override
    public boolean isManifestParsingEnabled() {
        return super.isManifestParsingEnabled();
    }

    @Override
    public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
        super.applyOptions(context, builder);
        Log.d(TAG, "applyOptions: ");
    }
}

同样make以下module,Glide的compiler将为我们生成如下的代码:

out
    ├── com.bumptech.glide    // 固定包名
    │           ├── GeneratedAppGlideModuleImpl.java
    │           └── GeneratedRequestManagerFactory.java
    └── <package_name>        // 用户包名
                ├── GlideApp.java
                ├── GlideOptions.java
                ├── GlideRequest.java
                └── GlideRequests.java

文件比较多,我们主要关注GeneratedAppGlideModuleImpl.java

package com.bumptech.glide;

@SuppressWarnings("deprecation")
final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule {
  private final GApp appGlideModule;

  GeneratedAppGlideModuleImpl() {
    // 获取到AppGlideModule服务实现类并实例化
    appGlideModule = new GApp();
    ...
  }

  @Override
  public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
    // 执行AppGlideModule实例的回调,使用户配置生效
    appGlideModule.applyOptions(context, builder);
  }

  @Override
  public void registerComponents(@NonNull Context context, @NonNull Glide glide,
      @NonNull Registry registry) {
    // 执行AppGlideModule实例的回调,使用户配置生效
    // 注意:先执行library的回调再执行application的回调,把最终决定权交给宿主!!!    
    new GModule1().registerComponents(context, glide, registry);
    appGlideModule.registerComponents(context, glide, registry);
  }

  @Override
  public boolean isManifestParsingEnabled() {
    return appGlideModule.isManifestParsingEnabled();
  }

  @Override
  @NonNull
  public Set<Class<?>> getExcludedModuleClasses() {
    ...
  }

  @Override
  @NonNull
  GeneratedRequestManagerFactory getRequestManagerFactory() {
    ...
  }
}


那么Glide是如何解决上述代码的调用问题的呢?由于是配置服务,所以肯定是在Glide初始化时调用,找到对应的代码:

private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder) {
  Context applicationContext = context.getApplicationContext();
  // 实例化GeneratedAppGlideModule
  GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules();
  // 获取清单文件中的配置
  List<com.bumptech.glide.module.GlideModule> manifestModules = Collections.emptyList();
  if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) {
    manifestModules = new ManifestParser(applicationContext).parse();
  }
  // 执行配置排除项
  if (annotationGeneratedModule != null
      && !annotationGeneratedModule.getExcludedModuleClasses().isEmpty()) {
    Set<Class<?>> excludedModuleClasses =
        annotationGeneratedModule.getExcludedModuleClasses();
    Iterator<com.bumptech.glide.module.GlideModule> iterator = manifestModules.iterator();
    while (iterator.hasNext()) {
      com.bumptech.glide.module.GlideModule current = iterator.next();
      if (!excludedModuleClasses.contains(current.getClass())) {
        continue;
      }
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "AppGlideModule excludes manifest GlideModule: " + current);
      }
      iterator.remove();
    }
  }
  // 打印保留的清单文件配置
  if (Log.isLoggable(TAG, Log.DEBUG)) {
    for (com.bumptech.glide.module.GlideModule glideModule : manifestModules) {
      Log.d(TAG, "Discovered GlideModule from manifest: " + glideModule.getClass());
    }
  }
  // 执行其他配置回调
  RequestManagerRetriever.RequestManagerFactory factory =
      annotationGeneratedModule != null
          ? annotationGeneratedModule.getRequestManagerFactory() : null;
  builder.setRequestManagerFactory(factory);
  for (com.bumptech.glide.module.GlideModule module : manifestModules) {
    module.applyOptions(applicationContext, builder);
  }
  if (annotationGeneratedModule != null) {
    annotationGeneratedModule.applyOptions(applicationContext, builder);
  }
  Glide glide = builder.build(applicationContext);
  for (com.bumptech.glide.module.GlideModule module : manifestModules) {
    module.registerComponents(applicationContext, glide, glide.registry);
  }
  if (annotationGeneratedModule != null) {
    annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry);
  }
  applicationContext.registerComponentCallbacks(glide);
  Glide.glide = glide;
}

再看下GeneratedAppGlideModule是如何被实例化的:

private static GeneratedAppGlideModule getAnnotationGeneratedGlideModules() {
  GeneratedAppGlideModule result = null;
  try {
    // 简单的反射即可,因为GeneratedAppGlideModuleImpl是固定包名的
    Class<GeneratedAppGlideModule> clazz =
        (Class<GeneratedAppGlideModule>)
            Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl");
    result = clazz.getDeclaredConstructor().newInstance();
  } catch (ClassNotFoundException e) {
    if (Log.isLoggable(TAG, Log.WARN)) {
      Log.w(TAG, "Failed to find GeneratedAppGlideModule. You should include an"
          + " annotationProcessor compile dependency on com.github.bumptech.glide:compiler"
          + " in your application and a @GlideModule annotated AppGlideModule implementation or"
          + " LibraryGlideModules will be silently ignored");
    }
  // These exceptions can't be squashed across all versions of Android.
  } catch (InstantiationException e) {
    throwIncorrectGlideModule(e);
  } catch (IllegalAccessException e) {
    throwIncorrectGlideModule(e);
  } catch (NoSuchMethodException e) {
    throwIncorrectGlideModule(e);
  } catch (InvocationTargetException e) {
    throwIncorrectGlideModule(e);
  }
  return result;
}

通过Glide的实现思路我们得到一些启发:

  1. 用annotation+apt做服务发现;
  2. 用固定包名(即协议)+反射做服务实例化,实现初始化;

由于服务发现是在编译期由apt实现的,只是用反射做了实例化的工作,严格上应该属于静态代码范畴,但这里是按是否使用反射进行分类。

无反射技术

通过编译期发现服务并生成服务初始化和注册的代码片段插入相关预留入口。

字节码技术图谱

这些技术由于是在编译期生成了代码或修改代码,是静态的,相比动态实现方式具有天生的性能优势,同时也失去了动态的灵活性优势。适用于编译完成后服务的种类和数量都已确定的场景。

Annotation Processing

在Java生态中提供了apt工具可以帮助我们在运行期获取到源码相关元数据,从进行一些编译前预操作,比如常见的代码生成、配置文件生成等,通常的步骤为:

  1. 为每种特定目的定义annotation,用于在源码中打标记和提供参数;
  2. 自定义AbstractProcessor来处理这些annotation标记执行对应的逻辑;
public class TestAnnotationProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        AptUtil.init(processingEnv);
        AptUtil.log("Options = "+processingEnv.getOptions());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<TypeElement> typeElements = ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Router.class));
        if (typeElements.size()>0) {
            // 生成代码或其他处理逻辑
        }
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(Router.class.getName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

}
  1. META-INF/services下创建对应的配置文件以使AbstractProcessor可以被JDK识别;
  2. 在gradle中使用annotationProcessor等引入以使之生效;

通常APT都会配合一些代码生成工具使用,常见的如javapoet、kotlinpoet等。这个组合可以配合注解生成新的代码,但是不能修改已有的java代码,同时为了实现自动初始化等逻辑,还是需要根据生成代码的类名规则使用反射(如Glide的GlideModule)或者借助用户的手进行调用(如Glide的GlideApp、ButterKnife等)。

字节码插桩

字节码插装是指在javac生成class之后,转换成dex期间对class文件进行修改的一种时兴的技术。由于在这个阶段已经完成了编译的工作,已经不存在编译检查之类的环节,相比APT相关技术我们具有更高的修改自由度和广度,可以修改任何jar包中的文件。

目前比较主流的可实现字节码插装的技术分为两个流派:Aspectj和Transform。

Aspectj

通过hook gradle的javaCompile这个task,在编译完成后对class文件进行二次处理:

class AspectjPlugin implements Plugin<Project>{
    @Override
    void apply(Project project) {

        project.dependencies {
            api 'org.aspectj:aspectjrt:1.9.2'
        }

        def variants
        try {
            variants = project.android.libraryVariants
        } catch (Exception ignore) {
            variants = project.android.applicationVariants
        }

        variants.all { variant->
            def javaCompile = variant.javaCompile
            javaCompile.doLast{
                String[] args = ["-showWeaveInfo",
                                 "-1.8",
                                 "-inpath", javaCompile.destinationDir.toString(),
                                 "-aspectpath", javaCompile.classpath.asPath,
                                 "-d", javaCompile.destinationDir.toString(),
                                 "-classpath", javaCompile.classpath.asPath,
                                 "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
                project.logger.debug "ajc args: " + Arrays.toString(args)

                MessageHandler handler = new MessageHandler(true);
                new Main().run(args, handler);
            }
        }
    }
}

在使用方便,基于AOP思想,分别定义切点和切面然后进行想要的修改即可。

Transform

Transform是Android Gradle V1.5.0 版本以后提供的API,用于在class文件被转化为dex文件之前去修改字节码以实现插桩需求。Transform最终会在AGP中被转化成对应的TransformTask,被TaskManager管理。

自定义Transform需要依赖implementation 'com.android.tools.build:gradle:1.5.0+'

注册Transform:

class TestTransformPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {

        project.android.registerTransform(new CustomTransform(project))
        ...
    }
}

Transform的5个重写方法:

class TransformerTransform extends Transform {

    private final Project mProject

    TransformerTransform(Project project) {
        mProject = project
    }

    @Override
    String getName() {
        /*
         * 1. 会出现在 app/build/intermediates/transforms 目录下;
         * 2. 在gradle tasks下面也能找到;
         */
        return "${Your.Custom.Name}Transformer"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        /* 指定输入的类型:
        通过这里设定,可以指定我们要处理的文件类型,这样确保其他类型的文件不会传入 */
        /**
         * {@link TransformManager#CONTENT_CLASS}:表示需要处理 java 的 class 文件
         * {@link TransformManager#CONTENT_JARS}:表示需要处理 java 的 class 与 资源文件
         * {@link TransformManager#CONTENT_RESOURCES}:表示需要处理 java 的资源文件
         * {@link TransformManager#CONTENT_NATIVE_LIBS}:表示需要处理 native 库的代码。
         * {@link TransformManager#CONTENT_DEX}:表示需要处理 DEX 文件。
         * {@link TransformManager#CONTENT_DEX_WITH_RESOURCES}:表示需要处理 DEX 与 java 的资源文件。
         * */
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        /* 指定Transfrom的作用范围: */
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        /*
        如果返回 true,TransformInput 会包含一份修改的文件列表;
        如果返回 false,则会进行全量编译,并且会删除上一次的输出内容;
        */
        return false
    }


    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        /* 必须要重写该方法,哪怕什么都不做,都需要把上一个transform的输入完整地传递给下一个transform,否则apk将会是空的 */
        // Transform的inputs有两种类型:
        // 1.directoryInputs:目录,源码以及R.class、BuildConfig.class以及R$XXX.class等
        // 2.jarInputs:jar包,一般是第三方依赖库jar文件
        ...
    }
}

重点是transform方法 ,在这个方法中去修改class文件注入我们的逻辑,通常有两种方式:Javassist和ASM,其中Javassist更接近java编码习惯,因此比较容易入门,ASM则比较复杂,需要了解相关的语法规则,可以借助类似Bytecode Outline这类插件辅助进行编码。

目前已经有一些现成的基于transform+ASM的服务发现方案,比如BlankJ的utilcode和字节内部的Claymore。

服务发现技术的应用

IDEA插件

插件框架赋予了IDE更多功能,使得三方开发者可以为IDE开发很多拓展功能,从而丰富应用生态。

这些插件位于:/Applications/Android Studio.app/Contents/plugins,那么IDE是如何知道这些插件并运行它们呢?其实每个插件都有自己的清单文件plugin.xml,其内容大致如下:

<idea-plugin>
    <id>io.github.qzcsfchh.idea.plugin.demo</id>
    <name>PluginDemo</name>
    <version>1.0</version>
    <vendor email="" url="">io.github.qzcsfchh</vendor>

    <description><![CDATA[
      My test Idea plugin, demonstrates how to create a plugin.<br>
      <em>Test only.</em>
    ]]></description>

    <change-notes><![CDATA[
      Initial release of the plugin.
    ]]>
    </change-notes>

    <!-- please see https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html for description -->
    <idea-version since-build="173.0"/>

    <!-- please see https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html
         on how to target different products -->
    <depends>com.intellij.modules.platform</depends>

    <extensions defaultExtensionNs="com.intellij">
        <!-- Add your extensions here -->
    </extensions>

    <actions>
        <!-- Add your actions here -->
        <!--        <group id="PluginDemo.MyGroup" text="MyGroup" description="Test my idea plugin">-->
        <!--            <add-to-group group-id="MainMenu" anchor="last"/>-->
        <!--            <action class="io.github.qzcsfchh.idea.plugin.demo.TextBoxed"-->
        <!--                    id="io.github.qzcsfchh.idea.plugin.demo.TextBoxed"-->
        <!--                    text="Input Dialog"-->
        <!--                    description="Show input dialog">-->
        <!--            </action>-->

        <!--            <action id="io.github.qzcsfchh.idea.plugin.demo.action.dialog.InputDialog"-->
        <!--                    class="io.github.qzcsfchh.idea.plugin.demo.action.dialog.InputDialog" text="InputDialog"-->
        <!--                    description="Show input dialog">-->
        <!--            </action>-->
        <!--        </group>-->


        <action id="io.github.qzcsfchh.idea.plugin.demo.action.dialog.InputDialog"
                class="io.github.qzcsfchh.idea.plugin.demo.action.dialog.InputDialog"
                text="InputDialog"
                description="Test input dialog">
            <add-to-group group-id="ToolsMenu" anchor="last"/>
        </action>
        <action id="NotificationTest"
                class="io.github.qzcsfchh.idea.plugin.demo.action.notification.NotificationTest"
                text="NotificationTest"
                description="Test notification">
            <add-to-group group-id="ToolsMenu" anchor="last"/>
        </action>
    </actions>

</idea-plugin>

IDE只需要在启动时先扫描插件目录下的jar包,加载到classloader中,读取清单文件,最终在需要使用的时候进行反射实例化。

可执行jar包

jar包分为可执行和库文件两种,所谓可执行jar包是指可以通过java -jar xxx.jar来运行的jar包。那么Java如何区分这两种类型的jar包呢?同样还是清单文件,可执行jar需要在打包时在META-INF/MANIFEST.MF中指定Main-Class

Manifest-Version: 1.0
Main-Class: com.github.thinwonton.jarstarter.JarStarterMain
Class-Path: ./lib/commons-lang3-3.5.jar

java程序在启动时加载指定的jar包到classloader,读取元清单文件Main-Class,反射实例化类对象并最终调用main方法。

扩展阅读

  • 字节码技术在Android平台的应用

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

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

相关文章

线性表→顺序表→链表 逐个击破

一. 线性表 1. 前言 线性表&#xff0c;全名为线性存储结构。使用线性表存储数据的方式可以这样理解&#xff0c;即 “ 把所有(一对一逻辑关系的)数据用一根线儿串起来&#xff0c;再存储到物理空间中 ”。这根线有两种串联形式&#xff0c;如下图&#xff0c;即顺序存储(集中…

【收藏级】MySQL基本操作的所有内容(常看常新)

文章目录前言一、ER模型二、数据类型三、字段命名规范四、数据库创建与管理4.1、创建数据库4.2、删除数据库4.3、列出数据库4.4、备份数据库4.5、还原数据库4.6、使用某个数据库五、数据表创建与管理5.1、创建表、结构5.2、查看表结构5.3、查看数据表5.4、复制表结构5.5、复制表…

m基于PSO粒子群算法的重采样算法仿真,对比随机重采样,多项式重采样,分层重采样,系统重采样,残差重采样,MSV重采样

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 重采样的主要方法有随机重采样,多项式重采样,分层重采样,系统重采样,残差重采样,MSV重采样等。 a.随机采样是一种利用分层统计思想设计出来的&#xff0c;将空间均匀划分&#xff0c;粒子打点后…

Lecture6:激活函数、权值初始化、数据预处理、批量归一化、超参数选择

目录 1.最小梯度下降&#xff08;Mini-batch SGD&#xff09; 2.激活函数 2.1 sigmoid 2.2 tanh 2.3 ReLU 2.4 Leaky ReLU 2.5 ELU 2.6 最大输出神经元 2.7 建议 3.数据预处理 4. 如何初始化网络的权值 5. 批量归一化 6.超参数的选择 1.最小梯度下降&#xf…

Flowable定时器与实时流程图

1. 定时器 1.1. 流程定义定时激活 在之前松哥给小伙伴们介绍流程定义的时候&#xff0c;流程都是定义好之后立马就激活了&#xff0c;其实在流程定义的这个过程中&#xff0c;我们还可以设置一个激活时间&#xff0c;也就是流程定义好之后&#xff0c;并不会立马激活&#xf…

Java一些面试题(简单向)

以下全部简单化回答(本人新手,很多都是直接百度粘贴收集得来的,如有不对请留下正确答案,谢谢) (问题来源https://www.bilibili.com/video/BV1XL4y1t7LL/?spm_id_from333.337.search-card.all.click&vd_source3cf72bb393b8cc11b96c6d4bfbcbd890) 1.重写 重载的区别 重写(ov…

dubbo3.0使用

dubbo3.0使用 介绍 官方网址&#xff1a;https://dubbo.apache.org/ 本文基于springCloud依赖的方式演示相关示例&#xff1a;https://github.com/alibaba/spring-cloud-alibaba/wiki/Dubbo-Spring-Cloud dubbo示例项目&#xff1a;https://github.com/apache/dubbo-sample…

9 内中断

内中断 任何一个通用的CPU&#xff0c;比如8086 &#xff0c;都具备一种能力&#xff0c;可以在执行完当前正在执行的指令之后&#xff0c;检测到从CPU 外部发送过来的或内部产生的一种特殊信息&#xff0c;并且可以立即对所接收到的信息进行处理。这种特殊的信息&#xff0c;…

S7-200SMART高速脉冲输出的使用方法和示例

S7-200SMART高速脉冲输出的使用方法和示例 S7-200SMART PLC内部集成了高速脉冲发生器,不同的CPU型号,高速脉冲发生器的数量不同。 具体型号可参考下图: 注意:要输出高速脉冲的话,必须选择ST晶体管型号的PLC,SR继电器型的不支持。 S7-200SMART PLC能产生2种类型的高速脉冲…

【瑞吉外卖】公共字段填充

&#x1f341;博客主页&#xff1a;&#x1f449;不会压弯的小飞侠 ✨欢迎关注&#xff1a;&#x1f449;点赞&#x1f44d;收藏⭐留言✒ ✨系列专栏&#xff1a;&#x1f449;瑞吉外卖 ✨欢迎加入社区&#xff1a; &#x1f449;不会压弯的小飞侠 ✨人生格言&#xff1a;知足上…

激光雷达标定(坐标系转换)

文章目录1. 旋转矩阵2. 平移矩阵3. 坐标系的转换4. 坐标转换代码1. 旋转矩阵 由于激光雷达获取的点云数据的坐标是相对于激光雷达坐标系的&#xff0c;为了使车最终得到的点云数据坐标是在车坐标系下的&#xff0c;我们需要对点云中每一个点的坐标进行坐标转换。首先是需要对坐…

Docker笔记--创建容器、退出容器、查看容器、进入容器、停止容器、启动容器、删除容器、查看容器详细信息

目录 1--docker run创建容器 2--exit退出容器 3--docker ps查看容器 4--docker exec进入容器 5--docker stop停止容器 6--docker start启动容器 7--docker rm删除容器 8--docker inspect查看容器详细信息 1--docker run创建容器 sudo docker run -it --nametest redis…

Python 可迭代对象(Iterable)、迭代器(Iterator)与生成器(generator)之间的相互关系

1、迭代 通过重复执行的代码处理相似的数据集的过程&#xff0c;并且本次迭代的处理数据要依赖上一次的结果继续往下做&#xff0c;上一次产生的结果为下一次产生结果的初始状态&#xff0c;如果中途有任何停顿&#xff0c;都不能算是迭代。 # 非迭代例子 n 0 while n < …

SSM如何

目录 1、整合Mybatis 1.1.新建项目 1.2.添加pom依赖 1.3.application.yml 1.4.generatorConfig.xml 1.5.设置逆向生成 1.6.编写controller层 1.7.测试 2、整合 Mybatis-plus 2.1Mybatis-plus简介 2.2.创建项目 2.3.添加pom依赖 2.4.application.yml 2.5.MPGenerator 2.6.生成…

Stm32旧版库函数1——adxl335 模拟输出量 usart2

主函数&#xff1a; /******************************************************************************* // // 使用单片机STM32F103C8T6 // 晶振&#xff1a;8.00M // 编译环境 Keil uVision4 // 在3.3V的供电环境下&#xff0c;就能运行 // 波特率 19200 串口2 PA2(Tx) P…

equals方法:黑马版

目录 Object类的equals方法 Student类 测试类 第一步&#xff1a;使用比较 第二步&#xff1a;使用equals比较 第三步&#xff1a;在子类-Student类中重写equals方法 代码逐句分析 运行 Object类的equals方法 首先写一个类Student&#xff0c;属性有name和age&#xf…

UE5笔记【十二】蓝图函数BluePrint Function

上一篇讲了蓝图变量&#xff0c;这一篇说蓝图函数。BluePrint Function 函数&#xff0c;一般是为了将一段功能的代码提取出来&#xff0c;然后方便我们反复使用。重复的代码可以提取一个函数。类似的&#xff0c;相同的蓝图&#xff0c;我们也可以提取出一个蓝图函数来。 如…

青龙面板 香蕉

香蕉角本教程 介绍 香蕉视频 app —【多用户版】 一个账户每天稳定1元&#xff0c;可以自己提现&#xff0c;也可以兑换会员&#xff0c;脚本不停会员也不停&#xff01;可注册多个账户&#xff01;&#xff08;多账户福利自行看文章底部&#xff01;&#xff09; 拉取文件 …

【微服务】springboot 整合javassist详解

一、前言 Javassist 是一个开源&#xff0c;用于分析、编辑和创建Java字节码的类库&#xff0c;由东京工业大学数学和计算机科学系的 Shigeru Chiba &#xff08;千叶滋&#xff09;所创建。目前已加入了开放源代码JBoss 应用服务器项目&#xff0c;通过使用Javassist对字节码操…

linux redhat 8 创建逻辑卷

LVM与直接使用物理存储相比,有以下优点: 1. 灵活的容量. 当使用逻辑卷时,文件系统可以扩展到多个磁盘上,你可以聚合多个磁盘或磁盘分区成单一的逻辑卷. 2. 方便的设备命名 逻辑卷可以按你觉得方便的方式来起任何名称. 3.磁盘条块化. 你可以生成一个逻辑盘,它的数据可以被…