android热修复相关之Multidex解析

从本篇文章开始,对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= [--minimal-main-dex]]

参数说明:

  • --multi-dex:多 dex 打包的开关
  • --main-dex-list=:参数是一个类列表的文件,在该文件中的类会被打包在第一个 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 files,
            File optimizedDirectory) {
        ArrayList elements = new ArrayList();
        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的相关源码分析的文章很多,大家可以自行查看,这里只看一下核心代码。基本上可以归结成两步:

    1. 将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");
    1. 利用反射,将所有的zip放到DexPathList的Elements数组中,并进行调用。
private static final class V14 {
    private V14() {
    }
    private static void install(ClassLoader loader, List 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 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> dex_files;
  std::vector 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> 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>* 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 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::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的热修复相关工作。感兴趣的同学欢迎进群交流。


android热修复相关之Multidex解析_第1张图片
image.png

你可能感兴趣的:(android热修复相关之Multidex解析)