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,一经查实,立即删除!

相关文章

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

作者&#xff1a;柳文浩、李浩杰、苏伟男、贾思萌、张天芸 单位&#xff1a;西安外事学院 指导老师&#xff1a;胡宝权、陈小虎 1. 产品说明 1.1 设计目的 近年来&#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…

属性级情感分析

笔记为自我总结整理的学习笔记&#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, // 禁止关闭按钮…

vue随意置换页面元素位置

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

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>相…

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

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

讯飞星火知识库文档问答Web API的使用(二)

上一篇提到过星火spark大模型&#xff0c;现在有更新到3.0&#xff1a; 给ChuanhuChatGPT 配上讯飞星火spark大模型V2.0&#xff08;一&#xff09; 同时又看到有知识库问答的web api&#xff0c;于是就测试了一下。 下一篇是在ChuanhuChatGPT 中单独写一个基于星火知识库的内容…

【Android Jetpack】Navigation的使用

引入 单个Activity嵌套多个Fragment的UI架构模式&#xff0c;非常非常普遍。但是&#xff0c;对Fragment的管理一直是一件比较麻烦的事情。工程师需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。页面的切换通常还包括对应用程序App bar的管理、Fragme…

[个人笔记] Zabbix实现Webhook推送markdown文本

系统工程 - 运维篇 第四章 Zabbix实现Webhook推送markdown文本 系统工程 - 运维篇系列文章回顾Zabbix实现Webhook推送markdown文本前言实施步骤 Zabbix新增报警媒介类型Zabbix给用户新增报警媒介Zabbix修改动作的执行操作和恢复操作验证&测试 参考来源 系列文章回顾 第一章…

探索RockPlus SECS/GEM平台 - 赋能半导体行业设备互联

SECS/GEM协议&#xff0c;全称为半导体设备通讯标准/通用设备模型&#xff08;SECS/Generic Equipment Model&#xff09;&#xff0c;是一种广泛应用于半导体制造行业的通信协议。它定义了半导体设备与工厂主控系统&#xff08;如MES&#xff09;之间的通信方式&#xff0c;使…

PGP 遇上比特币

重复使用 PGP 密钥作为比特币密钥 介绍 在数字安全领域&#xff0c;密码学在确保数据的完整性和真实性方面发挥着至关重要的作用。 一种广泛使用的加密技术是使用 Pretty Good Privacy (PGP1)。 PGP 为安全通信&#xff08;例如电子邮件、文件传输和数据存储&#xff09;提供加…

解密Spring Cloud微服务调用:如何轻松获取请求目标方的IP和端口

公众号「架构成长指南」&#xff0c;专注于生产实践、云原生、分布式系统、大数据技术分享。 目的 Spring Cloud 线上微服务实例都是2个起步&#xff0c;如果出问题后&#xff0c;在没有ELK等日志分析平台&#xff0c;如何确定调用到了目标服务的那个实例&#xff0c;以此来排…

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《考虑氢储一体化协同的综合能源系统低碳优化》

这个标题涉及到考虑了多个方面的能源系统优化&#xff0c;其中关键的关键词包括"氢储一体化"、"协同"、"综合能源系统"和"低碳优化"。以下是对这些关键词的解读&#xff1a; 氢储一体化&#xff1a; 氢储存&#xff1a; 指的是氢气的存…

计算机组成原理-Cache替换算法

文章目录 总览随机算法&#xff08;RAND&#xff09;先进先出算法&#xff08;FIFO&#xff09;近期最少使用算法&#xff08;LRU&#xff09;最不经常使用算法&#xff08;LFU&#xff09;总结 总览 随机算法&#xff08;RAND&#xff09; 没有选择性地考虑替换哪一块Cache&a…

功率整流器的作用是什么?SURS8340T3G车规级功率整流器的介绍

汽车级功率整流器是一种用于汽车电子系统的功率电子器件&#xff0c;用于将交流电转换为直流电以供电子设备使用。汽车级功率整流器需要具有高效率、高可靠性、高稳定性和高温度工作能力等特点。其中&#xff0c;SURS8340T3G 是一种常见的汽车级功率整流器。 SURS8340T3G 是一…