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对象的父ClassLoader
    private 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 的 ClassLoader
            final 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 对象:

	@Override
    public 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,并指定其父加载器是 BootClassLoader
        return 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 文件的路径 dexPath
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
        ...
    }
    
    @Override
    protected 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 BaseDexClassLoader
        this.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[],查找类的工作会再次转交给 Element
        for (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详解

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

爬楼梯(力扣LeetCode)动态规划

爬楼梯 题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a; 输入&#xff1a;n 2 输出&#xff1a;2 解释&#xff1a;有两种方法可以爬到楼顶。 1 阶 1 阶2 阶 示…

树状数组专题

折叠 区间修改&#xff0c;区间查询&#xff0c;这一类题通常都可以使用线段树解决&#xff0c;但对于此题&#xff0c;树状数组同样可以&#xff0c;而且常数较小&#xff0c;代码简单。 思路&#xff1a; 考虑使用树状数组去维护差分数组&#xff0c;即对于 a i a_i ai​,我们…

找不到vcomp120.dll该如何修复?vcomp120.dll丢失的5个可行解决方法

本文将对vcomp120.dll文件的丢失原因进行详细分析&#xff0c;并提供五个有效的修复方法。同时&#xff0c;本文还将深入介绍vcomp120.dll文件的作用及其在程序运行中的重要性。 一、vcomp120.dll文件丢失原因 操作系统损坏&#xff1a;由于病毒感染、系统错误等原因&#xf…

linux复习笔记04(小滴课堂)

软件安装rpm方式介绍&#xff1a; 先去挂载光盘&#xff1a; 要确保这是已连接状态。 我们查看到已经挂载成功了。 进到这个目录下。 我们可以看到这有很多rpm软件包。 man rpm: 可以看到很多参数&#xff0c;但是我们不需要全部掌握。 举例&#xff1a; 这就是告诉我们需要安…

docker (简介、dcoker详细安装步骤)- day01

一、 为什么出现 Docker是基于Go语言实现的云开源项目。 Docker的主要目标是“Build&#xff0c;Ship and Run Any App,Anywhere”&#xff0c;也就是通过对应用组件的封装、分发、部署、运行等生命周期的管理&#xff0c;使用户的APP&#xff08;可以是一个WEB应用或数据库应…

WiFi的CSMA/CA竞争窗口流程简述

1、若站点最初有数据要发送&#xff08;不是发送不成功再进行重传的那种&#xff09;&#xff0c;且检测到信道空闲&#xff0c;在等待DIFS后&#xff0c;就发送整个数据帧。 2、否则&#xff0c;站点执行退避算法。一旦检测到信道忙&#xff0c;就冻结退避计时器。只要信道空…

深度学习之循环神经网络

视频链接&#xff1a;6 循环神经网络_哔哩哔哩_bilibili 给神经网络增加记忆能力 对全连接层而言&#xff0c;输入输出的维数固定&#xff0c;因此无法处理序列信息 对卷积层而言&#xff0c;因为卷积核的参数是共享的&#xff0c;所以卷积操作与序列的长度无关。但是因为卷积…

北塞浦路斯土耳其共和国关于成立欧洲数字股票交易所企业交流会

在地中海的温暖波涛中&#xff0c;北塞浦路斯土耳其共和国这个古老而充满活力的国家正成为全球关注的焦点。2023年11月22日至11月24日&#xff0c;为期三天的北塞浦路斯土耳其共和国关于成立欧洲数字股票交易所企业交流会隆重谢幕&#xff0c;北塞副总统&#xff0c;经济部长&a…

【学习记录】从0开始的Linux学习之旅——驱动模块编译与加载

一、概述 Linux操作系统通常是基于Linux内核&#xff0c;并结合GNU项目中的工具和应用程序而成。Linux操作系统支持多用户、多任务和多线程&#xff0c;具有强大的网络功能和良好的兼容性。本文主要讲述如何编译及加载linux驱动模块。 二、概念及原理 应用程序通过系统调用与内…

STK Components 二次开发-创建地面站

1.地面站只需要知道地面站的经纬高。 // Define the location of the facility using cartographic coordinates.var location new Cartographic(Trig.DegreesToRadians(-75.596766667), Trig.DegreesToRadians(40.0388333333), 0.0); 2.创建地面站 创建方式和卫星一样生成对…

【开源】基于Vue+SpringBoot的食品生产管理系统

项目编号&#xff1a; S 044 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S044&#xff0c;文末获取源码。} 项目编号&#xff1a;S044&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 加工厂管理模块2.2 客户管理模块2.3…

UE5人物残影学习(材质实现)

学习视频 UE4简单的材质球残影人教学&#xff0c;你学会了吗&#xff01;_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1rY411q7Yb/?spm_id_from333.788.top_right_bar_window_history.content.click 结果预览 1.创建残值&#xff0c;混合模式勾选半透明 “混合模…

Qt4利用MVC开发曲线数据编辑器

目录 1 需求 2 开发流程 1 搭建框架 2 构造函数 3 打开工程 4 实现应用程序参数加载 5 QCustomPlot和TableView的联动 6 数据的可视化修改 7 列表点击事件事先键盘控制 8 表格实现复制&#xff0c;粘贴&#xff0c;删除等一系列功能 9 曲线实现自适应范围和统一范围…

MyBatis插入操作返回主键报错问题记录

一开始用直接传参数的方法写的插入操作 StudentMapper.java接口 Integer insertStudent(Param("sname") String name,Param("sage") int age); 然后在网上搜了返回主键的方法 StudentMapper.xml: <insert id"insertStudent" useGenerat…

CAN通信协议

CAN 文章目录 CAN前言一、什么是CAN二、CAN的用途三、CAN协议简解1.can的通信过程1.1空闲状态1.2.起始状态1.3 仲裁机制1.4 位时序 前言 前面学了232、485、IIC、SPI等通信协议&#xff0c;还有一个强大的协议CAN&#xff0c;值得记录一下 一、什么是CAN CAN是Controller Ar…

爬取极简壁纸

js反编译的代码需要解密之类的&#xff0c;直接给我干蒙圈了&#xff0c;借助selenium可以直接获取到调式工具中的源码&#xff0c;可以获取渲染后的链接&#xff0c;然后将链接交给下载函数&#xff08;使用异步提高效率&#xff09;即可。 后续学习完js反编译的话&#xff0…

Unity-链接MySql8.0

链接MySql8.0 1.准备dll 一、找到l18N相关的dll 这里给出一个参考地址 D:\Unity\2020.3.48f1c1\Editor\Data\MonoBleedingEdge\lib\mono\unityjit在里面找到如下图的四个dll 二、下载数据库链接dll https://downloads.mysql.com/archives/c-net/在这里搜索历史版本(Archiv…

AIGC ChatGPT4总结Linux Shell命令集合

在Linux中,Shell命令的数量非常庞大,因为Linux提供了各种各样的命令来处理系统任务。这些命令包括GNU核心工具集、系统命令、shell内置命令以及通过安装获得的第三方应用程序命令。以下是一些常见的Linux命令分类及其示例,但请注意,这不是一个全面的列表,因为列出所有命令…

linux复习笔记05(小滴课堂)

hell脚本与crontab定时器的运用 查看状态&#xff1a; 关闭服务&#xff1a; 开启服务&#xff1a; 重启服务&#xff1a; crontab定时器的使用&#xff1a; 我们可以看到没有任何任务。 编辑&#xff1a; 我们可以看到这个任务了。 删除所有任务&#xff1a; 这代表着每分钟…

【NeRF】3、MobileR2L | 移动端实时的神经光场(CVPR2023)

论文&#xff1a;Real-Time Neural Light Field on Mobile Devices 代码&#xff1a;https://github.com/snap-research/MobileR2L 出处&#xff1a;CVPR2023 贡献&#xff1a; 设计了一套移动端实时的 R2L 网络结构 MobileR2L&#xff0c;在 iphone13 上渲染一张 1008x756…