Android 虚拟机与类加载机制

1、Dalvik 虚拟机

Android 应用程序运行在 Dalvik/Art 虚拟机上,并且每一个应用程序都有一个单独的 Dalvik/Art 虚拟机实例。

1.1 JVM 与 Dalvik

Dalvik 虚拟机也算是一个 Java 虚拟机,它是按照 JVM 虚拟机规范实现的,二者的特性差不多,不过还是有一些区别的:

  1. 执行的指令集不同:Java 虚拟机执行的是 class 文件,Dalvik 虚拟机执行的是 dex 文件
  2. Java 虚拟机的指令集基于堆栈,Dalvik 虚拟机的指令集基于寄存器
  3. JDK 1.8 默认垃圾回收器是 Parallel Scavenge(年轻代)+ ParallelOld(老年代),而 Dalvik 默认垃圾回收器是 CMS

两个虚拟机对比

DEX(Dalvik Executable Format)是专为 Dalvik 设计的一种压缩格式,它是很多 class 文件处理压缩后的产物,最终可以在 Android 运行时环境执行。

基于栈的虚拟机

基于栈的虚拟机会给每一个运行的线程分配一个独立的栈(虚拟机栈,是 JVM 运行时数据区的五大组成部分之一)。栈中记录了方法调用的历史,每有一次方法调用,栈中便会多一个栈桢。最顶部的栈桢称作当前栈桢,代表当前执行的方法。基于栈的虚拟机通过操作数栈进行所有操作。示意图如下:

虚拟机栈示意图

对于一段很简单的代码,可以通过查看字节码文件查看它的指令:

源代码与字节码

指令含义:

  • ICONST_1:将 int 类型常量 1 压入操作数栈位置 1
  • ISTORE0:将栈顶 int 类型值存入局部变量表位置 0。test() 是一个静态方法,因此局部变量表就不用把位置 0 预留出来保存 this
  • IADD:执行 int 类型的加法

这些指令的执行过程图如下:

指令执行过程

基于寄存器的虚拟机

寄存器是 CPU 的组成部分,是有限存储容量的高速存储部件,它们可用来暂存指令、数据和地址。

寄存器结构示意图

上图是寄存器的简化结构,运行步骤如下:

  • 从程序计数器指定的位置取出指令放到指令寄存器中
  • 根据指令内容 LOADA,100 从内存地址 100 取出数据放到数据寄存器 AX 中
  • 上一条指令执行完毕,程序计数器 +1,再从头循环上述过程。直到执行完 STOREC,108 将计算结果保存到内存地址 108 处

基于寄存器的虚拟机,实际上是为了模拟上述的工作流程,而不是真正的在物理上使用了 CPU 中的寄存器来完成上述工作的。

基于寄存器的虚拟机中没有操作数栈和局部变量表,但是有很多虚拟寄存器(理解成用寄存器代替了操作数栈和局部变量表吧)。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。与 JVM 相似,在 DalvikVM 中每个线程都有自己的 PC 和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。

基于寄存器的虚拟机执行字节码过程

栈式虚拟机vs寄存器式虚拟机

与 JVM 相比,可以发现 DalvikVM 的指令数明显减少了,数据移动次数也明显减少了(没有了在操作数栈和局部变量表之间的移动)。

1.2 ART 与 Dalvik

Dalvik 虚拟机执行的是 dex 字节码,解释执行。从 Android 2.2 版本开始,支持 JIT 即时编译(JustInTime)。JIT 是指在程序运行的过程中会选择热点代码(经常执行的代码)进行编译或者优化。

而 ART(Android Runtime)是在 Android 4.4 中引入的一个开发者选项,也是 Android 5.0 及更高版本的默认 Android 运行时。ART 虚拟机执行的是本地机器码。Android 的运行时从 Dalvik 虚拟机替换成 ART 虚拟机,并不要求开发者将自己的应用直接编译成目标机器码,APK 仍然是一个包含 dex 字节码的文件。

ART 虚拟机执行的本地机器码是安装的时候预编译产生的。

Dalvik 下安装应用时,会将 dex 字节码文件优化生成 odex 文件;ART 引入了预先编译机制(AheadOfTime),即在安装时,ART 使用设备自带的 dex2oat 工具来编译应用,dex 中的字节码将被编译成本地机器码。进行 AOT 最恰当的时机也是在安装应用的时候。

通过上图可以看出两个虚拟机对 dex 文件不同的处理方式:

  • Dalvik 虚拟机进行 dexopt 操作,在加载一个 dex 文件时,对 dex 文件进行验证和优化的操作,其对 dex 文件的优化结果变成了 odex(Optimizeddex)文件,这个文件和 dex 文件很像,只是使用了一些优化操作码。
  • ART 虚拟机进行 dex2oat 操作,采用预先编译机制,在安装时对 dex 文件执行 AOT 提前编译操作,编译为 ART 可执行的 elf 文件(机器码)。

预先编译机制 AOT 是和即时编译 JIT 相对应的概念。

1.3 Art 虚拟机的优化

Android 从最初的版本使用的是 Dalvik 虚拟机,在 2.2 版本加入了 JIT。而 Art 虚拟机在 4.4 版本是一个开发者选项,从 5.0 版本开始作为默认使用的虚拟机。

由于 Art 虚拟机在安装应用时会使用 AOT 的预编译,因此 5.x 和 6.x 版本在安装应用时会比原来慢,为了解决这个问题又在 7.0 时做了改进,混合使用 AOT 编译、解释和 JIT:

  1. 最初安装应用时不进行任何 AOT 编译(安装又快了),运行过程中解释执行,对经常执行的方法进行 JIT,经过 JIT 编译的方法将会记录到 Profile 配置文件中
  2. 当设备闲置和充电时,编译守护进程会运行,根据 Profile 文件对常用代码进行 AOT 编译。待下次运行时(下一次启动时)直接使用

Art优化后的工作方式

在读取 base.odex 文件执行代码时,会对经常执行的方法做 JIT 处理,放到 Profile 配置文件中,这是图中 collect 的过程;然后在设备闲置和充电时,通过 get profile 拿到这些配置文件,在 BackgroundDexOptService(这是一个 JobService)内运行 dex2oat 工具得到 base.art 文件。

当应用下次运行时,去看 /data/app 目录下是否有 base.art 这个文件,如果有,就用 ClassLoader 把这个文件中的类加载进内存,以后可以执行机器码了,这样就不用再去找 base.odex 文件以解释方式执行代码了。

2、ClassLoader

任何一个 Java 程序都是由一个或多个 class 文件组成,在程序运行时,需要将 class 文件加载到 JVM 中才可以使用,负责加载这些 class 文件的就是 Java 的类加载器 ClassLoader。ClassLoader 的作用简单来说就是加载 class 文件,提供给程序运行时使用。每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的:

class Class<T> { ... private transient ClassLoader classLoader; ... }

ClassLoader 是一个抽象类,而它在 Android 中的具体子类主要有:

  • BootClassLoader:用于加载 Android Framework 层 class 文件。
  • PathClassLoader:用于 Android 应用程序类加载器。可以加载指定的 dex,以及 jar、zip、apk 中的 classes.dex。
  • DexClassLoader:用于加载指定的 dex,以及 jar、zip、apk 中的 classes.dex。

用代码进行一下测试:

    Log.d(TAG, "Activity.class 由: " + Activity.class.getClassLoader() + " 加载");Log.d(TAG, "MainActivity.class 由: " + MainActivity.class.getClassLoader() + " 加载");Log.d(TAG, "String.class 由: " + String.class.getClassLoader() + " 加载");

输出为:

D/MainActivity: Activity.class 由: java.lang.BootClassLoader@5052f32 加载
D/MainActivity: MainActivity.class 由: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.myapplication-x6QX4W6vcSz6MxKY4vpW2w==/base.apk", dex file "/data/user/0/com.example.myapplication/app_fake_apk/app/classes.dex"],nativeLibraryDirectories=[/data/app/com.example.myapplication-x6QX4W6vcSz6MxKY4vpW2w==/lib/x86, /system/lib]]] 加载
D/MainActivity: String.class 由: java.lang.BootClassLoader@5052f32 加载

MainActivity 继承自 AppCompatActivity,而 AppCompatActivity 是官方提供的第三方库中的类,不算是系统文件,因此它是被 PathClassLoader 加载的。

一些博客里说 PathClassLoader 只能加载已安装的 apk 的 dex,其实这说的应该是在 Dalvik 虚拟机上,但现在一般不用关心 Dalvik 了。

ClassLoader关系图

2.1 PathClassLoader

ClassLoader 的任务就是把 class 文件读取成 byte[] 然后再转换成 Class 对象,像我们平时自己做的应用中的 class 文件都是通过 PathClassLoader 进行加载的,下面结合源码来看看具体是怎么做的。

loadClass()

类加载器都是通过 ClassLoader 的 loadClass() 去加载一个类的,那么去 PathClassLoader 中查看:

public class PathClassLoader extends BaseDexClassLoader {public PathClassLoader(String dexPath, ClassLoader parent) {super(dexPath, null, null, parent);}public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {super(dexPath, null, librarySearchPath, parent);}
}

PathClassLoader 中没有 loadClass(),去父类 BaseDexClassLoader 中找,也没有,再向上找到抽象父类 ClassLoader:

public abstract class ClassLoader {// 当前ClassLoader对象的父ClassLoaderprivate final ClassLoader parent;public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{// 1.找缓存,如果该类已经被加载过,直接从缓存中取。Class<?> c = findLoadedClass(name);if (c == null) {try {if (parent != null) {// 2.调用父加载器(注意不是父类加载器)的loadClass()c = parent.loadClass(name, false);} else {// 看源码就是直接 return null(与Java不同)。c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// 3.如果前面两次都没找到要加载的类,通过自己的findClass()再找一次。c = findClass(name);}}return c;}
}

我们看到 loadClass() 主要做了三件事:

  1. 先通过 findLoadedClass() 去找缓存,看看 name 所对应的类是否在之前被加载过,如果是,就可以直接作为返回结果了,否则就继续向下执行。
  2. 倘若缓存未命中,先让自己的父加载器通过其 loadClass() 去加载这个类,如果加载成功也就直接作为返回结果,否则执行下一步。需要注意的是,父加载器并不是指父类,与当前 ClassLoader 并不存在继承关系。
  3. 上一步加载失败后,就只能通过当前 ClassLoader 的 findClass() 去加载 name 对应的类了。

关于第二点的父加载器我们再多说一点。在创建 PathClassLoader 对象时,其构造方法要求传入一个 ClassLoader,就是父加载器 parent:

    public PathClassLoader(String dexPath, ClassLoader parent) {super(dexPath, null, null, parent);}public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {super(dexPath, null, librarySearchPath, parent);}

这个 parent 的类型不必是 PathClassLoader 的父类,即父加载器类型不必是当前类加载器的父类。

这一点在源码中也有所印证,创建系统使用的 PathClassLoader 对象时,传的 parent 是一个 BootClassLoader 实例。在 ActivityThread 的 handleBindApplication() 进行 Application 绑定时,会获取 Application Context 的 ClassLoader:

	private void handleBindApplication(AppBindData data) {...// Continue loading instrumentation.if (ii != null) {initInstrumentation(ii, data, appContext);} else {mInstrumentation = new Instrumentation();mInstrumentation.basicInit(this);}...}private void initInstrumentation(InstrumentationInfo ii, AppBindData data, ContextImpl appContext) {...final ContextImpl instrContext = ContextImpl.createAppContext(this, pi,appContext.getOpPackageName());try {// 获取 App Context 的 ClassLoaderfinal ClassLoader cl = instrContext.getClassLoader();mInstrumentation = (Instrumentation)cl.loadClass(data.instrumentationName.getClassName()).newInstance();} catch (Exception e) {throw new RuntimeException("Unable to instantiate instrumentation "+ data.instrumentationName + ": " + e.toString(), e);}...}  

ContextImpl 的 getClassLoader() 会根据条件获取 ClassLoader 对象:

	@Overridepublic ClassLoader getClassLoader() {return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());}

我们看保底的由系统提供的 ClassLoader:

	public static ClassLoader getSystemClassLoader() {return SystemClassLoader.loader;}static private class SystemClassLoader {public static ClassLoader loader = ClassLoader.createSystemClassLoader();}private static ClassLoader createSystemClassLoader() {String classPath = System.getProperty("java.class.path", ".");String librarySearchPath = System.getProperty("java.library.path", "");// 创建 PathClassLoader,并指定其父加载器是 BootClassLoaderreturn new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());}

PathClassLoader 的父类是 BaseDexClassLoader,而系统为 PathClassLoader 指定的父加载器是 BootClassLoader。

双亲委托机制

回到 ClassLoader 的 loadClass(),可以看出对于任意一个 ClassLoader,它想要加载 class 文件时,都是先去找它的父加载器去 loadClass(),这就是我们常说的双亲委托机制

某个类加载器在加载类时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。

使用双亲委托机制的原因是:

  1. 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子 ClassLoader 再加载一次,直接去父加载器的缓存中拿就可以了。
  2. 安全性考虑,防止核心 API 库被随意篡改。

对于第二点,需要解释一下。比如说,没有采取双亲委托机制,即代码变成这样:

    protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{// 找缓存,如果该类已经被加载过,直接从缓存中取。Class<?> c = findLoadedClass(name);if (c == null) {c = findClass(name);}return c;}

缓存中没有直接就在该 ClassLoader 对象中自己用 findClass() 去找这个类,那么假如我写一个跟系统全类名相同的类 java.lang.String,findClass() 在加载的时候就会加载到我们自己写的 java.lang.String 类,使得系统类被篡改,引发安全问题。

反过来说,使用了双亲委托,那么总是 BootClassLoader 先去加载系统里边的类,相当于加了一层拦截,将篡改系统代码的隐患给拦截掉了。

findClass()

最后看到 findClass(),它在抽象基类 ClassLoader 中是一个未实现的方法:

    protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}

所以就得看子类实现了,BaseDexClassLoader 重写了该方法:

    private final DexPathList pathList;public BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent, boolean isTrusted) {super(parent);// 实例化 pathList,传入了 dex 文件的路径 dexPaththis.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);...}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {List<Throwable> suppressedExceptions = new ArrayList<Throwable>();Class c = pathList.findClass(name, suppressedExceptions);// c 的判空处理,省略...return c;}

在看 DexPathList 的 findClass() 之前,先看它的构造方法,会实例化一个非常重要的成员变量 dexElements:

/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java:private Element[] dexElements;DexPathList(ClassLoader definingContext, String dexPath,String librarySearchPath, File optimizedDirectory, boolean isTrusted) {// save dexPath for BaseDexClassLoaderthis.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions, definingContext, isTrusted);}// dexPath 是 dex 文件的路径,它可以是多个 dex 文件,形式为 /a/a\.dex;/a/b.dex// 该方法会将 searchPath 中的所有文件路径分离,并创建出文件对象装入 List<File> 中。private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {List<File> result = new ArrayList<>();if (searchPath != null) {// File.pathSeparator是分号; 而File.separator是反斜杠\for (String path : searchPath.split(File.pathSeparator)) {...result.add(new File(path));}}return result;}// 遍历 List<File>,将 File 封装成 DexFile 后再封装成 Element,存入 Element[]private static Element[] makeDexElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {Element[] elements = new Element[files.size()];int elementsPos = 0;for (File file : files) {if (file.isDirectory()) {// We support directories for looking up resources. Looking up resources in// directories is useful for running libcore tests.elements[elementsPos++] = new Element(file);} else if (file.isFile()) {String name = file.getName();DexFile dex = null;if (name.endsWith(DEX_SUFFIX)) {// Raw dex file (not inside a zip/jar).try {// 将普通的 file 文件封装成 DexFile 对象dex = loadDexFile(file, optimizedDirectory, loader, elements);if (dex != null) {elements[elementsPos++] = new Element(dex, null);}} catch (IOException suppressed) {...}} else {try {dex = loadDexFile(file, optimizedDirectory, loader, elements);} catch (IOException suppressed) {suppressedExceptions.add(suppressed);}if (dex == null) {elements[elementsPos++] = new Element(file);} else {elements[elementsPos++] = new Element(dex, file);}}if (dex != null && isTrusted) {dex.setTrusted();}} else {System.logW("ClassLoader referenced unknown path: " + file);}}if (elementsPos != elements.length) {elements = Arrays.copyOf(elements, elementsPos);}return elements;}

dexElements 是一个 Element 数组,而 Element 内封装着 DexFile 对象。然后我们再来看 DexPathList 的 findClass():

	public Class<?> findClass(String name, List<Throwable> suppressed) {// dexElements 是一个 Element[],查找类的工作会再次转交给 Elementfor (Element element : dexElements) {Class<?> clazz = element.findClass(name, definingContext, suppressed);if (clazz != null) {return clazz;}}...return null;}static class Element {private final DexFile dexFile;public Class<?> findClass(String name, ClassLoader definingContext,List<Throwable> suppressed) {// 最终是由 DexFile 的 loadClassBinaryName() 做类的加载return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;}}

通过 Element 内封装的 DexFile 的 loadClassBinaryName() 调用 native 方法 defineClassNative() 完成类的加载:

/libcore/dalvik/src/main/java/dalvik/system/DexFile.java:public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {return defineClass(name, loader, mCookie, this, suppressed);}private static Class defineClass(String name, ClassLoader loader, Object cookie,DexFile dexFile, List<Throwable> suppressed) {Class result = null;try {result = defineClassNative(name, loader, cookie, dexFile);} catch (NoClassDefFoundError e) {if (suppressed != null) {suppressed.add(e);}} catch (ClassNotFoundException e) {if (suppressed != null) {suppressed.add(e);}}return result;}// native 方法调用的是 /art/runtime/native/dalvik_system_DexFile.cc 中的 DexFile_defineClassNative()private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,DexFile dexFile)throws ClassNotFoundException, NoClassDefFoundError;

整个流程下来,大致是 ClassLoader 持有一个 DexPathList 对象,DexPathList 内维护着一个 Element[],每个 Element 内都封装着一个 DexFile 可以用来加载对应的 dex 文件。时序图如下:

类加载时序图

2.2 DexClassLoader

PathClassLoader 与 DexClassLoader 具有共同父类 BaseDexClassLoader:

    public class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);}}public class PathClassLoader extends BaseDexClassLoader {public PathClassLoader(String dexPath, ClassLoader parent) {super(dexPath, null, null, parent);}public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {super(dexPath, null, librarySearchPath, parent);}}

可以看到两者唯一的区别在于:创建 DexClassLoader 需要传递一个 optimizedDirectory 参数,并且会将其创建为 File 对象传给 super,而 PathClassLoader 则直接给该参数传 null。因此两者都可以加载指定的 dex,以及 jar、zip、apk 中的 classes.dex:

    PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());File dexOutputDir = context.getCodeCacheDir();DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex", dexOutputDir.getAbsolutePath(), null, getClassLoader());

其实,optimizedDirectory 参数就是 dexopt 产出 odex 的目录。那 PathClassLoader 创建时,这个目录为 null,是否意味着不进行 dexopt?并不是,optimizedDirectory 为 null 时的默认路径为:/data/dalvik-cache。

在 API 26 源码中,将 DexClassLoader 的 optimizedDirectory 标记为了 deprecated 弃用,实现也变为了:

    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {super(dexPath, null, librarySearchPath, parent);}

和 PathClassLoader 别无二致。

3、热修复原理简述

ClassLoader 的一个典型应用是热修复,简单说下原理。

首先基于 ClassLoader 加载类过程的分析,找该 ClassLoader 缓存 -> 让父 ClassLoader 加载 -> 该 ClassLoader 自己加载 dex 文件,如果想要实现热修复,那么前两步是无法实现的,也就是说你需要在第一次加载 dex 文件时就把需要做热修复的 dex 文件(以下称补丁 dex)加载进内存。

而在如何加载补丁 dex 的问题上,可以先看下图:

由于使用双亲委托机制加载类,两个全类名相同的类,先被加载的那个会进入内存变成 Class 对象,而后被遍历到的 dex 文件中的类就不会被加载。

再加上 ClassLoader 遍历 Element 数组时是按照数组角标顺序由小到大遍历的,那么我们可以通过反射,把带有需要热修复的类的 Patch.dex 文件放到 Element 数组的最前面,然后让 ClassLoader 先加载 Patch.dex 中的类,就可以实现热修复了。

以上是实现思路,实现过程可以这样安排:

  • 获取到当前应用的 PathClassLoader
  • 反射获取到 DexPathList 的成员 pathList
  • 反射修改 pathList 的 dexElements:
    1. 把补丁包的 patch.dex 转化为 Element[]
    2. 获得 pathList 的 dexElements 属性
    3. patch+old 合并,并反射赋值给 pathList 的 dexElements

如果对热修复感兴趣可以参考这篇文章 Android 热修复。

参考资料:

AndroidRuntime(ART)和Dalvik

AndroidN混合使用AOT编译,解释和JIT三种运行时

Android 9.0 ART编译分析(二)-Installd触发dex2oat编译流程

系统ClassLoader相关及Application初始化简单分析及总结

为什么PathClassLoader的父加载器(parent)是BootClassLoader?

Android动态加载之ClassLoader详解

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

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

相关文章

Springboot 使用 RabbitMq 延迟插件 实现订单到期未支付取消订单、设置提醒消息

示例业务场景&#xff1a; 场景1&#xff1a;客户下单后&#xff0c;15分钟内未支付取消订单&#xff01; 场景2&#xff1a;客户下单支付成功后&#xff0c;5分钟内商家未处理订单&#xff0c;需要推送一条消息提醒商家。如依旧未处理&#xff0c;则需要每隔2分钟消息提醒一下…

STL常用算法-C++

概述&#xff1a; 算法主要是由头文件 <algorithm> <functional> <numeric> 组成。<algorithm> 是所有 STL 头文件中最大的一个&#xff0c;范围涉及是比较、交换、查找、遍历操作、复制、修改等等。<functional> 定义了一些模板类&#xff0c;…

机器人制作开源方案 | 网球自动拾取机

作者&#xff1a;柳文浩、李浩杰、苏伟男、贾思萌、张天芸 单位&#xff1a;西安外事学院 指导老师&#xff1a;胡宝权、陈小虎 1. 产品说明 1.1 设计目的 近年来&#xff0c;网球运动越来越受到老百姓的欢迎&#xff0c;各种规模的比赛层出不穷。然而由于网球运动极为激烈…

中国改性聚丙烯产业调研与投资战略报告(2023版)

内容介绍&#xff1a; 改性聚丙烯就是基于聚丙烯原料对其性能和其他方面的一些改进&#xff0c;如增强聚丙烯材料的冲击、拉伸强度、弹性等。聚丙烯改性主要通过化学改性和物理改性进行调整&#xff0c;使其在拥有本身优异性能的同时可以兼顾其他有利于生产和使用的性能。改性…

Python3基础

导包 在 python 用 import 或者 from...import 来导入相应的模块。 将整个模块(somemodule)导入&#xff0c;格式为&#xff1a; import somemodule 从某个模块中导入某个函数,格式为&#xff1a; from somemodule import somefunction 从某个模块中导入多个函数,格式为&#…

C++基础 -4- C/C++混合编程

引用格式(图片代码段呈现) extern "C" {#include "string.h" }代码验证 &#xff08;分别使用了C/C 的标准输出&#xff09; #include "iostream"using namespace std;extern "C" { #include "stdio.h" #include "…

Win11修改用户名(超详细图文)

新买的电脑一般预装Windows11系统&#xff08;家庭与学生版&#xff09;&#xff0c;新电脑初次开机使用微软邮箱账号登录&#xff0c;则系统将用户名自动设置成邮箱前5位字符。我的用户名便是一串数字【231xx】&#xff08;qq邮箱前5位&#xff09;&#xff0c;看着很不舒服&a…

快速掌握Pyqt5的10种容器(Containers)

快速掌握Pyqt5的三种主窗口 快速掌握Pyqt5的2种弹簧 快速掌握Pyqt5的5种布局 快速弄懂Pyqt5的5种项目视图&#xff08;Item View&#xff09; 快速弄懂Pyqt5的4种项目部件&#xff08;Item Widget&#xff09; 快速掌握Pyqt5的6种按钮 快速掌握Pyqt5的20种输入控件&#xff08;…

属性级情感分析

笔记为自我总结整理的学习笔记&#xff0c;若有错误欢迎指出哟~ 属性级情感分析 简介数据集介绍数据加载和预处理&#xff08;data_utils.py&#xff09;预训练模型&#xff08;skep&#xff09;模型定义模块&#xff08;model.py&#xff09;训练配置&#xff08;config.py&am…

element 的 Notification 通知,自定义内容

通知事件&#xff1a; // 商户后台通知 MerchantBackgroundNotice() {// 禁止消息通知弹出多条if(this.notifyInstance) {this.notifyInstance.close();}const h this.$createElement; // 创建文本节点this.notifyInstance this.$notify({showClose: false, // 禁止关闭按钮…

Powerbuilder中利用API实现动画特效

Powerbuilder中利用API实现动画特效 摘要:本文通过在Powerbuilder中调用Windows API函数,实现任意图片的放大及任意图片从正面逐渐翻转到背面的功能 关键词:内存设备描述表 显示器设备描述表 API 一、问题的提出 Powerbuilder是一个面向对象的开发大型数据库的图形化的前端开…

vue随意置换页面元素位置

写在前面&#xff0c;博主是个在北京打拼的码农&#xff0c;从事前端工作5年了&#xff0c;做过十多个大大小小不同类型的项目&#xff0c;最近心血来潮在这儿写点东西&#xff0c;欢迎大家多多指教。 对于文章中出现的任何错误请大家批评指出&#xff0c;一定及时修改。有任何…

数据结构 / 顺序表操作 / 顺序表尾部删除

1.实现逻辑 只需要把顺序表的长度减1。 2.顺序表尾部删除函数代码 /**顺序表尾部删除* */int delete_tail(sqlist *list) {if(NULLlist || 1is_list_empty(list)){return -1;}list->len--;return 0;}3. 完整代码 #include <string.h> #include <stdlib.h> #in…

Redis Cluster主从模式详解

在软件的架构中&#xff0c;主从模式&#xff08;Master-Slave&#xff09;是使用较多的一种架构。主&#xff08;Master&#xff09;和从&#xff08;Slave&#xff09;分别部署在不同的服务器上&#xff0c;当主节点服务器写入数据时&#xff0c;同时也会将数据同步至从节点服…

图解算法数据结构-LeetBook-树03_层序遍历奇数偶数行方向不同

一棵圣诞树记作根节点为 root 的二叉树&#xff0c;节点值为该位置装饰彩灯的颜色编号。请按照如下规则记录彩灯装饰结果&#xff1a; 第一层按照从左到右的顺序记录 除第一层外每一层的记录顺序均与上一层相反。即第一层为从左到右&#xff0c;第二层为从右到左。 示例 1&…

自动化测试-Selenium

一. Selenium介绍 selenium 是用来做web自动化测试的框架,支持各种浏览器,各种,支持各种语言 原理: 二. 元素定位 2.1 XPath 定位 绝对路径: /html/head/title 相对路径以双斜杠开头,常见的相对路径定位有以下几种: <1>相对路径索引: 索引是从1开始的 <2>相…

语音识别学习笔记

目录 开源的语音识别项目 端到端的多说话人语音识别序列化训练方法简介 新一代 Kaldi: Two-pass 实时语音识别 开源的语音识别项目 有哪些语音识别的开源项目&#xff1f; - 知乎 端到端的多说话人语音识别序列化训练方法简介 端到端的多说话人语音识别序列化训练方法简介 …

探索深度学习:从理论到实践的全面指南

探索深度学习&#xff1a;从理论到实践的全面指南 摘要&#xff1a; 本文旨在提供一个关于深度学习的全面指南&#xff0c;带领读者从理论基础到实践应用全方位了解这一技术。我们将介绍深度学习的历史、基本原理、常用算法和应用场景&#xff0c;并通过Python代码示例和Tens…

STM32使用多路PWM注意事项

这是使用CubeMX自动产生的代码&#xff0c;使用TIM2产生了PA0,PA1,PA2,PA3这4路PWM&#xff0c;可以看到里面Pulse是共同使用了一个sConfigOC,如果是需要动态调整Pulse&#xff0c;就需要特别注意。 如果是用来产生呼吸灯&#xff0c;就会把这4个PWM都打乱&#xff0c;我觉得&a…

Go查询Elasticsearch

在 Go 中需要在 Elasticsearch 中执行带有过滤条件的查询时&#xff0c;你可以使用 github.com/olivere/elastic 库的过滤器&#xff08;Filter&#xff09;功能。以下是一个示例代码&#xff0c;展示了如何在 Go 中使用 Elasticsearch 进行带有过滤条件的分页查询&#xff1a;…