Android Too many classes in --main-dex-list 錯誤原因及Android分包原理

[TOC]

錯誤表現(xiàn)

  • app 無法打包锋叨,日志為
com.android.dex.DexException:Too many classes in --main-dex-list, main dex capacity exceeded

錯誤原因

生成的第一個classes.dex中方法數(shù)操過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創(chuàng)建一個新的Byte[]對象放入dexOutputArrays
processAllFiles遇到dexOutputArrays.size > 0就會拋DexException

分包過程解析

分包的原因

Android系統(tǒng)安裝運行應(yīng)用的時候触菜,有一步是對 dex 進行運行優(yōu)化仆救,增加運行效率
優(yōu)化過程中,有過處理匯編文件加載的優(yōu)化叫dexOpt

dexOpt的執(zhí)行過程
在第一次加載Dex文件的時候執(zhí)行的挑胸,這個過程會生成一個 odex文件,即Optimised dex
odex的用途是分離程序資源和可執(zhí)行文件、以及做預(yù)編譯處理
執(zhí)行 odex處理過的 的效率會比直接執(zhí)行 dex純粹jar包 文件的效率要高很多

dexOpt有一個設(shè)計玩荠,會把每一個類的方法id檢索起來,存在一個鏈表結(jié)構(gòu)里面
這個鏈表的長度是用一個short類型來保存的贼邓,導(dǎo)致了方法id的數(shù)目不能夠超過65536(Short.MAX_VALUE)

  • Dalvik 模式下阶冈,dexOpt 肯定開啟
  • ART 模式下,雖然不是JIT塑径,仍然復(fù)用了這個優(yōu)化策略

解決方法數(shù)超限的問題,需要將該dex文件拆成兩個或多個

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

分包過程

  • 找出必須優(yōu)先加載的類在主包
  • 其余類按引用順序堂飞,隨機到其余包
  • 打包構(gòu)建出APP發(fā)布
  • 運行分包的APP在 Applicaion 實例化之后绑咱,會檢查系統(tǒng)版本是否支持 multidex
  • 運行加載主包 Dalvik 模式將子包拷貝到應(yīng)用的沙盒目錄 ART 則是運行主包绰筛,混合子包生成 oat 文件
  • Dalvik 運行時,先運行編譯運行主包,然后通過反射將子dex注入到當(dāng)前的 classloader 中
  • ART 模式加載 包含多個dex的 oat 文件描融,然后解釋運行主包,一樣通過反射將子dex注入到當(dāng)前的 classloader 中

Android運行時ART加載OAT文件的過程分析
http://blog.csdn.net/luoshengyang/article/details/39307813

5.0 系統(tǒng)前后分包支持

  • Android5.0之前骏庸,使用 Dalvik 方式運行毛甲,先加載主分包,然后反射加載其余的包
  • Android5.0之后具被,使用 ART 方式運行玻募,ART預(yù)編譯時七咧,掃描主分包和子包叮叹,生成 .oat 文件用于用戶運行

分包方案的隱患

  • API14 之前的不能支持分包 Dalvik linearalloc bug
  • 復(fù)雜的依賴的工程,分包后蛉顽,不同依賴項目間的dex文件函數(shù)相互調(diào)用,報錯找不到方法
  • 帶有混淆的工程悼粮,非常容易出現(xiàn)依賴沾粘(不同依賴項目間的dex文件同一個類定義樹)噪叙,安裝時報告類定義安全檢查異常
  • 開發(fā)過程有分包,導(dǎo)致每一次的模塊構(gòu)建過程都是相當(dāng)耗時苞笨,開發(fā)效率低下
  • 分包文件過大子眶,安裝分割dex文件在某些設(shè)備上表現(xiàn)很差,非常容易導(dǎo)致ANR (此問題 5.0 以后的設(shè)備表現(xiàn)為安裝不成功)
  • 分包數(shù)量過多引起安裝失敗粤咪,分到第6個以后容易出現(xiàn)渴杆,原理同上面一點
  • 應(yīng)用程序使用 multiedex 就大量使用反射磁奖,會造成使用比較大的內(nèi)存囊拜,OOM 將會非常容易出現(xiàn)
  • 工程過大冠跷,且依賴管理混亂時,主分包因為必須加載蜜托,方法數(shù)還是超過了65536橄务,導(dǎo)致主分包無法生成

解決multiedex隱患思路

  • 刪除無意義代碼,無意義資源
  • 刪除重復(fù)代碼輪子
  • 拆分重型模塊柑司,依賴越多锅劝,越成樹狀越易維護
  • 降低工程依賴圈復(fù)雜度(不要循環(huán)依賴蟆湖,用反射解耦)
  • 分離開發(fā)和發(fā)布構(gòu)建,盡量讓模塊功能最小化诬垂,減少模塊本身分包可能性
  • 活用混淆伦仍,降低dex文件大小(混淆實際上起不到安全作用隧枫,Dalvik 匯編很容易理解掌握破解混淆)
  • 使用二進制優(yōu)化工具谓苟,減小dex的大小
  • 業(yè)務(wù)過于多,做多個 APP 通訊的方式卑笨,或者拆分子 app 動態(tài)加載
  • 優(yōu)化主分包依賴

主分包詳解

主分包在Android編譯仑撞,發(fā)布隧哮,運行時地位很高,而主分包的生成是靠分析出的 maindexlist.txt 來生成的

maindexlist.txt 創(chuàng)建分析

源碼地址

android gradle plugin
有一個類專門負(fù)責(zé)創(chuàng)建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最終調(diào)用AndroidBuilder.createMainDexList
實際是通過開啟后臺進程執(zhí)行ClassReferenceListBuilder.main
分析類的依賴關(guān)系歧譬,生成一個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
再調(diào)用addDependencies分析這個類的依賴關(guān)系

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類瑰步,調(diào)用addClassWithHierachy繼續(xù)分析繼承關(guān)系

也可以通過javap -verbose先反匯編,再分析匹配”= class”的字符串來獲取來調(diào)試

依賴關(guān)系分析結(jié)束后读虏,輸出maindexlist.txt

所以一句話 componentClasses.jar最終決定了maindexlist.txt的大小

componentClasses.jar 生成分析

這個中間生成的componentClasses.jar最后會在打包成功后刪除

當(dāng)然出現(xiàn)方法超過的時候袁滥,這個包在 moduel/build/intermediates/multi-dex/對應(yīng)渠道里面

gradle plugin 2.3.0 以后位置有變動,不過一樣可以找到

componentClasses.jar 生成任務(wù) proguardComponentsTask

根據(jù)manifest_keep.txtallclasses.jar中抽取生成的揩徊,manifest_keep.txt內(nèi)容一般是

-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私有內(nèi)部類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中

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

這6種標(biāo)簽的類

以及繼承

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

的類都會會用來產(chǎn)生maindexlist.txt

總結(jié),必須在主分包中的類有

  • 基于apk運行的加載機制齿税,Application 中的引用肯定在在主包內(nèi)
  • 開啟 MultiDex 分包 那么android.support.multidex 包肯定必須在主包內(nèi)
  • 繼承 java.lang.annotation.Annotation android.app.backup.BackupAgent 在主包
  • AndroidManifest.xml 注冊的四大組件炊豪,必須在第一個包
  • 使用 instrumentation 測試技術(shù)實現(xiàn)的必須在第一個包內(nèi)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末溜在,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子仆葡,更是在濱河造成了極大的恐慌志笼,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腰涧,死亡現(xiàn)場離奇詭異紊浩,居然都是意外死亡疗锐,警方通過查閱死者的電腦和手機滑臊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門箍铲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人关划,你說我怎么就攤上這事翘瓮。” “怎么了脱货?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵律姨,是天一觀的道長臼疫。 經(jīng)常有香客問我烫堤,道長,這世上最難降的妖魔是什么鸽斟? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任富蓄,我火速辦了婚禮,結(jié)果婚禮上灭红,老公的妹妹穿的比我還像新娘口注。我一直安慰自己,他們只是感情好寝志,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著唯竹,像睡著了一般悔醋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芬骄,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天账阻,我揣著相機與錄音,去河邊找鬼姻僧。 笑死蒲牧,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的松嘶。 我是一名探鬼主播挎扰,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尽超!你這毒婦竟也來了梧躺?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤棘脐,失蹤者是張志新(化名)和其女友劉穎龙致,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屈梁,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡在讶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了革答。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片曙强。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡碟嘴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出娜扇,到底是詐尸還是另有隱情,我是刑警寧澤枢析,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布登疗,位于F島的核電站嫌蚤,受9級特大地震影響断傲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜认罩,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一垦垂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧间校,春花似錦页慷、人聲如沸胁附。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弓候。三九已至他匪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撰筷,已是汗流浹背畦徘。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留关筒,地道東北人杯缺。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像袍榆,于是被迫代替她去往敵國和親塘揣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容