Android 虚拟机与类加载机制

news2024/11/28 14:31:26

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详解

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

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

相关文章

11【保姆级】-GO语言的struct

11【保姆级】-GO语言的struct 一、Go的面向对象1.1 说明 二、结构体2.1 结构体和结构体变量(实例)的区别和联系2.2 声明结构体 和 细节说明2.3 结构体在内存中的布局2.4 创建结构体和访问结构体的四种方式 在学习GO语言时&#xff1a; 先建立整体框架&#xff0c;然后再去抠细节…

王者荣耀小游戏

第一步是创建项目 项目名自拟 第二部创建个包名 来规范class 然后是创建类 GameFrame 运行类 package com.sxt; package com.sxt;import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.…

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

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

BC77 简单计算器(牛客)

#include <stdio.h> int main() {double a, b, d;//用来接收浮点数char c;//用来接受符号scanf("%lf %c %lf", &a, &c, &b);if (c || c - || c * || c /)//判断输入的运算符号不包括在&#xff08;、-、*、/&#xff09;范围内{switch (c)//根…

Ps:使用钢笔工具绘制自由路径的技巧

只有熟练掌握使用钢笔工具绘制自由路径的技巧&#xff0c;才能快速完成复杂形状的创建以及精准抠图等工作。 钢笔工具是 Photoshop 中绘制路径的主要工具。无论是直线路径还是曲线路径&#xff0c;钢笔工具都能够提供高度的控制和精确度。 ◆ ◆ ◆ 绘制直线路径 绘制直线路径…

4. 标准 IO 库

4. 标准 IO 库 1. 标准 IO 简介2. FILE 指针3. 标准输入、标准输出和标准错误4. fopen() 和 flose()5. fread() 和 fwrite()6. fseek 定位7. 检查或复位状态7.1 feof()7.2 ferrof()7.3 clearerr() 8. 格式化 IO8.1 格式化输出8. 2 格式化输入 9. IO 缓冲9.1 文件 IO 的内核缓冲…

【古诗生成AI实战】之五——加载模型进行古诗生成

回顾上一篇博客&#xff0c;我们已经成功地训练了我们的模型&#xff0c;并将其保存下来。这是一个重要的里程碑&#xff0c;因为训练好的模型是我们进行文本生成的基础。 现在&#xff0c;接下来的步骤是加载这个训练好的模型&#xff0c;然后使用它来生成古诗。 本章的内容属…

AI4S Cup学习赛-中枢神经系统药物研发:药物筛选与优化

赛题介绍 链接&#xff1a;Bohrium 案例广场 (dp.tech) 中枢神经系统类疾病长期以来存在着重要的临床未满足需求。据统计&#xff0c;在当前人口老龄化趋势下&#xff0c;阿兹海默&#xff08;AD&#xff09;、帕金森病&#xff08;PD&#xff09;等神经退行性疾病和脑癌、中…

ATK-ESP8266 WIFI模块串口通信通用实现方案

ATK-ESP8266 WIFI模块是一种常用的无线模块&#xff0c;它可以通过串口与外部设备进行通信&#xff0c;实现数据的收发和控制。本文将介绍一种通用的实现方案&#xff0c;帮助您在项目中使用ATK-ESP8266 WIFI模块进行串口通信。 【方案概述】 这个通用实现方案涵盖了ATK-ESP82…

探索 Rollup:简化你的前端构建流程

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

SCI一区级 | Matlab实现GWO-CNN-LSTM-selfAttention多变量多步时间序列预测

SCI一区级 | Matlab实现GWO-CNN-LSTM-selfAttention多变量多步时间序列预测 目录 SCI一区级 | Matlab实现GWO-CNN-LSTM-selfAttention多变量多步时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现GWO-CNN-LSTM-selfAttention灰狼算法优化卷积长短…

Java8实战-总结49

Java8实战-总结49 CompletableFuture&#xff1a;组合式异步编程对多个异步任务进行流水线操作构造同步和异步操作将两个 CompletableFuture 对象整合起来&#xff0c;无论它们是否存在依赖 CompletableFuture&#xff1a;组合式异步编程 对多个异步任务进行流水线操作 构造同…

jdk17安装全方位手把手安装教程 / 已有jdk8了,安装JDK17后如何配置环境变量 / 多个不同版本的JDK,如何配置环境变量?

&#x1f9f8;欢迎来到dream_ready的博客&#xff0c;&#x1f4dc;相信您对博主首页也很感兴趣o (ˉ▽ˉ&#xff1b;) 学生邮箱白嫖/免费安装JetBrains全家桶(IDEA/pycharm等) —— 保姆级教程 目录 1、下载jdk17 2、安装jdk17 3、配置环境变量 -> 电脑无其他jdk 4、…

Windows安装Python环境(V3.6)

文章目录 一&#xff1a;进入网址&#xff1a;https://www.python.org/downloads/ 二&#xff1a;执行安装包 默认C盘&#xff0c;选择自定义安装目录 记得勾选add python path 下面文件夹最好不要有 . 等特殊符号 可以创建 python36 如果安装失败Option可以选默认的&#x…

IDEA DeBug

文章目录 01_Debug简介和意义02_IDEA中的Debug步骤03_跳转到当前代码执行的行04_步过调试的使用05_步入调试的使用06_强制步入调试的使用07_步出调试的使用08_回退断点调试的使用09_运行到光标处10_计算表达式11_条件断点12_多线程调试 01_Debug简介和意义 什么是程序DeBug&am…

PCIE链路训练-状态机描述4

Recovery Recovery.RcvrLock &#xff08;1&#xff09;如果link是在8.0GT/s或以上的速率工作&#xff0c;那么rx只会认为当前lane获得Block alignment之后收到的TS0&#xff0c;TS1&#xff0c;TS2是有效的。如果进入当前状态是从L1或recovery.speed或L0s&#xff0c;获取Blo…

STK Components 二次开发-地面站传感器

上一篇我们说了创建地面站&#xff0c;那么这次我们在地面站添加一些特效。 1. 创建地面站 var locationPoint1 new PointCartographic(m_earth, new Cartographic(Trig.DegreesToRadians(117.17066), Trig.DegreesToRadians(31.84056), 240.359)); m_facility new Platfor…

大型网站系统架构演化(Web)

大型网站系统架构演化 大型网站系统架构演化需要关注的维度涉及的技术演进过程单体架构垂直架构使用缓存改善网站性能缓存与数据库的数据一致性问题缓存技术对比Redis分布式存储方案Redis集群切片的常见方式Redis数据类型Redis 淘汰算法 大型网站系统架构演化 需要关注的维度 …

【虚拟机Ubuntu 18.04配置网络】

虚拟机Ubuntu 18.04配置网络 1.配置网络连接方式,查看自己网关 2.修改主机名 3.修改系统配置1.配置网络连接方式,查看自己网关 选择虚拟机镜像设置网络连接模式,可以选择桥接或者NAT连接(我这里选择是NAT连接) 确定自己网关&#xff0c;可以在虚拟机 -》 编辑 -》虚拟网络编…

Android 10.0 mtp模式下连接pc后显示的文件夹禁止删除copy重命名功能实现

1.前言 在10.0的系统开发中,usb连接pc端的时候有好几种模式,在做otg连接pc端的时候,改成mtp模式的时候,在pc端可以看到产品设备 的显示的文件夹的内容,对于产品设备里面的文件在pc端禁止做删除重命名拷贝等操作功能的实现 2.mtp模式下连接pc后显示的文件夹禁止删除copy重命…