PathClassLoader,DexClassLoader,mutidex,热修复,art ,dalvik总结

转载请注明出处:
PathClassLoader和DexClassLoader区别和各自在mutidex,热更新等的使用

地址:http://www.jianshu.com/p/54378566aa86

目录

一直对一些概念,知道的一知半解。也看过记住过,不过过后又忘了。这里做一些总结。可以分享,也可以自己以后查阅。源码什么的其实都不是很难,之前分析过mutidex的源码,也懒得贴了。这个博客只是对一些概念的总结。源码网上其实很多,可以如果需要,可以自己找找哈~。最后我也贴了一些。

Dalvik

当初设计Dalvik的时候,设定的是 安装app的时候只会把主dex封装成element放到PathclassLoader里类型是DexPathList的成员变量pathList中(即new PathclassLoader时,只传入了主dex的路径,根据这个路径生成pathList。关于android中的ClassLoader等细节知识看下面注解1)。DexPathList是什么呢?它内部含有DexFile数组。DexFile可以理解就是dex文件的封装,DexFile中持有dex的路径,以及一些方法比如loadClass(),其在dex真正加载到内存时调用;openDexFile()其在new DexFile()时调用,用于检查dex文件是不是正确,以及进行dex->odex的优化工作。

在app启动后,application#attachBaseContext()时,mutidex会检查是不是之前已经有了缓存的从dex 文件,如果没有,那么就从apk中解压出来那些从dex(耗时)文件。如果有,那么就使用这些缓存的从dex文件。注意缓存的是dex文件,不是DexFile。所以每次冷启动app时,不管用的是缓存的dex文件还是新解压的dex文件,都需要dex文件->new DexFile的过程,前面说了在new DexFile时,会调用DexFile#openDexFile(),这个方法会进行dex->odex的优化工作(这里的odex只是对dex进行了优化,并不是生成机器码),dex->odex是显著耗时的,所以即使用了dex文件缓存,使用mutidex还是会有显著耗时。有了dex文件后,就会调用MultiDex#installSecondaryDexes方法,在installSecondaryDexes()方法内,会进行dex文件->new DexFile->new Element(dexFile)->反射拿到PathClassLoader中类型为DexPathList的成员变量pathList,把前面的element放到DexPathList的dexElements数组中(这样hook一下PathClassLoader,之后项目中使用context.getClassLoader().loadClass(从dex中的类的全名)才不会报classnotFound异常。context.getClassLoader()得到的就是PathClassLoader)。在new DexFile时,会调用DexFile#openDexFile(),这个方法会进行dex->odex的优化工作,dex->odex是显著耗时的。

在app运行时,如果需要某个类,那个类如果还没有加到内存中,那么会去classLoader的DexPathList中的DexFile数组中去找,是不是有哪个dexFile含有对应的类,如果有,那么就调用对应的dexFile#loadClass()将对应的类文件加载到内存。如果没有找到,那么抛ClassNotFound异常。

ART

android5.0及以上(api对应21)的机子是采用的art系统。在这个系统上安装apk的时候(art系统吸取了之前的经验),会把apk中所有的dex翻译成机器码,预编译成多个oat文件(推测:之后会把这些oat文件放到classloader的dexpathlist中)。所以有了art系统,app启动的时候,就不需要在application初始化阶段执Multidex.install(this);。Dalvik系统运行时,如果需要加载某各类,需要从classloader中的dexlist中寻找对应的字节码文件路径来加载到内存,并翻译成机器码。而ART系统运行时,不需要再把字节码文件翻译成机器码的过程了,因为在安装时已经把所有dex都翻译成oat文件了。

但是,虽然在application初始化阶段不需要执行Multidex.install(this)。但是如果项目的总方法数超过了64k,还是需要在构建阶段把项目达成多个dex(一个dex文件不能超过64k方法数)。所以在构建apk时还是需要mutidex进行分包,5.0及以上机器运行时不再需要mutidex的安装方案。

在构建apk时,不管minsdk版本是多少,虽然都需要使用mutidex分包。但是系统还是做了一些优化。如果设置的minsdk<21,那么在分包时会做很多决策,这些决策决定哪些类放到主dex,哪些类放到从dex中。这是一个很耗时的决策,因为构建时间会很长。如果设置的minsdk>=21,那么意味着你的apk安装在android5.0及以上,也就是art系统上(所有的dex在apk安装时都会翻译成机器码,不需要mutidex.install()),也就无所谓哪个类在主dex,那个类在从dex了。所以android gradle 插件(比如3.0.0版本额)在构建工程时,发现minsdk>=21,就会启动pre-dexing功能(pre-dexing用于增量编译,即下次编译时,只会编译哪些改变的dex,没改变的复用。工作原理是:事先就把依赖里面的jar包装成dex,而不需要在构建的dex阶段去做这个事情,也不再需要计算文件放到哪个dex里的决策。),即把每一个模块\每一个依赖项都弄成一个dex,不再决策哪些类放到主dex了,哪些放到从dex了(在3.0.0gradle插件中做的更彻底,pre-dexing进化成了per-class dexing,每个类都是一个dex,这样如果我只改了一个类,那么只有一个dex需要变化,其他的dex都不用改变,更能加快构建时间)。这样能减少构建时间。这也为我们减少项目构建时间提供了一个思路:可以设置一个minSdkVersion 21的flavor,专门用于开发阶段的apk构建(apk需要运行在5.0及以上机器)。

    android {
defaultConfig {
    ...
    multiDexEnabled true
}
productFlavors {
    dev {
        // Enable pre-dexing to produce an APK that can be tested on
        // Android 5.0+ without the time-consuming DEX build processes.
        minSdkVersion 21
    }
    prod {
        // The actual minSdkVersion for the production version.
        minSdkVersion 14
    }
}
buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                             'proguard-rules.pro'
    }
}
}
dependencies {
    compile 'com.android.support:multidex:1.0.1'
 }   

注解1

java下的ClassLoader

Class clazz=Classloader.loadClass(类全名),其实就是通过一个类的全名,生成这个类的Class对象。loadClass()内部是先进行parent.loadClass()让父类先进行加载,如果加载不成功,再使用该classLoader加载(双亲委派)。然后,通过findClass(类全名)来加载得到Class对象。我们如果想自定义一个classLoader,那么就是重写findClass()方法。在findClass()中,我们拿到我们要加载的路径(可以是在构造方法中提供,也可以在findClass中定义好,也可以使用其他方式),然后拿到路径对应文件的数据流。然后使用classLoader定义好的defineClass(inputStream)来生成Class对象。所以真正生成class对象的部分,我们不需要管,调用一下defineClass(inputStream)就可以了。我们自定义一个classLoader,主要就是给它提供一个路径,然后类全名能找到这个路径下对应的.class文件,然后生成inputStream流。所以自定义一个Classloader也没什么,不同的ClassLoader并不是根据流生成class对象的方式不一样(都是一样的,通过defineClass(inputstream))。但是不同的ClassLoader即使加载的同一路径下的.class文件也是两个不同的Class对象。

android下的ClassLoader

android下的classloader和java下的有些不一样。android下的classloader虽然也满足双亲委派,但是findClass会抛异常(所以我们不能直接继承classloader来自定义classLoader)。所以都得使用BaseDexClassLoader,BaseDexClassLoader重写了findClass()。其实就是从DexPathList中找dexFile,然后使用dexFile.findClass(className)(native c++方法),所以这里的classloader加载的是dex,不是class字节码。

android经常使用的classLoader分为PathClassLoader和DexClassLoader。
PathClassLoader和DexClassLoader都是继承于BaseDexClassLoader。BaseDexClassLoader继承于ClassLoader。在BaseDexClassLoader中定义了DexPathList。不管是调用PathClassLoader或DexClassLoader的loadClass(其实真正调用的BaseDexClassLoader的findClass(classname)方法),其实都是从DexPathList找有没有对应类的路径(dexpath路径是在classLoader构造方法中传入的,然后形成DexPathList)。如果有,那么dexFile的findClass(classname)来加载生成对应的class对象。如果没有就报classnotfound异常。不过注意一下,DexClassLoader中的pathlist肯定不能访问PathClassLoade中的pathlist,因为这是两个实例。

PathClassLoader以及应用

PathClassLoader用来加载系统的和apk中的类,dexpath路径在构造方法中传入,且只能是系统apk路径。当然也可以像mutidex那样hookPathClassLoader,来改变其内部dexPathList成员变量中的元素,即便是hookPathClassLoader,也是改变系统PathClassLoader的dexPathList成员变量,而不是new 一个PathClassLoader,然后传入从dex的路径,因为PathClassLoader不允许外界传入非系统的路径。(包括从dex中的类。mutidex做的事情就是把dex一层层封装dex->odex->dexFile->element,然后把element放到PathClassLoader的pathList中,这样我们使用的时候context.getClassLoder()进行loadclass(类全名)才能加载到从dex中的类。context.getClassLoder()得到的是PathClassLoader。mutidex没有使用DexClassLoader的东西。同样的,qq空间的热修复方案,tink,都是hook的PathClassLoader。只是qq空间的热修复方案把补丁放到dexPathList中数组的最前面。tink是将补丁和原来的dex合并以后替换原来的dex)。为什么tink不使用DexClassLoader来加载补丁,而使用PathClassLoader。因为app在运行时,一般加载类时都是使用的PathClassLoader,比如context.getClassLoader对应的就是PathClassLoader。

DexClassLoader以及应用

DexClassLoader用来加载外部的类(.jar或.apk),外部类的dexpath路径在构造方法中传入,比如从网络下载的dex等,或插件化的apk(比较robust中加载补丁的时候就是使用的DexClassLoader来加载网络的dex。从网络下载到dex后。new 一个DexClassLoader(dexpath,outputName..),new的时候就把网络下载到的dex路径告诉DexClassLoader,DexClassLoader会将dex一步步封装,放到DexClassLoader中的pathList里面。dex放到DexClassLoader之后,使用DexClassLoader.loadClass(需要加载的patch类全名)得到补丁类的Class对象,然后class.newInstance()对应的实例。通过这个实例里的信息来找到要修补的是哪个类,然后找到这个类对应的Class对象,如果没有就使用PathClassLoader加载。然后将刚才new好的那个补丁实例赋值给这个Class对象中的一个类变量。这样,在app某一处调用该类的某个方法的时候,会先判断那个类变量是不是为null,不为null且确实是需要修复这个方法,那么就使用补丁实例中的逻辑,不再走原来方法的逻辑。其中每个class中添加一个类对象,每个方法添加一段拦截逻辑是在编译期操作字节码加载的。整体的robust的工作原理就是这样。可以看到robust没有hook操作PathClassLoader。只是正常使用了DexClassLoader。为什么robust不用hookPathClassLoader,因为它其实并不是替换类,而是新增加类(逻辑),只是表现形式上看是替换了老方法。所以以前的类并不需要被替换或者置后。)。为什么robust不用PathClassLoader加载dex,因为我们不能给PathClassLoader传入dex的路径,它必须接收系统的路径。

DexClassLoader使用注意事项

注意DexClassLoader(dexpath,outputPath,ClassLoader parent)在进行构造的时候,需要传入一个outputPath路径,它是dexpath路径下的dex解压优化后的路径。前面说了,dex->dexFile时,会执行opendex(),这里会将dex进行优化,生成odex。odex的路径就是这个outputPath。这样在正在加载的时候,其实是从这个outputPath路径下加载类文件的,而不是原来的dexPath。那么注意outputPath路径需要是app自己的缓存目录:File dexOutputDir = context.getDir("dex", 0);把这个路径给到outputPath就可以了。如果直接指定一个sdcard的缓存路径,那么会报错。PathClassLoader不需要我们管outputPath,传入null。一般我们也不会接触PathClassLoader的构造。详情可看这里
注意DexClassLoader中还要传入一个ClassLoader作为该DexClassLoader的父类。这样,我们使用DexClassLoader加载一个类时,根据双亲委派,会先让父类classloader进行加载。父类加载不了,再使用该DexClassLoader加载。一般我们使用getClassLoader()即app的context对应的classLoader:PathClassLoader作为DexClassLoader的父类。这样也就解释了为什么robust补丁的类和app中的相同类没有冲突,因为都是使用context对应的classLoader加载的那些。

参考文章:

Android类加载之PathClassLoader和DexClassLoader

【Android高级】DexClassloader和PathClassloader动态加载插件的实现

配置方法数超过 64K 的应用

MultiDex工作原理分析和优化方案

Dalvik,ART与ODEX相爱相生

推荐阅读更多精彩内容