在Java所编写的应用程序里,实质上都是ClassLoader来负责加载类的。有隐式加载类:如new一个实例对象,会自动触发ClassLoader来加载该类;有显示加载类:如直接调用ClassLoader的loadClass()方法来加载。实现热修复的关键技术之一就是ClassLoader,我们通过自定义ClassLoader来动态加载类,以实现热修复的目的。JVM的ClassLoader采用的是双亲委派模型,从上往下有BootstrapClassLoader、ExtClassLoader、AppClassLoader,它们都有不同的职责。Android平台同样也采用了双亲委派模型,但是Android运行的是dex文件,JVM运行的是class文件,所以Android平台没有采用JVM那一套机制,而是自己重新实现了自己特有的ClassLoader。我们必须先了解清楚Android平台的ClassLoader是怎么一回事,才能着手进行热修复的实践。
1. Android的ClassLoader层次结构
从图中可以看到,Android中实际用到的ClassLoader是PathClassLoader和DexClassLoader,它们都继承自BaseDexClassLoader,而BaseDexClassLoader继承自java.lang.ClassLoader。注意这里它们都是父子继承关系,以前我们讲到ClassLoader的双亲委派模型时,是通过组合的方式来实现的。
我们来看个具体的例子,打印出不同类的ClassLoader是什么:
String str = new String("abc");
System.out.println(str.getClass().getClassLoader());
ClassLoader classLoader = MainActivity.this.getClassLoader();
do {
System.out.println(classLoader);
classLoader = classLoader.getParent();
} while (classLoader != null);
System.out.println(Button.class.getClassLoader());
截取部分执行结果如下:
java.lang.BootClassLoader@32a45faa
dalvik.system.PathClassLoader......
java.lang.BootClassLoader@32a45faa
java.lang.BootClassLoader@32a45faa
可以看到String类是由一个名为BootClassLoader的启动类加载的,而由我们自己编写的类MainActivity则是由PathClassLoader来加载的,PathClassLoader的双亲直接为启动类BootClassLoader,Button类也是由BootClassLoader来加载的。
Android中几个主要的ClassLoader及其作用为:
- BootClassLoader:顶层的启动类加载器,Android SDK里的类都是直接由它加载的;
- PathClassLoader:Android系统默认的类加载器,只能加载系统中已经安装过的apk;
- DexClassLoader:可以加载任何路径的apk/dex/jar,所以要实现Android的动态加载,一般采用DexClassLoader来实现;
2. PathClassLoader
PathClassLoader有两个构造函数:
public PathClassLoader(String dexPath, ClassLoader parent)
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent)
其几个参数的含义为:
dexPath:dex/apk等文件路径列表,多个文件用":"分隔
librarySearchPath:lib库的搜索路径,多个以":"分隔
parent:父类加载器
前面的例子中,我们看看控制台里打印出的完整的PathClassLoader信息:
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.bwton.hjydemo-2/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
从中可以看到,dexPath值为"/data/app/com.bwton.hjydemo-2/base.apk",这是我们的apk实际安装路径,librarySearchPath的值为"/vendor/lib, /system/lib"。
通常情况下,调用Context.getClassLoader()方法获取到的就是PathClassLoader。
3. DexClassLoader
DexClassLoader的构造函数为:
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
与PathClassLoader相比,DexClassLoader的构造函数只多了一个参数optimizedDirectory。
optimizedDirectory:经过优化后的dex文件(odex)输出目录
4. PathClassLoader与DexClassLoader的区别
从网上找了下这2个类的源码,大概如下:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String, String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
从源码中可以看到,这两个类及其相似,两者都是继承自BaseDexClassLoader,唯一的差别是DexClassLoader多了一个参数optimizedDirectory,而PathClassLoader里该值必定为null。由此我们可以判断,必定是这个optimizedDirectory搞的鬼,所以还得看看BaseDexClassLoader里的源码。
先看看构造函数:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
发现主要是生成了一个DexPathList对象,其他什么都没做,我们再来看看DexPathList是个什么东西:
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
//检查classLoader不能为空
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
//检查jar/apk等文件路径不能为空
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
//判断dex优化后的文件存储路径是否为空,前面介绍到PathClassLoader的optimizedDirectory为null,
//而DexClassLoader多了这个参数,所以它俩的差别就是在这里体现的
if (optimizedDirectory != null) {
//optimizedDirectory目录必须事先创建好
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
//optimizedDirectory目录必须能读写
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//根据传入的jar/apk文件路径,创建dex element
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
里面的关键代码在makeDexElements,字面意思是根据jar/apk路径创建出dex element,我们继续看该方法:
//files表示传入的所有jar/apk文件列表
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
//遍历所有的文件,然后逐一生成DexFile
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
//如果是以.dex结尾的dex文件,可以直接加载
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)) {
//如果是.apk、.jar、.zip文件,则采用loadDexFile方法加载
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if the
* zip file turns out to be resource-only (that is, no classes.dex file in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only useful for running libcore tests.
//如果是个文件目录
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
从代码里可以看到,支持加载的文件类型有:.dex、.jar、.zip、.apk共4种,其他文件类型都不支持,最终进行加载动作的都是loadDexFile()方法,所以不管是jar包也好,apk包也罢,其内部加载时都会返回DexFile,再继续跟进去看看该方法实现:
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
//如果dex优化后的存储路径为空,则直接创建DexFile
//PathClassLoader加载的时候,就会执行到这里
return new DexFile(file);
} else {
//DexClassLoader加载的时候,会执行到这里
//在dex优化文件存储路径上,创建一个同名的.dex文件
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
从这里可以看出,根据optimizedDirectory的不同,生成的DexFile对象也不同。实际的load操作是在DexFile.loadDex()方法里执行的,我们继续跟下去:
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
//这里直接返回一个DexFile对象
return new DexFile(sourcePathName, outputPathName, flags);
}
再来看看DexFile的构造函数:
/**
* Opens a DEX file from a given filename. This will usually be a ZIP/JAR
* file with a "classes.dex" inside.
*
* The VM will generate the name of the corresponding file in
* /data/dalvik-cache and open it, possibly creating or updating
* it first if system permissions allow. Don't pass in the name of
* a file in /data/dalvik-cache, as the named file is expected to be
* in its original (pre-dexopt) state.
*
* @param fileName
* the filename of the DEX file
*
* @throws IOException
* if an I/O error occurs, such as the file not being found or
* access rights missing for opening it
*/
public DexFile(String fileName) throws IOException {
mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
guard.open("close");
}
/**
* Opens a DEX file from a given filename, using a specified file
* to hold the optimized data.
*
* @param sourceName
* Jar or APK file with "classes.dex".
* @param outputName dex数据优化后的存储目录
* File that will hold the optimized form of the DEX data.
* @param flags
* Enable optional features.
*/
private DexFile(String sourceName, String outputName, int flags) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}
最终的实现逻辑又是在openDexFile()方法里来实现的,该方法里实际是调用了native方法来实现的,native里的方法这里不再跟踪了。
Android在载入dex文件时,首先会对原dex文件进行优化,然后将优化后的dex数据(odex文件)存储到某个缓存文件里,最后再加载缓存里的dex文件,如果发现已经优化过,则直接读取缓存里的dex文件。
从前面的代码中可以分析出,PathClassLoader是通过new DexFile(file)来载入dex文件的,而应用程序在安装阶段已经生成了形如/data/dalvik-cache/xxx@classes.dex的dex优化文件,PahtClassLoader在载入dex时不需要先优化dex,而是会直接读取/data/dalvik-cache目录下的缓存dex文件,所以说PathClassLoader只能加载系统已经安装过的应用程序。但这种说法并不准确,因为只是PathClassLoader默认的优化dex缓存路径固定为/data/dalvik-cache而已,而这个目录通常只是在安装应用时系统来操作,但如果我们拥有root权限,实际上PathClassLoader应该也是能加载其他路径的dex文件的。
DexClassLoader来加载dex时,会先优化原来的dex文件,然后将优化后的dex文件缓存在optimizedDirectory目录,优化这个操作通常只执行一次,后续会直接加载缓存目录的dex文件了。
5. 小结
PathClassLoader与DexClassLoader唯一的差别就是,后者能够指定dex优化文件的缓存目录,而前者的缓存目录固定为/data/dalvik-cache,通常情况下应用是没有该目录的读写权限的,只有应用安装时会在该目录下生成缓存dex文件,所以我们一般采用DexClassLoader来实现动态加载。