Android中的dex、apk、ClassLoader详解

来源:互联网 发布:python循环读取文件 编辑:程序博客网 时间:2024/05/22 13:26

dalvik加载、运行过程

我们编写java代码都是.java格式的,但是jvm并不能识别.java文件,它只能加载、执行.class文件,所以我们要通过javac命令将.java文件编译成.class文件,然后通过java命令运行.class文件。其实,如果用C或者Python编写的程序正确转换成.class文件后,java虚拟机也是可以识别运行的。

dalvik与jvm差不多,区别就是dalvik只能加载、运行.dex文件(至于如何识别、运行,后面会讲到)。我们的Android程序也是用java编写的,生成的也是.java文件,所以需要把.java文件转换成.dex文件,dalvik才能执行。IDE编译、打包的过程,就是将.java文件转换成.dex文件的过程,我们可以简单看一下编译过程,加深理解。

下面是官方介绍的打包流程图:

这里写图片描述

总结一下,主要就是这几步:

1、根据res目录下的资源文件、AndroidManifest.xml生成R.java文件;
2、处理aidl,生成对应的java文件,如果没有aidl,则跳过;
3、将前两步生成的java文件和src目录下源码一起编译成class文件;
4、通过class文件生成成dex文件;
5、将资源文件和dex文件一起打包,生成初始apk;
6、对初始apk签名 ;

由此可见,项目编译后,主要结晶就是dex文件。apk的安装过程,就是把apk解压成第4步中的dex文件和原始资源文件(比如图片),运行过程就是dalvik加载、运行dex文件的过程。这里有两个过程,一个是加载,一个是运行,它们又是怎样运作的呢?

dex文件的加载是通过DexClassLoader、PathClassLoader等类来完成的,下面将会从源码角度对这个过程详细分析,这也是热修复技术、插件化技术的核心。

dex的运行就涉及到比较底层的东西了,本文只做一定介绍,了解一下dex文件的大概。

Dex文件

通过命令“javac HelloWorld.java”可以生成HelloWorld.class文件。
再通过命令“dx –dex –output=HelloWorld.dex HelloWorld.class”就会生成HelloWorld.dex文件了。

我们通过十六进制文本编辑器打开HelloWorld.dex文件,如下图:

这里写图片描述

注意看下面的“Name、Value”,这就是dex文件的标准格式。就像通信协议一样,dalvik虚拟机读到什么内容,就按照预定好的协议执行,这就是dalvik运行dex文件的过程。

ClassLoader

先把我们需要分析的类列出来,捋一捋继承关系、类的主要作用。

ClassLoader
所有XXXClassLoader的基类,负责加载apk/dex/jar文件;

BootClassLoader
继承自ClassLoader,负责加载Android系统类库,我们不会用到;

BaseDexClassLoader
继承自ClassLoader,看名字就知道是对类加载的抽象;

PathClassLoader
继承自BaseDexClassLoader,负责加载宿主apk/dex/jar文件;

DexClassLoader
继承自BaseDexClassLoader,这个比较灵活,每个参数都可以自定义,我们一般用这个来加载自定义的apk/dex/jar文件;

DexPathList
这个类有两个作用:
① 把dex文件解压到宿主程序的私有目录中,因为jvm只能运行宿主程序的dex文件;
② 通过apk/dex/jar文件生成Element[]数组,方便ClassLoader使用;

由于BootClassLoader是加载系统类库的,我们就不分析了。我们主要分析PathClassLoader加载apk中的dex文件,分析完这个过程,自定义一个ClassLoader来加载自定义dex文件就不成问题了。

1、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);    }}// 参数dexPath:待加载的apk/dex/jar文件路径;// 参数optimizedDirectory:dex的输出路径,将apk/dex/jar解压出dex文件,复制到指定路径,用于dalvik运行// 参数librarySearchPath:加载时候需要用到的lib库,这个一般不用,可以传入Null// 参数parent:指定父加载器public BaseDexClassLoader(String dexPath, File optimizedDirectory,                String libraryPath, ClassLoader parent) {    super(parent);    this.originalPath = dexPath;    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);}

这里有两个“疑点”,一是PathClassLoader是无法指定optimizedDirectory参数的,也就是说,无法保证解压出来的dex文件在宿主程序中,dalvik就无法运行。另一个就是new一个对象还必须传一个父类对象作为参数,为什么呢?下面分析loadClass()方法时再说明。

我们再看一下加载的方法,加载方法在基类ClassLoader中:

// 通过类名加载protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {    // 在已加载的类中查找    Class<?> clazz = findLoadedClass(className);    if (clazz == null) {        ClassNotFoundException suppressed = null;        try {            // 如果没有,就让parent去加载            clazz = parent.loadClass(className, false);        } catch (ClassNotFoundException e) {            suppressed = e;        }        if (clazz == null) {            try {                // 如果parent也没有加载到,就自己加载                clazz = findClass(className);            } catch (ClassNotFoundException e) {                e.addSuppressed(suppressed);                throw e;            }        }    }    return clazz;}

逻辑是这样的:

1、先去已加载的列表中查找,如果有(已经加载过),就直接返回;如果没有,就让parent去加载;
2、父类仍旧是调用基类ClassLoader的loadClass()方法(如果parent没有重写该方法,一般parent都会传系统自带的类,甚至是基类,所以基本不存在被重写的情况),等于是把第一步重复一次;
3、一直找到最顶层的parent,如果顶层parent在已加载列表中还是没有找到,就会调用findClass()进行加载,并返回;
4、通过parent一层一层地返回,如果最终还是没有(所有parent都没有加载到),就自己进行加载;

这样设计的逻辑就是防止多次加载,一个类只永远只会被加载一次。

另外要注意的是,只有“PackageName + ClassName + 加载它的ClassLoader”这三个元素一致,才认为它是同一个类。所以,指定系统的parent能最大限度地保证类的一致性。

上面的逻辑中可以看到,如果没有加载过,就会调用findClass()方法进行加载,BaseDexClassLoader重载了这个方法:

@Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        Class clazz = pathList.findClass(name);        if (clazz == null) {            throw new ClassNotFoundException(name);        }        return clazz;}

最终调用了DexPathList的findClass()方法,那我们分析一下DexPathList的主要逻辑:

// 构造方法,BaseDexClassLoader的构造方法中会new出DexPathList实例public DexPathList(ClassLoader definingContext, String dexPath,            String libraryPath, File optimizedDirectory) {    ……    // 通过dexPath路径下的apk/jar/dex文件解压到optimizedDirectory目录中,并生成Elements[]数组    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);}private static Element[] makeDexElements(ArrayList<File> files,            File optimizedDirectory) {    ArrayList<Element> elements = new ArrayList<Element>();    // 遍历该路径下的文件    for (File file : files) {        ZipFile zip = null;        DexFile dex = null;        String name = file.getName();        if (name.endsWith(DEX_SUFFIX)) {            // 如果是dex文件,就加载该文件            dex = loadDexFile(file, optimizedDirectory);        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)                    || name.endsWith(ZIP_SUFFIX)) {            // 如果是压缩文件,就生成ZipFile            zip = new ZipFile(file);        }         ……        if ((zip != null) || (dex != null)) {            // 通过上面生成的DexFile或ZipFile,生成Element对象,添加到List中            elements.add(new Element(file, zip, dex));        }    }    // List转换成数组返回    return elements.toArray(new Element[elements.size()]);}// 通过File生成DexFileprivate static DexFile loadDexFile(File file, File optimizedDirectory)            throws IOException {    if (optimizedDirectory == null) {        // 如果输出路径为空,就会使用默认路径(当前File所在路径)        return new DexFile(file);    } else {        // 生成解压路径        String optimizedPath = optimizedPathFor(file, optimizedDirectory);        return DexFile.loadDex(file.getPath(), optimizedPath, 0);    }}/** * Converts a dex/jar file path and an output directory to an * output file path for an associated optimized dex file. */private static String optimizedPathFor(File path,            File optimizedDirectory) {    String fileName = path.getName();    if (!fileName.endsWith(DEX_SUFFIX)) {        int lastDot = fileName.lastIndexOf(".");        if (lastDot < 0) {            fileName += DEX_SUFFIX;        } else {            StringBuilder sb = new StringBuilder(lastDot + 4);            sb.append(fileName, 0, lastDot);            sb.append(DEX_SUFFIX);            fileName = sb.toString();        }    }    File result = new File(optimizedDirectory, fileName);    return result.getPath();}// 根据name查找dex,将其转换成Class,返回给ClassLoaderpublic Class findClass(String name) {    // 遍历Element[]数组    for (Element element : dexElements) {        DexFile dex = element.dexFile;        if (dex != null) {            // 通过name尝试加载Class            Class clazz = dex.loadClassBinaryName(name, definingContext);            if (clazz != null) {                // 如果加载成功,就返回                return clazz;            }        }    }    return null;}

分析到这里ClassLoader的加载机制就完结了。

1 0