[TOC]

错误表现

  • app 无法打包,日志为
com.android.dex.DexException:Too many classes in --main-dex-list, main dex capacity exceeded

错误原因

生成的第一个classes.dex中方法数操过65535 也就是 Short.MAX_VALUE

在Android生成APK工具链的 dx 源码中有

dalvik/dx/src/com/android/dx/command/dexer/Main.java

if (args.mainDexListFile != null) {  // with --main-dex-list  // ...  // forced in main dex  for (int i = 0; i < fileNames.length; i++) {    // call processClass    processOne(fileNames[i], mainPassFilter);  }  if (dexOutputArrays.size() > 0) {    throw new DexException("Too many classes in " + Arguments.MAIN_DEX_LIST_OPTION      + ", main dex capacity exceeded");  }}

processClass

private static boolean processClass(String name, byte[] bytes) {  int numMethodIds = outputDex.getMethodIds().items().size();  int numFieldIds = outputDex.getFieldIds().items().size();  int constantPoolSize = cf.getConstantPool().size();  int maxMethodIdsInDex = numMethodIds + constantPoolSize + cf.getMethods().size() +  MAX_METHOD_ADDED_DURING_DEX_CREATION;  int maxFieldIdsInDex = numFieldIds + constantPoolSize + cf.getFields().size() +  MAX_FIELD_ADDED_DURING_DEX_CREATION;  if (args.multiDex    && (outputDex.getClassDefs().items().size() > 0)    && ((maxMethodIdsInDex > args.maxNumberOfIdxPerDex) ||      (maxFieldIdsInDex > args.maxNumberOfIdxPerDex))) {    DexFile completeDex = outputDex;  createDexFile();}

createDexFile创建一个新的Byte[]对象放入dexOutputArrays
processAllFiles遇到dexOutputArrays.size > 0就会抛DexException

分包过程解析

分包的原因

Android系统安装运行应用的时候,有一步是对 dex 进行运行优化,增加运行效率
优化过程中,有过处理汇编文件加载的优化叫dexOpt

dexOpt的执行过程
在第一次加载Dex文件的时候执行的,这个过程会生成一个 odex文件,即Optimised dex
odex的用途是分离程序资源和可执行文件、以及做预编译处理
执行 odex处理过的 的效率会比直接执行 dex纯粹jar包 文件的效率要高很多

dexOpt有一个设计,会把每一个类的方法id检索起来,存在一个链表结构里面
这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536(Short.MAX_VALUE)

  • Dalvik 模式下,dexOpt 肯定开启
  • ART 模式下,虽然不是JIT,仍然复用了这个优化策略

解决方法数超限的问题,需要将该dex文件拆成两个或多个

google官方文档 https://developer.android.com/tools/building/multidex.html#about

分包过程

  • 找出必须优先加载的类在主包
  • 其余类按引用顺序,随机到其余包
  • 打包构建出APP发布
  • 运行分包的APP在 Applicaion 实例化之后,会检查系统版本是否支持 multidex
  • 运行加载主包 Dalvik 模式将子包拷贝到应用的沙盒目录 ART 则是运行主包,混合子包生成 oat 文件
  • Dalvik 运行时,先运行编译运行主包,然后通过反射将子dex注入到当前的 classloader 中
  • ART 模式加载 包含多个dex的 oat 文件,然后解释运行主包,一样通过反射将子dex注入到当前的 classloader 中

Android运行时ART加载OAT文件的过程分析
http://blog.csdn.net/luoshengyang/article/details/39307813

5.0 系统前后分包支持

  • Android5.0之前,使用 Dalvik 方式运行,先加载主分包,然后反射加载其余的包
  • Android5.0之后,使用 ART 方式运行,ART预编译时,扫描主分包和子包,生成 .oat 文件用于用户运行

分包方案的隐患

  • API14 之前的不能支持分包 Dalvik linearalloc bug
  • 复杂的依赖的工程,分包后,不同依赖项目间的dex文件函数相互调用,报错找不到方法
  • 带有混淆的工程,非常容易出现依赖沾粘(不同依赖项目间的dex文件同一个类定义树),安装时报告类定义安全检查异常
  • 开发过程有分包,导致每一次的模块构建过程都是相当耗时,开发效率低下
  • 分包文件过大,安装分割dex文件在某些设备上表现很差,非常容易导致ANR (此问题 5.0 以后的设备表现为安装不成功)
  • 分包数量过多引起安装失败,分到第6个以后容易出现,原理同上面一点
  • 应用程序使用 multiedex 就大量使用反射,会造成使用比较大的内存,OOM 将会非常容易出现
  • 工程过大,且依赖管理混乱时,主分包因为必须加载,方法数还是超过了65536,导致主分包无法生成

解决multiedex隐患思路

  • 删除无意义代码,无意义资源
  • 删除重复代码轮子
  • 拆分重型模块,依赖越多,越成树状越易维护
  • 降低工程依赖圈复杂度(不要循环依赖,用反射解耦)
  • 分离开发和发布构建,尽量让模块功能最小化,减少模块本身分包可能性
  • 活用混淆,降低dex文件大小(混淆实际上起不到安全作用,Dalvik 汇编很容易理解掌握破解混淆)
  • 使用二进制优化工具,减小dex的大小
  • 业务过于多,做多个 APP 通讯的方式,或者拆分子 app 动态加载
  • 优化主分包依赖

主分包详解

主分包在Android编译,发布,运行时地位很高,而主分包的生成是靠分析出的 maindexlist.txt 来生成的

maindexlist.txt 创建分析

源码地址

android gradle plugin
有一个类专门负责创建maindexlist.txt,叫做CreateMainDexList

源码

tools/base/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/multidex/CreateMainDexList.groovy

https://android.googlesource.com/platform/tools/base/+/master/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/multidex/CreateManifestKeepList.groovy

@TaskActionvoid output() {    if (getAllClassesJarFile() == null) {        throw new NullPointerException("No input file")    }    // manifest components plus immediate dependencies must be in the main dex.    File _allClassesJarFile = getAllClassesJarFile()    Set mainDexClasses = callDx(_allClassesJarFile, getComponentsJarFile())    ...}

callDx最终调用AndroidBuilder.createMainDexList
实际是通过开启后台进程执行ClassReferenceListBuilder.main
分析类的依赖关系,生成一个maindexlist.txt

public void addRoots(ZipFile jarOfRoots) throws IOException {    ...    for (Enumeration<? extends ZipEntry> entries = jarOfRoots.entries();      entries.hasMoreElements();) {      ZipEntry entry = entries.nextElement();      String name = entry.getName();      if (name.endsWith(CLASS_EXTENSION)) {          DirectClassFile classFile;          ...          classFile = path.getClass(name);          ...          addDependencies(classFile.getConstantPool());      }  }}

ClassReferenceListBuilder.addRoots通过读文件遍历componentClasses.jar的每个entry
再调用addDependencies分析这个类的依赖关系

private void addDependencies(ConstantPool pool) {    for (Constant constant : pool.getEntries()) {        if (constant instanceof CstType) {            Type type = ((CstType) constant).getClassType();            String descriptor = type.getDescriptor();            if (descriptor.endsWith(";")) {                int lastBrace = descriptor.lastIndexOf('[');                if (lastBrace < 0) {                    addClassWithHierachy(descriptor.substring(1, descriptor.length()-1));                } else {                    assert descriptor.length() > lastBrace + 3                    && descriptor.charAt(lastBrace + 1) == 'L';                    addClassWithHierachy(descriptor.substring(lastBrace + 2,                            descriptor.length() - 1));                }            }        }    }}

addDependenciesConstantPool得到import类,调用addClassWithHierachy继续分析继承关系

也可以通过javap -verbose先反汇编,再分析匹配”= class”的字符串来获取来调试

依赖关系分析结束后,输出maindexlist.txt

所以一句话 componentClasses.jar最终决定了maindexlist.txt的大小

componentClasses.jar 生成分析

这个中间生成的componentClasses.jar最后会在打包成功后删除

当然出现方法超过的时候,这个包在 moduel/build/intermediates/multi-dex/对应渠道里面

gradle plugin 2.3.0 以后位置有变动,不过一样可以找到

componentClasses.jar 生成任务 proguardComponentsTask

根据manifest_keep.txtallclasses.jar中抽取生成的,manifest_keep.txt内容一般是

-keep class com.xxx.app.XXXXApplication {  ();  void attachBaseContext(android.content.Context);}-keep class com.xxx.splash.XXXXActivity { (); }-keep class com.xxx.app.MainActivity { (); }-keep class com.xxx.login.xxxx.LoginActivity { (); }-keep class com.xxx.sidebar.account.XXAccountActivity { (); }...

不难看出manifest_keep.txt是通过CreateManifestKeepList解析AndroidManifest.xml文件得到

./tools/base/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/multidex/CreateManifestKeepList.groovy

    @TaskAction    void generateKeepListFromManifest() {        SAXParser parser = SAXParserFactory.newInstance().newSAXParser()        Writer out = new BufferedWriter(new FileWriter(getOutputFile()))        try {            parser.parse(getManifest(), new ManifestHandler(out))            // add a couple of rules that cannot be easily parsed from the manifest.            out.write("""-keep public class * extends android.app.backup.BackupAgent {    ();}-keep public class * extends java.lang.annotation.Annotation {    *;}""")            if (proguardFile != null) {                out.write(Files.toString(proguardFile, Charsets.UTF_8))            }        } finally {            out.close()        }    }    ...

CreateManifestKeepList私有内部类ManifestHandlerCreateManifestKeepList.KEEP_SPECS[qName]决定哪些类需要放入manifest_keep.txt

private class ManifestHandler extends DefaultHandler {    ...    @Override    void startElement(String uri, String localName, String qName, Attributes attr) {        String keepSpec = CreateManifestKeepList.KEEP_SPECS[qName]        if (keepSpec) {            boolean keepIt = true            if (CreateManifestKeepList.this.filter) {                Map attrMap = [:]                for (int i = 0; i < attr.getLength(); i++) {                    attrMap[attr.getQName(i)] = attr.getValue(i)                }                keepIt = CreateManifestKeepList.this.filter(qName, attrMap)            }            if (keepIt) {                String nameValue = attr.getValue('android:name')                if (nameValue != null) {                    out.write((String) "-keep class ${nameValue} $keepSpec\n")                }

过滤的KEEP_SPECS

    private static String DEFAULT_KEEP_SPEC = "{ (); }"    private static Map KEEP_SPECS = [        'application' : """{    ();    void attachBaseContext(android.content.Context);}""",        'activity' : DEFAULT_KEEP_SPEC,        'service' : DEFAULT_KEEP_SPEC,        'receiver' : DEFAULT_KEEP_SPEC,        'provider' : DEFAULT_KEEP_SPEC,        'instrumentation' : DEFAULT_KEEP_SPEC,    ]

那么至少AndroidManifest.xml中

  • application
  • activity
  • service
  • receiver
  • provider
  • instrumentation

这6种标签的类

以及继承

  • java.lang.annotation.Annotation
  • android.app.backup.BackupAgent

的类都会会用来产生maindexlist.txt

总结,必须在主分包中的类有

  • 基于apk运行的加载机制,Application 中的引用肯定在在主包内
  • 开启 MultiDex 分包 那么android.support.multidex 包肯定必须在主包内
  • 继承 java.lang.annotation.Annotation android.app.backup.BackupAgent 在主包
  • AndroidManifest.xml 注册的四大组件,必须在第一个包
  • 使用 instrumentation 测试技术实现的必须在第一个包内

更多相关文章

  1. Android使用文件管理器打开指定文件夹,浏览里面的内容
  2. Android SSL 过程记录(证书的生成+例子代码)
  3. android 向serverGet和Post请求的两种方式,android向server发送
  4. Cocos2d-x3.3RC0加载Android的WebView
  5. android使用webview加载flash文件
  6. 修改Android中strings.xml文件

随机推荐

  1. Android(安卓)驱动Ok6410Led
  2. Android中,在C++层使用TinyXML解析XML文件
  3. 如何对android framework有更深入的理解
  4. Android(安卓)MP4取得播放时长的方法
  5. 如何保证Android设备的安全性
  6. Android清单文件详解(四) ---- backupAge
  7. Android(安卓)内容提供器---创建内容提供
  8. android中ListView点击和ListView的item
  9. 编译android 之后生成的 img 文件介绍
  10. Android(安卓)AOP(二):AspectJ在Android中