技术文章待学习

Android Too many classes in --ma

2017-08-28  本文已影响1089人  木猫尾巴

[TOC]

错误表现

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)

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

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

分包过程

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

5.0 系统前后分包支持

分包方案的隐患

解决multiedex隐患思路

主分包详解

主分包在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

@TaskAction
void 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<String> 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 {
  <init>();
  void attachBaseContext(android.content.Context);
}
-keep class com.xxx.splash.XXXXActivity { <init>(); }
-keep class com.xxx.app.MainActivity { <init>(); }
-keep class com.xxx.login.xxxx.LoginActivity { <init>(); }
-keep class com.xxx.sidebar.account.XXAccountActivity { <init>(); }
...

不难看出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 {
    <init>();
}
-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<String, String> 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 = "{ <init>(); }"
    private static Map<String, String> KEEP_SPECS = [
        'application' : """{
    <init>();
    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中

这6种标签的类

以及继承

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

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

上一篇下一篇

猜你喜欢

热点阅读