1.类加载机制

.java文件不是可执行的文件,需要先编译成.class文件才可以被虚拟机执行。而类加载就是指通过类加载器把.class文件加载到虚拟机的内存空间,具体来说是方法区。类通常是按需加载,即第一次使用该类时才加载。

Java与Android都是把类加载到虚拟机内存中,然后由虚拟机转换成设备可识别的机器码。由于它们使用的虚拟机不同,所以在类加载方面也有所区别。Java的虚拟机是JVM,Android的虚拟机是dalvik/art(5.0以后虚拟机是art,是对dalvik的升级)。Java虚拟机运行的是class文件,而Android 虚拟机运行的是dex文件。dex其实是class文件的集合,是对class文件优化的产物,为了避免出现重复的class。

先了解一下Android的运行流程:

①Android程序编译的时候,会将.java文件编译成.class文件;

②生成apk时,将.class文件打包为.dex文件;

③Android程序运行的时候,Android的Dalvik/art虚拟机就加载dex文件,然后加载其中的.class文件到内存中来使用。

当需要使用某个类时,虚拟机就会加载它的Class文件,并创建对应的Class对象。将Class文件加载到虚拟机的内存里,这个过程称为类加载。

类加载流程:

一个类被加载到虚拟机内存中需要经历加载、连接、初始化几个过程。其中连接分为三个步骤:验证、准备、解析。

①步骤一:加载:将外部的Class文件加载到虚拟机内,并存储到方法区。

加载过程主要做了三件事:

1)通过类的全限定名来获取定义此类的二进制字节流。即把类以流的形式加载进内存,类的来源没有说,可以是jar包,也可以是class文件或者是apk文件。这个特性是能够实现插件化技术的理论基础。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在获取到这个字节流以后,虚拟机就会把类中的静态存储结果保存到方法区中,保存的过程会转化对应方法区中的数据结构。所以说静态的结构都保存在内存中的方法区中。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的方位入口。当类加载进内存以后,每个类都会生成一个对应的Class对象,当使用这个类的时候,都是通过此Class对象为入口来使用的,比如写程序的时候通过new关键字创建一个类的对象就是通过这个类的Class对象来创建的。

②步骤二:验证:确保加载到Class文件里的信息符合虚拟机要求。主要是对类中的语法结构是否合法进行验证,确认类型符合Java语言的语义。

③步骤三:准备:为类变量分配内存,并设置类变量的初始化值(初始值通常为0,非开发者定义的值)。这个阶段是给类中的类变量分配内存,设置默认初始值,比如一个静态的int变量初始值是0,布尔变量初始值是false。

④步骤四:解析:将常量池内的符号引用转为直接引用,如hello()方法,hello是符号引用,地址值是直接引用。也就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。这个过程可以理解为一开始虚拟机对加载到内存中的各种类、字段等并没有一一编号,只是通过一个符号去表示,在解析阶段,虚拟机把内存中的类、方法等进行统一管理起来。

⑤步骤五:初始化:对类变量进行初始化。初始化阶段才真正到了类中定义的java代码的阶段,在这个阶段会对类中的变量和一些代码块进行初始化,比如为类变量进行初始化,在准备阶段对类变量进行的默认初始化,到这个阶段就对变量进行显式的赋值,其中静态代码块就是在这个阶段来执行的。

注意:初始化不会马上执行,当一个类被主动使用的时候才会去初始化,主要有几种情况:

1)当创建某个类的新实例时(如通过new或者反射等);

2)当调用某个类的静态方法时;

3)当使用某个类或接口的静态字段时;

4)当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时;

5)当初始化某个子类时;

类从被加载到虚拟机内存到被卸载,整个完整的生命周期包括:加载 --> 验证 --> 准备 --> 解析 --> 初始化 --> 使用 --> 卸载七个阶段,其中验证、准备、解析这三个部分统称为连接。

类加载流程比较复杂,但是开发者能够控制的只有第一步「加载」还有最后一步「初始化」,第一步记载的理论基础决定了插件化可以实现,最后一步初始化就是执行实际程序中的代码。其余都是由虚拟机控制的。

类加载的主要作用:

①实现类的加载功能

②确保被加载类在虚拟机中的唯一性

2.Android中的类加载器

由于java编译出来的是class文件,而Android的APK中包含的是dex文件,dex文件是将所需的所有class文件重新打包,打包的规则不是简单地压缩,而是完全对class文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,所以java和Android中的ClassLoader也不一样,这里主要来看一下Android中的ClassLoader。

类加载进内存后,Android程序通过ClassLoader类去加载内存中的类,然后进行解析运行。

Android中包含以下几种类加载器:

①BootClassLoader :用来加载Framework层的字节码文件。只能加载Android系统的类,是ClassLoader的内部类,开发者无法调用。Android系统启动时会使用BootClassLoader来预加载常用类。

②BaseDexClassLoader :是PathClassaLoader和DexClassLoader父类。

③PathClassLoader :用来加载内存中已经安装的apk中的dex文件。通常用于加载APK中开发者自己写的类(含三方库)。

④DexClassLoader :用来加载指定目录中的字节码文件,可以加载内存以外的aar/apk/jar文件。通常用于执行动态加载,能够加载指定路径的apk/jar/zip/dex文件, 因此很多热修复和插件化方案都采用。(DexClassLoader可以指定odex的路径,而PathClassLoader则采用系统默认的缓存路径,在8.0以后没有区别)

⑤URLClassLoader:加载.jar文件和文件夹中的class,javaWeb等使用,谷歌不用

BaseDexClassLoader为核心类。

常用的ClassLoade就两个:DexClassLoader 和 PathClassLoader,这两个类的源码都很简单:

public class DexClassLoader extends BaseDexClassLoader {

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {

super(dexPath, new File(optimizedDirectory) , libraryPath, parent);

}

}

DexClassLoader类中只有一个构造方法,构造方法中直接调用了父类的构造,DexClassLoader继承了BaseDexClassLoader,构造方法中的参数的含义是:

参一 dexpath:要加载的dex文件的路径。

参二 optimizedDirectory:dex文件首次加载时会进行优化操作,这个参数即为优化后的odex文件的存放目录,官方推荐使用应用私有目录来缓存优化后的dex文件,dexOutputDir = context.getDir(“dex”, 0);

参三 libraryPath:动态库的路径。

参四 parent:当前加载器的父类加载器。

接着看PathClassLoader:

public class PathClassLoader extends BaseDexClassLoader {

public PathClassLoader(String dexPath, ClassLoader parent) {

super(dexPath, null, null, parent);

}

public PathClassLoader(String dexPath, String libraryPath,ClassLoader parent) {

super(dexPath, null, libraryPath, parent);

}

}

PathClassLoader有两个构造方法,同样也是直接调用了父类的构造方法,从构造方法上来看,DexClassLoader和PathClassLoader的区别只有第二个参数optimizedDirectory,在PathClassLoader中optimizedDirectory默认传入的是null。

这两个类的作用也是因为optimizedDirectory参数的不同而不同,在源码中看使用PathClassLoader由于没有传入optimizedDirectory,系统会自动生成以后缓存目录,即/data/dalvik-cache/,在这个目录存放优化后的dex文件。所以PathClassLoader只能加载已安装的apk的dex,即加载系统的类和已经安装的应用程序(安装的apk的dex文件会存储在/data/dalvik-cache中)。而DexClassLoader可以加载指定路径的apk、dex,也可以从sd卡中进行加载。

在DexPathList的makeDexElements方法中,对于dex文件,需要调用loadDexFile方法来生成一个DexFile。

private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,Element[] elements) throws IOException {

if (optimizedDirectory == null) {

return new DexFile(file, loader, elements);

} else {

String optimizedPath = optimizedPathFor( file, optimizedDirectory);

return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);

}

}

这里会判断optimizedDirectory是否为空,在PathClassLoader中传入的参数为空,而在DexClassLoader中传入了这个路径,会调用DexFile的loadDex方法。

因为apk其实也是一个压缩文件zip包,像第一次启动时,PathClassLoader会将apk解压存放在 /data/dalvik-cache目录下,而使用DexClassLoader则会将apk中可运行的文件提取出来,存放在optimizedDirectory路径下,那么应用程序启动时将会加载optimizedDirectory下的文件,启动速度更快,这就是odex优化。

3.双亲委托机制

双亲委托机制即父委托机制,指多个类加载器之间存在父子关系,当一个ClassLoader去加载一个类的时候,它会去判断该类是否已经加载,如果没有,它不会马上去加载,而是委托给父加载器进行查找,这样递归一直找到最上层的ClassLoader类,因此所有的类加载都会委托给顶层的父类即BootClassLoader进行加载。如果找到了,就直接返回这个类所对应的Class对象,如果都没有加载过,就从顶层的ClassLoader开始依次向下查找,每个加载器会从自己规定的位置去查找这个类,如果都没有,最后再由请求发起者去加载该类。

也就是说,第一次查找的时候,是从下到上依次从缓存中查找之前有没有加载过,如果有就返回,如果都没有,就从上到下依次从自己指定的位置去查找这个类,最后再交给发起者去加载该类。这时候如果子加载器不能加载,则抛出ClassNotFoundException异常。

双亲委托机制的代码流程在java.lang.ClassLoader#loadClass()中:

protected Class<?> loadClass(String name, boolean resolve) {

Class<?> c = findLoadedClass(name);

//检查类是否被加载过

if (c == null) {

try {

//如果没有加载,调用父类的加载器

if (parent != null) {

c = parent.loadClass(name, false);

} else {

//父类加载器为空,使用默认的启动类加载器

c = findBootstrapClassOrNull(name);

}

} catch (ClassNotFoundException e) {

}

if (c == null) {

//父类加载器无法加载,则调用自身的findClass()进行类加载

c = findClass(name);

}

}

return c;

}

注意,这里有一个误区:虽然PathClassLoader继承自BaseDexClassLoader,但是PathClassLoader的父类加载器并不是BaseDexClassLoader,从前面的例子中也可以看到,它的父类是parent,这里不要认为父类就是父类加载器,这是两个概念。

双亲委托机制的好处:

①保证class只会被加载一次,能有效确保一个类的全局唯一性,也就是说类的数据结构只会在第一次创建的时候被加载进内存(方法区),以后要创建这个类的对象的时候,直接用方法区中的Class在堆内存创建一个对象,这样创建对象就会比较快;

②保证系统类的安全性。因为在启动应用进程的时候就已经加载好了系统类(BootClassLoader),那后面运行期就不可能通过恶意伪造加载的方式去造成一些系统安全问题。因为虚拟机认为只有两个类名一致并且被同一个类加载器加载的类才是同一个类,所以这种机制保证了系统定义的类不会被替代。如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类。

4.ClassLoader加载类的过程

从源码看一下Android中ClassLoader加载类的整个流程。

首先需要获取DexClassLoader或PathClassLoader,然后调用loadClass()方法来动态加载某个类。

比如下面这样创建对象,然后调用loadClass方法加载某个类:

DexClassLoader dexClassLoader = new DexClassLoader(dexPath, getDir("dex", 0).getAbsolutePath(), null, getClassLoader());

loadClass()方法的实现在父类ClassLoader中,代码上面已经有了,loadClass()整个流程符合双亲委托机制,首先询问父加载器有没有加载过,如果没有加载过会使用参数传递进来的父加载器去加载,如果父加载器都没有加载过,会使用当前创建的加载器去加载该类。

parent是传入的加载器,通过Context的getClassLoader方法返回的,在Context中getClassLoader是个抽象方法,具体的实现在ContextImpl中,代码如下:

ContextImpl.java:

@Override

public ClassLoader getClassLoader() {

return mPackageInfo != null ?mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();

}

ClassLoader.java:

public static ClassLoader getSystemClassLoader() {

return SystemClassLoader.loader;

}

SystemClassLoader为ClassLoader的内部类:

static private class SystemClassLoader {

public static ClassLoader loader = ClassLoader.createSystemClassLoader();

}

private static ClassLoader createSystemClassLoader() {

String classPath = System.getProperty( "java.class.path", ".");

//最终返回了PathClassLoader作为系统加载器SystemClassLoader,而其父类为根加载器BootClassLoader

return new PathClassLoader(classPath, BootClassLoader.getInstance());

}

从上面的源码中可以看到,最后返回的是一个PathClassLoader对象,这个对象的父加载器是BootClassLoader,BootClassLoader是ClassLoader的一个内部类,从分析来看是Android平台上所有ClassLoader的最终parent。

返回PathClassLoader以后,它会接着调用loadClass方法,最终来到BootClassLoader中的loadClass方法中:

BootClassLoader.java:

@Override

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {

Class<?> clazz = findLoadedClass( className);

if (clazz == null) {

clazz = findClass(className);

}

return clazz;

}

在调用完findLoadClass以后,由于它已经是根加载器,如果它返回null直接就调用了findClass方法,到这里再回看上面的ClassLoader加载图,就已经到达了最顶端,开始向下查找,最后一级一级的到达BaseDexClassLoader中的findClass方法中,代码如下:

BaseDexClassLoader.java:

@Override

protected Class<?> findClass(String name) throws ClassNotFoundException {

List<Throwable> suppressedExceptions = new ArrayList<Throwable>();

//从pathList中去查找类

Class c = pathList.findClass(name, suppressedExceptions);

if (c == null) {

ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);

for (Throwable t : suppressedExceptions) {

cnfe.addSuppressed(t);

}

throw cnfe;

}

return c;

}

从现在开始进入到真正的加载类的过程了,上面全部都是在说ClassLoader按照双亲委托机制流程一层层的查找。

上面最重要的就是第二行代码,从pathList对象中去查找对应的类,查找不到会抛出异常,找到了直接返回。pathList是一个DexPathList对象,它是在BaseDexClassLoader的构造方法中完成了初始化:

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent, boolean isTrusted) {

super(parent);

this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

if (reporter != null) {

reportClassLoaderChain();

}

}

其中每个参数的意义:

dexPath:目标类所在的apk、dex或者jar文件的路径(SD卡也可以),这个路径可以是多个路径,使用分隔符:分开;

librarySearchPath:加载程序文件时需要用到的so库的路径;

parent:当前类加载器的父加载器。

在DexPathList的构造方法中,初始化了一个Element数组:

DexPathList.java:

DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) {

······

this.dexElements = makeDexElements( splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted)

}

DexPathList.java:

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;

//遍历所有dex文件(也可能是jar、apk或zip文件)

for (File file : files) {

if (file.isDirectory()) { //如果当前文件是一个文件夹

elements[elementsPos++] = new Element(file);

} else if (file.isFile()) { //如果是一个文件

String name = file.getName();

DexFile dex = null;

if (name.endsWith(DEX_SUFFIX)) { //如果是dex文件

try {

dex = loadDexFile(file, optimizedDirectory, loader, elements);

if (dex != null) {

elements[elementsPos++] = new Element(dex, null);

}

} catch (IOException suppressed) {

System.logE("Unable to load dex file: " + file, suppressed);

suppressedExceptions.add( suppressed);

}

} else { //如果是apk、jar、zip文件

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);

}

}

//把剩余的文件拷贝到elements数组中

if (elementsPos != elements.length) {

elements = Arrays.copyOf(elements, elementsPos);

}

return elements;

}

通过DexPathList的makeDexElements方法会将apk中的dex文件存放到dexElements数组当中,调用DexPathList的findClass方法,遍历dexElements数组,从数组中找到这个类然后加载。

BaseDexClassLoader小结:

①首先在BaseDexClassLoader构造方法内创建了PathDexList对象;

②然后在DexPathList构造方法中,通过makeDexElements()等方法经过一些列调用,把dex文件做优化再缓存到指定目录,如果是包含dex的apk/jar/zip等压缩文件的话,会先解压再优化缓存,最后得到DexFile对象;

③将DexFile对象包装成Element对象,然后加到Element[] 数组。

其实Android的类加载器(不管是PathClassLoader还是DexClassLoader),它们最后在加载文件时,都只认dex文件,而loadDexFile方法是加载dex文件的核心方法,它可以从jar、apk、zip中提取出dex。

接下来看看pathList的findClass方法是怎么实现的:

DexPathList.java:

private final Element[] dexElements;

//...

public Class findClass(String name, List<Throwable> suppressed) {

//遍历dexElements数组,拿到里面的dex

for (Element element : dexElements) {

DexFile dex = element.dexFile;

if (dex != null) {

Class clazz = dex.loadClassBinaryName( name, definingContext, suppressed);

if (clazz != null) {

return clazz;

}

}

}

if (dexElementsSuppressedExceptions != null){

suppressed.addAll(Arrays.asList( dexElementsSuppressedExceptions));

}

return null;

}

在findClass方法中遍历数组dexElements获取到里面的dex返回,dexElements里面保存的是apk中所有的dex。采用DexFile的loadClassBinaryName方法来加载class,是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件。

通过以上分析,可以发现整个类加载流程就是:

①类加载器BaseDexClassLoader先将dex文件解析放到pathList的dexElements里面

②加载类的时候从dexElements里面去遍历,看哪个dex里面有这个类就去加载,生成class对象

这个也是Tinker实现的原理,Tinker热修复就是将补丁dex插入到dexElements最前端,这样classLoader就会先加载补丁包中修复了bug的class文件,由于classLoader双亲委托,再加载原先有bug的class文件时,发现有一模一样的修复了bug的class被加载了,就会直接返回不会再去加载旧class文件,从而完成修复bug的目的。

到这里ClassLoader加载dex的流程基本上就完毕了,总结一下:

①加载一个类是通过双亲委托机制来实现的;

②第一次加载class,是通过BaseDexClassLoader中的findClass方法实现的;接着进入DexPathList中的findClass方法,内部通过遍历Element数组,从Element对象中去查找类;Element实际上是对Dex文件的包装,最终还是从dexfile去查找的class;

③一般app运行主要用到2个类加载器,一个是PathClassLoader:主要用于加载自己写的类;另一个是BootClassLoader:用于加载Framework中的类;

④热修复和插件化一般是利用DexClassLoader来实现;

⑤PathClassLoader和DexClassLoader其实都可以加载apk/jar/dex,区别是DexClassLoader可以指定optimizedDirectory,而PathClassLoader只能使用系统默认位置。但是在8.0 以后二者是没有区别的,只能使用系统默认的位置了。

Android 类加载机制相关推荐

  1. 【Java 虚拟机原理】Android 类加载机制 ( 双亲委派机制 | BootClassLoader | PathClassLoader | DexClassLoader )

    文章目录 一.Android 类加载机制 二.双亲委派机制 一.Android 类加载机制 Android 中的类加载 使用了 双亲委派 机制 , 如下图所示 : 在 Android 中提供了 333 ...

  2. android classloader异常,Android中ClassLoader类加载机制

    Android中apk的构建过程 构建apk 如图 所示,典型 Android 应用模块的构建流程通常依循下列步骤: 编译器将您的源代码转换成 DEX(Dalvik Executable) 文件(其中 ...

  3. android虚拟机加载机制,Android虚拟机与类加载机制

    JVM与Dalvik Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例.Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不 ...

  4. Android 虚拟机与类加载机制

    JVM与Dalvik Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例.Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不 ...

  5. 性能优化——Android热修复技术,类加载机制详解

    一.背景 热修复技术慢慢的成为Android开发必不可少的技术,也是成为一名高级程序员必不可少的技能之一.那么什么是热修复技术呢? 当app上线之后,发现了一个严重的bug,需要紧急修复,按照以往的惯 ...

  6. 类加载机制实现Android热修复

    本文通过类加载机制实现Android热修复,Demo实现的功能:检测服务器是否存在补丁,存在即下载补丁,安装补丁,重启APP生效.支持多个补丁包修复:如果已经下载了多个补丁包,重启app对补丁包进行排 ...

  7. 红橙Darren视频笔记 类加载机制(API28) 自己写个热修复 查看源码网站

    第一部分 类加载机制 一个Activity是如何被Android虚拟机找到的? 在之前的文章 红橙Darren视频笔记 自定义View总集篇(https://blog.csdn.net/u011109 ...

  8. v-html解析的相对地址img 显示不出来_还不懂java类加载机制的,建议看下这份阿里技术官总结的笔记!...

    作者:HandKnock 原文:https://blog.csdn.net/weixin_42547039 类加载机制 把class文件加载到内存,并对数据进行校验,准备,解析,初始化,形成可以被虚拟 ...

  9. 【胖虎的逆向之路】01——动态加载和类加载机制详解

    胖虎的逆向之路 01--动态加载和类加载机制详解 一.前言 二.类的加载器 1. 双亲委派模式 2. Android 中的类加载机制 1)Android 基本类的预加载 2)Android类加载器层级 ...

最新文章

  1. C# xml通过xslt转换为html输出
  2. guests mysql权限_MySQL降权运行之MySQL以Guests帐户启动设置方法
  3. LVS之NAT模型配置实验
  4. java语言执行过程_Java程序的运行过程(执行流程)分析
  5. 【网络安全工程师面试合集】—不要随便浏览一些奇怪的小网站哦
  6. python零基础能学吗-python 零基础该怎么学?
  7. ubuntu 系统网络突然网络已禁用
  8. 4.dialog 工具
  9. Atitit 提升开发效率 声明式编程范式 目录 1. 声明式编程体系树 1 1.1. 声明式(对比:指令式,,主要包括 函数式,逻辑式编程) 2 1.2. 声明式编程:表达与运行分离 3 1.3
  10. 关于NX8.5和VS2010环境配置后,执行DLL文件,报错:未加载图像,详细信息请参见日志文件
  11. 如何删除永中集成office 2009
  12. 如何使用C4D的反射通道创建一个金属外观?
  13. 全球高效能人士给青年的50个忠告(上)
  14. Creator 3.0中摄像机调整技巧: 1.旋转:Cmd+右键 移动:空格+右键 放缩:滚轮
  15. php自动关机代码,win7定时关机命令是什么
  16. 贵州小县城出身的“网约车品牌”,如何拓县出省、走向全国?
  17. ArcMap中Shapefile和dBASE文件的创建与管理
  18. 中控指纹考勤机使用实战
  19. Android开发——APP门户界面设计
  20. 计算机四级英语翻译,全国英语四级考试翻译特训题

热门文章

  1. F28335 SCI中断程序(不启用FIFO)
  2. 计算机网络简答题汇总,大学计算机基础简答题汇总(题库)
  3. 数字电路也需要注意电源滤波 (1117-3.3V芯片工频干扰一例)
  4. C和C++的字符串有什么区别
  5. 广告投放黑科技Cloak
  6. 宝付国际现身讲解电商跨界金融服务
  7. 2021年电工(初级)考试平台及电工(初级)
  8. 计算机测试题目及答案6,2015职称计算机《PowerPoint》章节习题及答案(6)
  9. 模拟前端的开关器件隔离度分析与优化
  10. oracle 11g复制数据库