Android中的dex、apk、ClassLoader详解

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生成DexFile
private 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,返回给ClassLoader
public 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的加载机制就完结了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值