[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(安卓)SDK下载和更新失败的解决方法!!!
  2. android开发环境搭建备忘
  3. Android(安卓)ROM研究---Android(安卓)build system增加模块
  4. S3C6410(M8用的) 移植Android(安卓)内核
  5. Android—— ubuntu下【CTS】测试TV真机
  6. Android(安卓)== 在Android系统上运行JAVA程序
  7. android 如何保护我们的app(二)(干货)
  8. Android使用文件管理器打开指定文件夹,浏览里面的内容
  9. NPM 和webpack 的基础使用

随机推荐

  1. React Native apk打包下载(android)
  2. android studio 的下拉菜单Spinner使用详
  3. 基于OpenCV和OpenGL 的简易美颜相机
  4. android中的spannable的使用(TextView分段
  5. android UI小结(五)
  6. Android 组件安全
  7. android - ViewPager 监听左右滑动
  8. Error: Error parsing D:\android-sdk-w
  9. OpenMax在Android上的实现
  10. android图形化学习1