从本篇文章开始,对classloader方案热修复的相关知识进行学习。这个方案的源头是基于google为了解决方法数超限问题而引入的MultiDex技术。关于方法数超限问题,估计大家都有所了解,这里就不多介绍了。
MultiDex的实现分为两方面,一方面在编译apk过程中,插件能将class文件打成多个dex文件,另一方面需要在程序运行时,将classes2.dex, classes3.dex加载进来。我们主要关注dex加载过程,至于dex拆分过程,这里简单的说一下。
编译apk过程中,android在5.0及其以上的SDK中dx工具支持multidex参数。
[--multi-dex [--main-dex-list=<file> [--minimal-main-dex]]
参数说明:
- --multi-dex:多 dex 打包的开关
- --main-dex-list=<file>:参数是一个类列表的文件,在该文件中的类会被打包在第一个 dex 中
- --minimal-main-dex:只有在--main-dex-list 文件中指定的类被打包在第一个 dex,其余的都在第二个 dex 文件中,主要是为了减小主dex的大小。
和Multidex相关的gradle任务如下:
:transformClassesWithJarMergingForDebug UP-TO-DATE
:collectDebugMultiDexComponents UP-TO-DATE
:transformClassesWithMultidexlistForDebug UP-TO-DATE
:transformClassesWithDexForDebug UP-TO-DATE
-
transformClassesWithJarMergingForDebug
这个transform的作用是将所用到的 jar 转换至一个单一的 Jar 中,输出产物在 build/intermediates/transforms/jarMerging 目录下的 combined.jar文件。 -
collectDebugMultiDexComponents
该task扫描AndroidManifest.xml中的application、activity、receiver、provider、service等相关类,并将这些类的信息写入到manifest_keep.txt文件中,该文件位于目录build/intermediates/multi-dex/debug -
transformClassesWithMultidexlistForDebug
这个transform根据之前的 mainfest_keep 及一些 proguard 文件来生成 mainDex 中指定的类集合文件,对应生成的输出结果为 maindexlist.txt,同时生成componentClasses.jar文件,两个文件均位于build/intermediates/multi-dex/debug目录下。 -
transformClassesWithDexForDebug
调用dx命令,进行dex生成,这里在处理主dex是通过遍历maindexlist.txt对应的class文件,读取class文件格式中常量池的内容,从而获取到依赖类。
通过以上的transform和相关task,我们打出的apk会包含不只一个xx.dex。下面来讲一下多dex运行的问题。这里需要大家对类加载器有所了解,可以参考我之前写过的一篇文章Android插件化框架系列之类加载器或者查看相关文章进行学习。
对于apk中多dex成功加载的问题,按虚拟机类型进行分类分析:
Dalvik虚拟机
针对dalvik虚拟机,我们都知道google是借助Multidex库解决的。唯一的可能就是dalvik不能从一个apk中加载多个dex,我们去源码里验证一下,以4.2的源码为例,分析一下classloader加载dex的流程(PS:这里主要为了分析classloader,所以没有从apk安装开始,可以认为是在odex不存在的情况分析PathClassLoader加载dex的过程)我们看一下BaseDexClassLoader源码。
/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
可见我们传入apk路径后,在加载器构建时会构建一个DexPathList,此外什么都没做,跟进去看一下。
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
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是我们传入的apk路径,整个构造函数完成的任务就是填充dexElements,这个dexElements是一个Element[]类型的变量,Element又是什么?在dalvik下,我们可以这么理解,每个dex加载成功后,会对应成一个DexFile对象,这里暂时可以认为Element就是DexFile的一个封装(不考虑资源的情况下),也就是等同于一个dex。篇幅原因,直接进行解释啦,splitDexPath负责解析传入路径,支持路径中有多个dex或者压缩包,例如xxx.zip;xxx.zip;xxx.zip等,是利用File.pathSeparator进行分割的,每个路径会封装成一个File,最终形成一个list,当然了,绝大多数情况下,我们传给类加载器的路径都是单一的。
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)) {
// 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) {
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
这没什么好分析的,我们传入的是apk,可以看到,先是封装成一个ZipFile,然后调用了loadDexFile方法。最终会调用到DexFile的openDexFile方法,该方法是一个native方法。源码在 /dalvik/vm/native/dalvik_system_DexFile.cpp
static void Dalvik_dalvik_system_DexFile_openDexFile(const u4* args,
JValue* pResult)
{
StringObject* sourceNameObj = (StringObject*) args[0];
StringObject* outputNameObj = (StringObject*) args[1];
DexOrJar* pDexOrJar = NULL;
JarFile* pJarFile;
RawDexFile* pRawDexFile;
char* sourceName;
char* outputName;
...
...
if (hasDexExtension(sourceName)
&& dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) {
LOGV("Opening DEX file '%s' (DEX)", sourceName);
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = true;
pDexOrJar->pRawDexFile = pRawDexFile;
pDexOrJar->pDexMemory = NULL;
} else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) {
LOGV("Opening DEX file '%s' (Jar)", sourceName);
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = false;
pDexOrJar->pJarFile = pJarFile;
pDexOrJar->pDexMemory = NULL;
} else {
LOGV("Unable to open DEX file '%s'", sourceName);
dvmThrowIOException("unable to open DEX file");
}
if (pDexOrJar != NULL) {
pDexOrJar->fileName = sourceName;
addToDexFileTable(pDexOrJar);
} else {
free(sourceName);
}
RETURN_PTR(pDexOrJar);
}
hasDexExtension判断是否是.dex,我们这里是.apk,显然会走到dvmJarFileOpen方法中。dvmJarFileOpen方法在/dalvik/vm/JarFile.cpp中
int dvmJarFileOpen(const char* fileName, const char* odexOutputName,
JarFile** ppJarFile, bool isBootstrap)
{
...
...
fd = openAlternateSuffix(fileName, "odex", O_RDONLY, &cachedName);
if (fd >= 0) {
LOGV("Using alternate file (odex) for %s ...", fileName);
if (!dvmCheckOptHeaderAndDependencies(fd, false, 0, 0, true, true)) {
LOGE("%s odex has stale dependencies", fileName);
free(cachedName);
cachedName = NULL;
close(fd);
fd = -1;
goto tryArchive;
} else {
LOGV("%s odex has good dependencies", fileName);
//TODO: make sure that the .odex actually corresponds
// to the classes.dex inside the archive (if present).
// For typical use there will be no classes.dex.
}
} else {
ZipEntry entry;
tryArchive:
entry = dexZipFindEntry(&archive, kDexInJarName);
if (entry != NULL) {
bool newFile = false;
if (odexOutputName == NULL) {
cachedName = dexOptGenerateCacheFileName(fileName,
kDexInJarName);
if (cachedName == NULL)
goto bail;
} else {
cachedName = strdup(odexOutputName);
}
LOGV("dvmJarFileOpen: Checking cache for %s (%s)",
fileName, cachedName);
fd = dvmOpenCachedDexFile(fileName, cachedName,
dexGetZipEntryModTime(&archive, entry),
dexGetZipEntryCrc32(&archive, entry),
isBootstrap, &newFile, /*createIfMissing=*/true);
if (fd < 0) {
LOGI("Unable to open or create cache for %s (%s)",
fileName, cachedName);
goto bail;
}
locked = true;
....
....
return result;
}
首先openAlternateSuffix检查是否已经存在了对应的odex,如果存在,在dvmCheckOptHeaderAndDependencies中进行opt格式校验,如果不存在odex或者存在无效odex时,会利用dexzipFindEntry函数去查找匹配对应的dex,而kDexInJarName的值为常量,这就解释了我们的问题,dalvik虚拟机中只会对名为“classes.dex”的dex文件进行加载,其余的均不会加载。当找到dex后,会调用dvmOpenCachedDexFile函数,在函数内部会有启动执行dexopt相关的代码,进而执行dexopt过程,这个暂时不做分析。
static const char* kDexInJarName = "classes.dex";
到这里我们就从源码角度解释了为什么dalvik虚拟机只能加载apk包的一个dex,而且必须为classes.dex。整个流程也是为了记录下android类加载器加载流程,下面分析到art虚拟机的时候相似流程会跳过。因为apk中的classes2.dex,...等dex均无法加载,应用启动时肯定会报找不到类的异常。Multidex的作用就是想办法把classes2.dex,classes3.dex尽可能早的加载进来。
Multidex的相关源码分析的文章很多,大家可以自行查看,这里只看一下核心代码。基本上可以归结成两步:
- 将apk中的classes2.dex,classes3.dex...拷贝到目录/data/data/pkgName/code_cache/secondary-dexes/下,命名为/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classesN.zip,具体拷贝的过程在MultiDexExtractor的extract方法中,可以看出,相当于重命名为classes.dex压缩到了一个zip中。和前面源码分析的相符,即压缩包中的classes.dex。
ZipEntry classesDex = new ZipEntry("classes.dex");
- 利用反射,将所有的zip放到DexPathList的Elements数组中,并进行调用。
private static final class V14 {
private V14() {
}
private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));
}
private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class});
return (Object[])((Object[])makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory}));
}
}
这段是核心代码,也是所有classloader热修复方案的根本来源。我们来简要分析一下这段代码。additionalClassPathEntries是我们拷贝过来的所有的zip包列表,通过反射调用makeDexElements函数,得到新的Elements数组,然后调用用expandFieldArray函数,将两个Elements数组进行合并。这样应用的pathClassLoader中的elements数组就包含多个dex文件了当我们查找类的时候就是遍历elements中的dex,从每个dex中依次查找。
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[])((Object[])jlrField.get(instance));
Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
jlrField.set(instance, combined);
}
顺便说一下MultiDex中可能遇到的问题,因为我们项目是用的插件化框架,方法数虽然超出了65536,但是主项目方法数超出的并不多,线上并没有监测到ANR问题,但是从原理上来讲,dalvik下,apk安装时只对主dex进行了dexopt,而从dex都是在第一次启动时,进行dexopt操作的,具体时机是在makeDexElements函数被反射调用时进行的,至于dexopt为什么耗时,后面的文章会进行分析。所以如果Classes2.dex很大,或者从dex很多,加载过程将相当耗时,确实很有可能出现ANR,基本上大家的解决方案都是采用异步加载,做个等待页面来解决。
ART虚拟机
ok,分析完了dalvik下多dex加载,我们都知道Multidex库对于API20以上是不需要的,art虚拟机进行了相关的内建支持。来看一下art的类加载器在这一块的处理,以android6.0源码为例,java层代码几乎一样,直接看native层代码:
/art/runtime/native/dalvik_system_DexFile.cc
static jobject DexFile_openDexFileNative(
JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
ScopedUtfChars sourceName(env, javaSourceName);
ClassLinker* linker = Runtime::Current()->GetClassLinker();
std::vector<std::unique_ptr<const DexFile>> dex_files;
std::vector<std::string> error_msgs;
dex_files = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs);
if (!dex_files.empty()) {
jlongArray array = ConvertNativeToJavaArray(env, dex_files);
if (array == nullptr) {
ScopedObjectAccess soa(env);
for (auto& dex_file : dex_files) {
if (Runtime::Current()->GetClassLinker()->IsDexFileRegistered(*dex_file)) {
dex_file.release();
}
}
}
return array;
} else {
ScopedObjectAccess soa(env);
CHECK(!error_msgs.empty());
// The most important message is at the end. So set up nesting by going forward, which will
// wrap the existing exception as a cause for the following one.
auto it = error_msgs.begin();
auto itEnd = error_msgs.end();
for ( ; it != itEnd; ++it) {
ThrowWrappedIOException("%s", it->c_str());
}
return nullptr;
}
}
我们关注这三句就全明白了,dex_files是一个Vector对象,然后通过OpenDexFilesFromOat去加载apk中的所有dex,保存在dex_files中,然后通过ConvertNativeToJavaArray函数转化成jlongArray返回java端,保存在了DexFile的mCookie变量中。
std::vector<std::unique_ptr<const DexFile>> dex_files;
dex_files = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs);
jlongArray array = ConvertNativeToJavaArray(env, dex_files);
接下来的逻辑我们就不一一去追踪了,OpenDexFilesFromOat函数首先去判断有没有生成oat文件,如果没有,会先执行dexoat过程,生成oat文件,然后从oat文件中查找dex,最终会走到/art/runtime/dex_file.cc的OpenFromZip函数中
bool DexFile::OpenFromZip(const ZipArchive& zip_archive, const std::string& location,
std::string* error_msg,
std::vector<std::unique_ptr<const DexFile>>* dex_files) {
for (size_t i = 1; ; ++i) {
std::string name = GetMultiDexClassesDexName(i);
std::string fake_location = GetMultiDexLocation(i, location.c_str());
std::unique_ptr<const DexFile> next_dex_file(Open(zip_archive, name.c_str(), fake_location,
error_msg, &error_code));
if (next_dex_file.get() == nullptr) {
if (error_code != ZipOpenErrorCode::kEntryNotFound) {
LOG(WARNING) << error_msg;
}
break;
} else {
dex_files->push_back(std::move(next_dex_file));
}
if (i == std::numeric_limits<size_t>::max()) {
LOG(ERROR) << "Overflow in number of dex files!";
break;
}
}
return true;
}
}
std::string DexFile::GetMultiDexClassesDexName(size_t index) {
if (index == 0) {
return "classes.dex";
} else {
return StringPrintf("classes%zu.dex", index + 1);
}
}
ok,看到GetMultiDexClassesDexName函数就不需要解释什么了。
整体有点乱,简单总结一下,在art虚拟机中,Multidex是内建支持的,在apk安装时就完成了所有dex的dexoat过程。而dalvik下,apk安装时dalvik虚拟机只能对classes.dex进行处理,借助于MultiDex库反射elements数组进行dex添加完成的。
这篇文章写的目的一是为了引出classloader热修复方案,二是了解一下dalvik和art在类加载器方面的区别。
参考:
1.http://blog.csdn.net/jiangwei0910410003/article/details/50799573
2.http://blog.csdn.net/richie0006/article/details/51103976
目前本人在公司负责热修复相关的工作,主要是基于robust的热修复相关工作。感兴趣的同学欢迎进群交流。