Android类装载机制
前言
上两篇文章分析了资源的加载和进程,Activity启动相关的内容,这篇是Dex加载相关的内容了,本篇结束,我们也就可以开始对于一些热修复,插件化框架的实现剖析了。
Android中ClassLoader
上图为Android中ClassLoader的类图,与JVM不同,Dalvik的虚拟机不能用ClassCload直接加载.dex,Android从ClassLoader派生出了两个类:DexClassLoader
和PathClassLoader
。而这两个类就是我们加载dex文件的关键,这两者的区别是:
- DexClassLoader:可以加载jar/apk/dex,可以从SD卡中加载未安装的apk。
- PathClassLoader:要传入系统中apk的存放Path,所以只能加载已经安装的apk文件。
Dalvik虚拟机如同其他Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。而在Java标准的虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进制流。因此,我们常常利用这一点,在程序运行时手动加载Class,从而达到代码动态加载执行的目的。
只不过Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。
PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,其中的主要逻辑都是在BaseDexClassLoader完成的。
可以看出在加载类时首先判断这个类是否之前被加载过,如果有则直接返回,如果没有则首先尝试让parent ClassLoader进行加载,加载不成功才在自己的findClass中进行加载。这和java虚拟机中常见的双亲委派模型一致的,这种模型并不是一个强制性的约束模型,比如你可以继承ClassLoader复写loadCalss方法来破坏这种模型,只不过双亲委派模是一种被推荐的实现类加载器的方式,而且jdk1.2以后已经不提倡用户在覆盖loadClass方法,而应该把自己的类加载逻辑写到findClass中。
ClassLoader源码分析
- BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader { private final String originalPath; private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.originalPath = dexPath; //构造DexPahtList对象 this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //Class 查找在PathList中根据类名进行查找 Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } @Override protected URL findResource(String name) { return pathList.findResource(name); } @Override protected Enumeration<URL> findResources(String name) { return pathList.findResources(name); } @Override public String findLibrary(String name) { return pathList.findLibrary(name); } @Override protected synchronized Package getPackage(String name) { if (name != null && !name.isEmpty()) { Package pack = super.getPackage(name); if (pack == null) { pack = definePackage(name, "Unknown", "0.0", "Unknown", "Unknown", "0.0", "Unknown", null); } return pack; } return null; } @Override public String toString() { return getClass().getName() + "[" + originalPath + "]"; } }
通过源码可以看出,在BaseDexClassLoader的构造函数中创建了DexPathList实例,在BaseDexClassLoader中,对于类的查找和资源的查找,都是通过其中的DexPathList实例来进行的。对于类的等相关信息的查找是在DexPathList中实现的,接下来,我们看一下DexPathList的源码实现。
final class DexPathList { private static final String DEX_SUFFIX = ".dex"; private static final String JAR_SUFFIX = ".jar"; private static final String ZIP_SUFFIX = ".zip"; private static final String APK_SUFFIX = ".apk"; /** class definition context */ private final ClassLoader definingContext; //Dex,Resource元素 private final Element[] dexElements; //Native库的地址路径 private final File[] nativeLibraryDirectories; public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } if (dexPath == null) { throw new NullPointerException("dexPath == null"); } if (optimizedDirectory != null) { if (!optimizedDirectory.exists()) { throw new IllegalArgumentException( "optimizedDirectory doesn't exist: " + optimizedDirectory); } if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) { throw new IllegalArgumentException( "optimizedDirectory not readable/writable: " + optimizedDirectory); } } this.definingContext = definingContext; this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory); this.nativeLibraryDirectories = splitLibraryPath(libraryPath); } }
在构造函数中,根据dexPath,构建一个DexElement数组,在后面对于类的查找就会在该数组中进行查找。
- makeDexElements
/** * 根据给予的地址构建一个Element数组 */ private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) { ArrayList<Element> elements = new ArrayList<Element>(); /* * Open all files and load the (direct or contained) dex files * up front. */ for (File file : files) { ZipFile zip = null; DexFile dex = null; String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ex) { System.logE("Unable to load dex file: " + file, ex); } } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) { try { zip = new ZipFile(file); } catch (IOException ex) { } try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ignored) { } } else { } if ((zip != null) || (dex != null)) { elements.add(new Element(file, zip, dex)); } } return elements.toArray(new Element[elements.size()]); }
根据传递的路径,然后解析路径下的内容,根据dex,zip等构建Element实例,然后将这个实例添加到Element数组之中。
- loadDexFile
/** * 构造一个DexFile对象 */ private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException { if (optimizedDirectory == null) { return new DexFile(file); } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); return DexFile.loadDex(file.getPath(), optimizedPath, 0); } }
Element数据结构
static class Element { public final File file; public final ZipFile zipFile; public final DexFile dexFile; public Element(File file, ZipFile zipFile, DexFile dexFile) { this.file = file; this.zipFile = zipFile; this.dexFile = dexFile; } }
- 类的查找
/** * 类的查找,从DexElements中,逐个DexFile中查找,如果找到, * 就加载这个类,然后返回 */ public Class findClass(String name) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext); if (clazz != null) { return clazz; } } } return null; }
根据传递的目录,加载相应的dex,apk来构建层Element实例。
- ART和Dalvik
Android Runtime(缩写为ART),在Android 5.0及后续Android版本中作为正式的运行时库取代了以往的Dalvik虚拟机。ART能够把应用程序的字节码转换为机器码,是Android所使用的一种新的虚拟机。它与Dalvik的主要不同在于:Dalvik采用的是JIT技术,字节码都需要通过即时编译器转换为机器码,这会拖慢应用的运行效率,而ART采用Ahead-of-time(AOT)技术,应用在第一次安装的时候,字节码就会预先编译成机器码,这个过程叫做预编译。ART同时也改善了性能、垃圾回收(Garbage Collection)、应用程序出错以及性能分析。但是请注意,运行时内存占用空间较少同样意味着编译二进制需要更高的存储。
ART模式相比原来的Dalvik,会在安装APK的时候,使用Android系统自带的dex2oat工具把APK里面的.dex文件转化成OAT文件,OAT文件是一种Android私有ELF文件格式,它不仅包含有从DEX文件翻译而来的本地机器指令,还包含有原来的DEX文件内容。这使得我们无需重新编译原有的APK就可以让它正常地在ART里面运行,也就是我们不需要改变原来的APK编程接口。ART模式的系统里,同样存在DexClassLoader类,包名路径也没变,只不过它的具体实现与原来的有所不同,但是接口是一致的。实际上,ART运行时就是和Dalvik虚拟机一样,实现了一套完全兼容Java虚拟机的接口。
- dexopt还是dexoat
在安装的优化过程,dexopt函数会启动一个子进程来执行优化,同时会根据目前系统使用的是Dalvik虚拟机还是ART来决定将apk转换层何种格式,如果转换层odex格式,则调用/system/bin/dexopt文件来执行转化,如果转换层ART的oat格式,则调用/system/bin/dex2oat来执行转换。
则dexopt和dexoat中会执行一些优化操作,这个优化操作也会影响到我们后续类的动态加载。但由于上层接口一致,因此,作为开发者无需关心安装时具体的优化方式。
参考资料
Dexopt优化和验证Dalvik (Dalvik Optimization and Verification With dexopt)