前言
上两篇文章分析了资源的加载和进程,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)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。