理解 Multidex 生成

作者: alighters | 来源:发表于2017-01-16 15:46 被阅读143次

    介绍

    在 Android 开发中,提到 65536 问题,就不得不提 Multidex 的解决方案。具体问题就是在 Android 打包的期间,是需要对 java 文件编译成 class 文件,class 文件信息过多而又冗余,这就再经过一步合并变成 dex 文件的过程(这一步是 dx 工具来处理的),其才是 Davilk 虚拟机加载识别的东西。而单个 dex 文件,其对方法数、字段数做了限制,即不能超过 65536,这便是问题的由来了。

    当然,解决这个问题也是很简单的。在 gradle 中启用 Multidex,在 application 中添加 Multidex.install 方法。乍以为万事大吉了,但在我们项目中还是遇到了 65536 的问题,主要是因为项目太大,在生成 mainDex 的过程中,还是出现了方法数超过了 65536 的问题,解决这个问题的思想是要将 mainDex 中的一些类移除至其它的 dex 中。那这一步,应该怎么玩呢?这里先看一些 dx 工具为我们提供了什么?

      [--multi-dex [--main-dex-list=<file> [--minimal-main-dex]]
    

    执行 dx 命令,可以看到其提供了 --multi-dex 的选项参数,需要接受的是一个文件的 --main-dex-list 以及一个最小化主 dex 的 --minimal-main-dex 参数。这里就是需要我们最终控制的地方,但要接触到这里,需要我们先看看 gradle plugin 是如何集成与使用它的?

    生成

    在使用 Android Studio 的时候,在 build.gradle 文件中,已经提供了 multidex 的支持。相应的选项是在 defaultConfig 中的 multiDexEnabled 设置为 true 即可。那其是如何生成多个 dex 的过程,就需要在 gradle plugin 的源码中来寻找答案了。

    通过 gradle 编译生成 apk 的期间,可以通过 Gradle Console 视图查看 gradle 执行任务的输出,期间跟 multidex 几个相关的任务如下:

    :app:transformClassesWithJarMergingForDevDebug UP-TO-DATE
    :app:collectDevDebugMultiDexComponents
    :app:transformClassesWithMultidexlistForDevDebug UP-TO-DATE
    :app:transformClassesWithDexForDevDebug UP-TO-DATE
    

    这几个任务对应在 gradle plugin 的源码中的位置为 TaskManager 的方法 createPostCompilationTasks。这里针对其中相关的 transform 和 task 做以简单的讲解:

    1. JarMergingTransform

    JarMergingTransform 的主要作用是将所用到的 jar 转换至一个单一的 Jar 中。具体输出的结果,可以在 build/intermediates/transforms/jarMerging 目录下,看到一个名称为 combined 的 jar 文件。

    2. CreateManifestKeepList

    CreateManifestKeepList 继承自 DefaultAndroidTask, 这一步会读取项目之前合并后的 manifest 文件,根据既定的规则,获取其中的 application、activity、service、provider、instrumentation 类,与 Mainifest 中的类组件进行比较来获取,最后会在 build/intermediates/multidex 下生成名为 manifest_keep.txt 的文件。

    另外,此任务设置 Filter 类,支持对特定的类进行过滤,让指定的类保存在 maindex 中。但是此方法已被标记为 Deprecated, 可能会在后续的版本中废弃掉。

    3. MultiDexTransform

    MultiDexTransform 的主要任务是根据之前的 mainfest_keep 及一些 proguard 文件来生成 mainDex 中指定的类集合文件,对应生成的输出结果为 maindexlist.txt

    但这里有个问题,就是 mainDex 的生成规则,其是如何指定哪些类在 mainDexList 中?

    查看源码可看到它把这部分工作交给类 ClassReferenceListBuilder。其又调用了类 MainDexListBuilder,后者对应着 build_tools 中的 mainClasses 工具中处理依赖关系所使用到的类。这里真正的依赖判端逻辑是在 ClassReferenceListBuilder 中,所需要指定的两个参数 pathjarOfRoots,前者表示的是所需要处理的所有类文件的路径(对应上文的 combined.jar),后者指定的是所需要在 mainDex 中的类(即处理依赖时的 root,获取 root 所依赖的类, 对应上述步骤中生成的 manifest_keep.txt 中的类),

    其生成规则是遍历 jarOfRoots 中的 class 文件,将其对应程 DirectClassFile 对象(包含 class 信息的相应对象),之后从其中获取常量池中的类型,判断是类、方法、字段,并添加其类型所包含的类型信息。若是方法的时候,则需要的是方法的返回值类型以及参数值的类型。这里类型信息进行获取的时候,会从类、超类、实现的接口列表三个角度进行判断获取相应的类型信息。

    所以说,这里的依赖类的获取,是通过当前类的常量池来进行获取判断的,不得不说很机智。

    4. DexTransform

    它被 dexTask 所使用,相对应的调用程序为 build-tools 中的 dx 程序。在 DexTransform 中的参数 dexOptions、mainDexListFile 指定了 dx 命令执行过程中所需要的参数。其主要的任务用来生成 apk 中的 dex 文件,若是指定了 multidex 为 true 时,则会根据 mainDexList 文件(指定哪些类会在 mainDex)来划分生成最后的多个 dex 文件。

    这一步就是通过以上步骤的输出作为输入,进而执行 dx 命令的,来生成最终的 dex。了解到这里,接下来我们需要在以上的过程中动些手脚,来解决我们遇到的问题。

    解决

    方法一:改变 keepList 任务列表

    afterEvaluate {
      project.tasks.each { task ->
          if (task.name.startsWith('collect') && task.name.endsWith('MultiDexComponents')) {
              println "main-dex-filter: found task $task.name"
              task.filter { name, attrs ->
                  def componentName = attrs.get('android:name')
                  if ('activity'.equals(name)) {
                      println "main-dex-filter: skipping, detected activity [$componentName]"
                      return false
                  } else {
                      println "main-dex-filter: keeping, detected $name [$componentName]"
                      return true
                  }
              }
          }
      }
    }
    

    这一步对应 gradle 执行过程中的 CreateManifestKeepList,利用其提供的 filter,进行一些过滤操作,其中 name 参数表示为节点类型,例如 activity、service、receiver 等; attrs 参数表示相应的节点信息,它是一个 Map 类型的参数,可表示的值形如 ['android:name':'com.example.ActivityClass']

    这一步可对 mainDex 中的组件信息做一些过滤,而不是添加所有的组件信息。像上述代码的处理就很残暴,把所有的 activity 都过滤掉。

    PS: 需要注意的是,在源码中的 setFilter 已经被标为废弃,可能会在后续的版本被替换掉,所以用这种方案需要所使用的 gradle plugin 版本注意一二。

    方法二:修改 dx 的参数值

    afterEvaluate {
      tasks.matching {
          it.name.startsWith('dex')
          println("task name:" + it.name)
      }.each { dx ->
    
          if (dx.additionalParameters == null) {
              dx.additionalParameters = []
          }
    
          //允许生成多个dex文件
          dx.additionalParameters += '--multi-dex' // enable multidex
    
          // 设置multidex.keep文件中class为第一个dex文件中包含的class,如果没有下一项设置此项无作用
          dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
    
          //此项添加后第一个classes.dex文件只能包含-main-dex-list列表中class
          dx.additionalParameters += '--minimal-main-dex'
    
      }
    }
    

    这一步直接对应 dx 最终的调用,即修改我们上文所提到的参数值,将其替换我们手动填充的值,但是这一步的 multidex.keep 文件就需要我们折腾一二了。

    不过针对这个方案,笔者是一直没有找到在 Task 中相对应的以 dex 开头的任务,所以这个方案没有生效。那为什么会有这种写法呢?笔者在 Project 中的 Variant 中相对应的 ApkVariant 类中看到一点信息,此接口定义了 getDex() 方法,对应实现在 ApkVariantImpl 中如下:

     @Nullable
     @Override
     public Object getDex() {
         throw new RuntimeException("Access to the dex task is now impossible, starting with 1.4.0\n"
                 + "1.4.0 introduces a new Transform API allowing manipulation of the .class files.\n"
                 + "See more information: http://tools.android.com/tech-docs/new-build-system/transform-api");
     }
    

    代码中返回的值就是这个方案中与 dx 相对应的值。不过从异常信息中可以看到的是在 gradle plugin 1.4.0 的版本开始,此方法就已被废弃,而改为采用 transform 的实现。

    所以此方案只针对 gradle plugin 1.4.0 之前的版本。

    方案三:修改 MainDexList

    这里所说的 MainDexList 对应着 bulld-tools 目录下的 dx 工具中 --main-dex-list 参数,与 Gradle 任务中相对应的是上文中提到的 DexTransform 的参数 mainDexListFile。意味着我们在调用 dx 命令(对应着执行 DexTransform)时,可对 KeepList 进行二次修改,保证 mainDex 中的类不超过限制,同时不出现 NoClassDefFoundError 的错误。

    最终方案:DexKnifePlugin

    推荐使用 DexKnifePlugin。来简单描述一下它的实现。其定义了一套类似 Proguard 的规则,用来定制生成 mainDex 所需 MainDexList 的规则,另外其考虑了 Gradle Plugin 针对 Dex 生成的两个不同版本的兼容。最后达到缩减、调整 MainDexList 来保证 mainDex 的生成无误。(是上述方案二和三的结合)

    扩展

    NoClassDefFoundError 的出现

    出现这个错误时,解决办法是将异常中的这个类加至 mainDex 中。但是这个错误跟 NotClassFoundException 的区别,可查阅 链接

    其出现这个问题的说法,简单理解为虚拟机在第一次加载该类的出现了问题,当第二次再次使用这个类的时候,就会报出 NoClassDefFoundError。对应为我们 App 在运行时,在 mainDex 中有些类找不到,就会出现这样的错误。

    但是为什么出现 NoClassDefFoundError 呢?从上面的分析可知 mainDex 并不会把它依赖的所有类都包含进去,那么其类加载的规则是什么样的?这些内容是跟 Dalvik 虚拟机相关的,任务量不小,就暂且留作 2017 年的一项学习任务了。

    总结

    文章简单介绍了 Gradle Plugin 处理 MultiDex 的步骤,若是需要对 MainDex 做特殊处理时,便可根据 Manifest 文件生成的 keepList 或者 DexTransform 中的 MainDexList 做处理。

    相关文章

      网友评论

      本文标题:理解 Multidex 生成

      本文链接:https://www.haomeiwen.com/subject/qokpbttx.html