我是如何一步一步爬上 「64K限制」 的坑

分享初衷

分享這個填坑的記錄,主要是身邊很多 Androider 都會遇到難以解決的難題并重復走舊路。

大部分人都會按照這樣的步驟處理:

  1. 遇到一個 BUG ,優(yōu)先按照自己經(jīng)驗修復拾并;
  2. 修復不了,開始 Google(不要百度鹏浅,再三強調(diào))嗅义,尋找一切和我們 BUG 相似的問題,然后看看有沒有解決方案隐砸;
  3. 嘗試了很多解決方案之碗,a 方案不行換 b 方案,b 方案不行換 c 方案季希,直到?jīng)]有方案可以嘗試了褪那,開始懷疑人生幽纷;
  4. 如果影響不大,那就丟在項目里(估計也沒人發(fā)現(xiàn))博敬,如果影響很大友浸,那只能尋找別人幫助,如果別人也給不了建議偏窝,那就原地爆炸了收恢。

無論 BUG 影響多大,丟在項目里總不太好祭往。 當別人幫助不了的時候伦意,真的只有代碼能幫你。嘗試過很多方案不可行硼补,很多時候是因為每個方案的背景不一樣驮肉,包括開發(fā)環(huán)境背景如 Gradle 版本,編譯版本 括勺,API 版本場景差異化缆八。我遇到的這個問題也是如此。 希望通過以下的記錄能幫助你在面對無能為力的 BUG 時更堅定地尋找解決方案疾捍。

問題背景

在我們項目最近的一個版本中奈辰,QA 測試 Feature 功能時反饋 4.4 設備上 APP 全 Crash! 由于反饋該問題時已經(jīng)快周末了乱豆,按 PM 的流程我們需在下周一封包給兼容測試部門做質(zhì)量測試奖恰,這個問題就必須在周一前解決。

第一反應GG宛裕,感覺是個大坑瑟啃。立刻借了兩臺 4.4 的機型對發(fā)生 Crash 場景進行調(diào)試,發(fā)現(xiàn)都是 java.lang.NoClassDefFoundError揩尸。 這個crash表明找不到引用的類蛹屿,這類原本該在 主 Dex 文件中,但是主 Dex 文件中卻沒有提供這個類岩榆。
第一反應就是 “難道我們沒有 keep 住這個類嗎错负?” 經(jīng)過排查確定是構(gòu)建工具已經(jīng)把執(zhí)行了打包該類的邏輯,卻因為某些原因沒有被打進去勇边。我嘗試使用 mutilDexKeepProguard keep 住這個類犹撒,然后編譯直接不通過了。收到的異常為:

D8: Cannot fit requested classes in the main-dex file (# methods: 87855 > 65536 ; # fields: 74641 > 65536)

當然有了 LOG 信息就有了解決問題的希望了粒褒。

定位問題

Dex 文件規(guī)范明確指出:單個 dex 文件內(nèi)引用的方法總數(shù)只能為 65536识颊。而這個限制來源于是 davilk 指令中調(diào)用方法的引用索引數(shù)值,該數(shù)值采用 16 位 二進制記錄奕坟,也就是 2^16 = 65536祥款,方法數(shù)包括了 Android Framework 層方法清笨,第三方庫方法及應用代碼方法。

所謂 主dex镰踏,其實就是 classes.dex函筋。還可能存在 classes1.dex,classes2.dex...classesN.dex。因為完整項目可能包含超過 65536 個方法奠伪,所以需要對項目的 class 進行分 dex 打包跌帐。主dex 會被最先加載,必須包含啟動引用所需要的類及“依賴類”(后面會有詳細介紹)绊率。而我所遇到的問題就是 “包含啟動引用所需要的類及“依賴類包含的方法數(shù)” 超過 65536 個谨敛,構(gòu)建系統(tǒng)就 “罷工” 不干了。

事實上滤否,在 minsdkVersion >= 21 的應用環(huán)境下是不會出現(xiàn)這種異常的脸狸。因為構(gòu)建apk時方法數(shù)雖超過 65536 必須分包處理,但由于使用 ART 運行的設備在加載 APK 時會加載多個 dex 文件藐俺。其在安裝時執(zhí)行預編譯炊甲,掃描 classesN.dex 文件,并把他們編譯成單個.oat 文件欲芹。所以 “包含啟動引用所需要的類及“依賴類” 可以散落在不同的 dex 文件上卿啡。

但是 minsdkVersion < 21 就不一樣了,5.0 以下的機型用的是 Dalvik 虛擬機菱父,在安裝時僅僅會對 主dex 做編譯優(yōu)化颈娜,啟動時直接加載 主dex。如果必要的類被散落到其他未加載的dex中浙宜,則會出現(xiàn)crash官辽。也就是開頭所說的 java.lang.NoClassDefFoundError

關于這個 exception 和 java.lang.ClassNoFoundError 很像粟瞬,但是有比較大的區(qū)別同仆,后者在 Android中常見于混淆引起類無法找到所致。

尋找解決方案

明白了上述背景之后裙品,就要想辦法減少 主dex 里的類且確保應用能夠正常啟動俗批。

但是官方只告訴我們 “如何 Keep 類來新增主 dex 里面的類”,但是沒有告訴我們怎么減少啊 清酥!臥槽了...

于是開始 Google + 各種 github/issue 查看關于如何避免 主dex 方法爆炸的方案,全都是幾年前的文章蕴侣,這些文章出奇一致地告訴你焰轻。

“盡量避免在application中引用太多第三方開源庫或者避免為了一些簡單的功能而引入一個較大的庫”

“四大組件會被打包進 classes.dex”

首先我覺得很無奈,無法知道構(gòu)建系統(tǒng)是如何將四大組件打包進 classes.dex昆雀,項目內(nèi)的代碼無從考證辱志。其次在版本 feature 已經(jīng)驗收完畢之下我無法直接對啟動的依賴樹進行調(diào)整蝠筑,且業(yè)務迭代很久的前提下刪除或者移動一個啟動依賴是風險很大的改動。

我非常努力且小心翼翼地優(yōu)化揩懒,再跑一下什乙。

D8: Cannot fit requested classes in the main-dex file (# methods:87463 > 65536 ; # fields: 74531 > 65536)

此時的我非常絕望,按照這樣優(yōu)化不可能降低到 65536 以下已球。

在這里臣镣,我花費了很多時間在嘗試網(wǎng)上所說的各種方案。 我很難用 “浪費” 來描述對這段時間的使用智亮,因為如果不是這樣忆某,我可能不會意識到對待這類問題上我的做法可能是錯誤的,并指導我以后應該這樣做阔蛉。

“被迫”啃下源碼

既然是從 .class 到生成 .dex 環(huán)節(jié)出現(xiàn)了問題弃舒,那就只能從構(gòu)建流程中該環(huán)節(jié)切入去熟悉。 項目用的是 AGP3.4.1 版本状原,開始從 Transform 方向去嘗試解惑:從 Gradle 源碼 中嘗試跟蹤并找到一下問題的答案聋呢。主要要解決的問題有:

  1. 處理分包的 Transform 是哪個,主要做了什么
  2. 影響 maindexlist 最終的 keep 邏輯是怎么確定的 颠区? 構(gòu)建系統(tǒng)本身 keep 了哪些削锰,開發(fā)者可以 keep 哪些?
  3. 從上游輸入中接受的 clasee 是怎么根據(jù) keep 邏輯進行過濾的
  4. maindexlist 文件是什么時候生成的瓦呼,在哪里生成喂窟。

跟源碼比較痛苦,特別是 Gradle 源碼不支持跳轉(zhuǎn)所以只能一個一個類手動查央串,有些邏輯甚至要看上四五遍磨澡。

下面流程只列出核心步驟及方法。

尋找分包 Transform

在應用構(gòu)建流程中质和,會經(jīng)歷 “評估” 階段稳摄。當 apply com.android.application 插件之后,評估前后會經(jīng)歷以下流程

1. com.android.build.gradle.BasePlugin#apply()
2. com.android.build.gradle.BasePlugin#basePluginApply()
3. com.android.build.gradle.BasePlugin#createTasks()
4. com.android.build.gradle.BasePlugin#createAndroidTasks()
5. **com.android.build.gradle.internal.VariantManager#createAndroidTasks(**) //重點關注一
6. com.android.build.gradle.internal.VariantManager#createTasksForVariantData()
7. com.android.build.gradle.internal.ApplicationTaskManager#createTasksForVariantScope()  
8. com.android.build.gradle.internal.ApplicationTaskManager#addCompileTask() 
9. **com.android.build.gradle.internal.TaskManager#createPostCompilationTasks()** //重點關注二
10. com.android.build.gradle.internal.pipeline.TransformManager#addTransform() 

上述流程有兩個點留意:

  1. 知道 VariantManager#createAndroidTasks 開始構(gòu)建 Android Tasks
  2. TaskManager#createPostCompilationTasks方法 為某一個構(gòu)建場景添加 Task饲宿,其中包含了支持 Multi-Dex 的 Task

Multi-Dex support 核心代碼如下

D8MainDexListTransform multiDexTransform = new D8MainDexListTransform(variantScope);
transformManager.addTransform(taskFactory, variantScope, multiDexTransform,
        taskName -> {
            File mainDexListFile =
                    variantScope
                            .getArtifacts()
                            .appendArtifact(
                                    InternalArtifactType.LEGACY_MULTIDEX_MAIN_DEX_LIST,
                                    taskName,
                                    "mainDexList.txt");
            multiDexTransform.setMainDexListOutputFile(mainDexListFile);
        }, null, variantScope::addColdSwapBuildTask);

transformManager#addTransform 一共有6個參數(shù)

  • 第三個為 multiDexTransform 對象
  • 第四個為 預配置的任務厦酬,用于生成 mainDexList.txt 的 action,其實就是為了延遲創(chuàng)建任務的瘫想,用于設置 mainDexList.txt 文件路徑仗阅。

到這里,開始有點頭緒了国夜。

D8MainDexListTransform 做了什么减噪?

D8MainDexListTransform 的構(gòu)造器參數(shù)很關鍵。

class D8MainDexListTransform(
        private val manifestProguardRules: BuildableArtifact,
        private val userProguardRules: Path? = null,
        private val userClasses: Path? = null,
        private val includeDynamicFeatures: Boolean = false,
        private val bootClasspath: Supplier<List<Path>>,
        private val messageReceiver: MessageReceiver) : Transform(), MainDexListWriter {}
  1. manifestProguardRules 為 aapt 混淆規(guī)則,編譯時產(chǎn)生在 build/intermediates/legacy_multidex_appt_derived_proguard_rules 目錄下的 manifest_keep.txt
  2. userProguardRules 為項目 multiDexKeepProguard 申明的 keep 規(guī)則
  3. userClasses 為項目 multiDexKeepFile 申明的 keep class

這三份文件都會影響最終決定那些 class 會被打到 clesses.dex 中筹裕,邏輯在 transform方法 里面:

override fun transform(invocation: TransformInvocation) {
    try {
        val inputs = getByInputType(invocation)
        val programFiles = inputs[ProguardInput.INPUT_JAR]!!
        val libraryFiles = inputs[ProguardInput.LIBRARY_JAR]!! + bootClasspath.get()
         // 1 處
        val proguardRules =listOfNotNull(manifestProguardRules.singleFile().toPath(), userProguardRules)
        val mainDexClasses = mutableSetOf<String>()
        //  2 處
        mainDexClasses.addAll(
            D8MainDexList.generate(
                getPlatformRules(),
                proguardRules,
                programFiles,
                libraryFiles,
                messageReceiver
            )
        )
        // 3 處
        if (userClasses != null) {
            mainDexClasses.addAll(Files.readAllLines(userClasses))
        }
        Files.deleteIfExists(outputMainDexList)
        // 4處
        Files.write(outputMainDexList, mainDexClasses)
    } catch (e: D8MainDexList.MainDexListException) {
        throw TransformException("Error while generating the main dex list:${System.lineSeparator()}${e.message}", e)
    }
}

第一處代碼拿到 multiDexKeepProguard keep 規(guī)則.

第二處代碼使用 D8MainDexList#generate方法 生成所有需要 keep 在 classes.dex 的 class 集合醋闭, getPlatformRules方法 中強制寫死了一些規(guī)則。

internal fun getPlatformRules(): List<String> = listOf(
        "-keep public class * extends android.app.Instrumentation {\n"
                        + "  <init>(); \n"
                        + "  void onCreate(...);\n"
                        + "  android.app.Application newApplication(...);\n"
                        + "  void callApplicationOnCreate(android.app.Application);\n"
                        + "  Z onException(java.lang.Object, java.lang.Throwable);\n"
                        + "}",
        "-keep public class * extends android.app.Application { "
                        + "  <init>();\n"
                        + "  void attachBaseContext(android.content.Context);\n"
                        + "}",
        "-keep public class * extends android.app.backup.BackupAgent { <init>(); }",
        "-keep public class * implements java.lang.annotation.Annotation { *;}",
        "-keep public class * extends android.test.InstrumentationTestCase { <init>(); }"
)

第三處代碼把 multiDexKeepFile 申明需要保留的 class 添加到 2 步驟生成的集合中

第四出代碼最終輸入到 outputMainDexList 朝卒,這個文件就是在添加 D8MainDexListTransform 的時候預設置的 mainDexList.txt证逻,保存在 build/intermediates/legacymultidexmaindexlist 目錄下。

到這里抗斤,如果想辦法在勾住 mainDexList.txt則在真正打包 classes.dex 之前修改文件時應該能保證方法數(shù)控制在 65536 之下囚企。我們項目中使用了 tinkertinker 也 keep 了一些類到 classes.dex豪治。從 multiDexKeepProguard/multiDexKeepFile 手段上不存在操作空間洞拨,因為這些是業(yè)務硬要求的邏輯。只能看編譯之后生成的 mainDexList.txt负拟,然后憑借經(jīng)驗去掉一些看起來可能 “前期不需要” 的 class烦衣,但稍微不慎都有可能導致 crash 產(chǎn)生。

尋找明確的 “Keep” 鏈

希望能從代碼邏輯上得到 “更為明確的指導”掩浙,就得了解下為啥 D8 構(gòu)建流程花吟, 為啥 keep 了那么多類,這些類是否存在刪減的空間厨姚。

但是我在 gradle 源碼中并沒有找到 D8MainDexList.javagenerate方法 相關信息衅澈,它被放到 build-system 的另一個目錄中,核心邏輯如下谬墙。

public static List<String> generate(
        @NonNull List<String> mainDexRules,     
        @NonNull List<Path> mainDexRulesFiles,
        @NonNull Collection<Path> programFiles,
        @NonNull Collection<Path> libraryFiles,
        @NonNull MessageReceiver messageReceiver)
        throws MainDexListException {
    D8DiagnosticsHandler d8DiagnosticsHandler =
            new InterceptingDiagnosticsHandler(messageReceiver);
    try {
        GenerateMainDexListCommand.Builder command =
                GenerateMainDexListCommand.builder(d8DiagnosticsHandler)
                        .addMainDexRules(mainDexRules, Origin.unknown()) //d8強制寫死的規(guī)則
                        .addMainDexRulesFiles(mainDexRulesFiles) //開發(fā)者通過 multiDexKeepProguard 添加的規(guī)則
                        .addLibraryFiles(libraryFiles);
        for (Path program : programFiles) {
            if (Files.isRegularFile(program)) {
                command.addProgramFiles(program);
            } else {
                try (Stream<Path> classFiles = Files.walk(program)) {
                    List<Path> allClasses = classFiles
                            .filter(p -> p.toString().endsWith(SdkConstants.DOT_CLASS))
                            .collect(Collectors.toList());
                    command.addProgramFiles(allClasses);
                }
            }
        }
          //最終調(diào)用 GenerateMainDexList#run
        return ImmutableList.copyOf(
                GenerateMainDexList.run(command.build(), ForkJoinPool.commonPool()));
    } catch (Exception e) {
        throw getExceptionToRethrow(e, d8DiagnosticsHandler);
    }
}

上述最終通過構(gòu)建 GenerateMainDexListCommand 對象并傳遞給 GenerateMainDexList 執(zhí)行今布。 這兩個類在我們本地 AndroidSdk 里,路徑為 {AndroidSdk}/build-tools/{buildToolsVersion}/lib/d8.jar 中拭抬,可通過 JD_GUI 工具查看部默。

GenerateMainDexListCommandBuilder#build方法 在構(gòu)建對象的時候做了以下工作:

  1. 構(gòu)建 DexItemFactory 工廠對象,用于構(gòu)建 DexString造虎,DexMethod 等相關 dex 信息
  2. 預處理了規(guī)則文件傅蹂,比如刪除 “#” 注解相關等,解析成 ProguardConfigurationRule 對象集
  3. 構(gòu)建 AndroidApp 對象算凿,用于記錄程序資源的信息份蝴,比如 dexClass,libraryResource 等等

最終傳遞 AndroidApp 對象給 GenerateMainDexList#run方法 調(diào)用氓轰。

private List<String> run(AndroidApp app, ExecutorService executor) throws IOException, ExecutionException {
    // 步驟一
    DirectMappedDexApplication directMappedDexApplication =
         (new ApplicationReader(app, this.options,  this.timing)).read(executor).toDirect();
    // 步驟二
    AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping((DexApplication)directMappedDexApplication);
    // 步驟三
    RootSetBuilder.RootSet mainDexRootSet = 
        (new RootSetBuilder((DexApplication)directMappedDexApplication, (AppInfo)appInfo, (List)this.options.mainDexKeepRules, this.options)).run(executor);
    Enqueuer enqueuer = new Enqueuer(appInfo, this.options, true);
    Enqueuer.AppInfoWithLiveness mainDexAppInfo = enqueuer.traceMainDex(mainDexRootSet, this.timing);
    // 步驟四
    Set<DexType> mainDexClasses = (new MainDexListBuilder(new HashSet(mainDexAppInfo.liveTypes),        (DexApplication)directMappedDexApplication)).run();
    List<String> result = (List<String>)mainDexClasses.stream().map(c -> c.toSourceString().replace('.', '/') +             ".class").sorted().collect(Collectors.toList());
    if (this.options.mainDexListConsumer != null)
        this.options.mainDexListConsumer.accept(String.join("\n", (Iterable)result), (DiagnosticsHandler)this.options.reporter); 
    if (mainDexRootSet.reasonAsked.size() > 0) {
        TreePruner pruner = new TreePruner((DexApplication)directMappedDexApplication, mainDexAppInfo.withLiveness(), this.options);
        DexApplication dexApplication = pruner.run();
        ReasonPrinter reasonPrinter = enqueuer.getReasonPrinter(mainDexRootSet.reasonAsked);
        reasonPrinter.run(dexApplication);
    } 
    return result;
}
  • 步驟一婚夫,構(gòu)建了 ApplicationReader 對象,阻塞等待 read方法 讀取了所有程序的資源署鸡,如果是存在 .dex 資源案糙,則歸類到 dex 類型镐躲;如果存在 class 類型,則歸到 class 類型(但是過濾了 module-info.class 的文件)侍筛。這部分邏輯可在 com.android.tools.r8.util.FilteredArchiveProgramResourceProvider 查看。dex 類型使用 dex 格式解析撒穷,class 類型使用字節(jié)碼格式解析之后保存到 directMappedDexApplication 對象中匣椰。
  • 步驟二 AppInfoWithSubtyping 讀取了 directMappedDexApplication,計算并設置類的 super/sub 關系端礼。
  • 步驟三 把所有收集到的類信息及類的 super/sub 關系禽笑,及 keep 的規(guī)則傳遞給 RootSetBuilder 用于計算 Root 集合,該集合決定哪些類將最終被 keep 到 classes.dex 里面蛤奥。經(jīng)過匹配混淆之后獲得 Root 集合之后佳镜,調(diào)用 run() 進行向下檢索。主要是計算 Root 集合內(nèi)的 class 的依賴及使用枚舉作為運行時注解類凡桥。
  • 步驟四 根據(jù) Root 集合蟀伸,按照以下兩個方法順序檢索得到 mainDexClass 集合,方法邏輯如下缅刽。
    1. traceMainDexDirectDependencies方法
      • 添加 Root 節(jié)點 class啊掏,添加其所有父類及接口;
      • 添加 Root 節(jié)點 class 中靜態(tài)變量衰猛,成員變量迟蜜;
      • 添加 Root 節(jié)點 class 中的方法的參數(shù)類型的 class暖庄,返回值類型對應的 class涎才;
      • 收集 Root 節(jié)點 class 的注解。
    2. traceRuntimeAnnotationsWithEnumForMainDex方法
      • 所有類中掘托,如果 class 是注解類型且使用枚舉類卦睹,則收集畦戒;
      • 所有類中,如果 class 使用了上一條規(guī)則的枚舉類且枚舉可見分预,則也收集兢交。

因此,最終生成的集合笼痹,會在 D8MainDexListTransform#transform方法 中合并存在的 multiDexKeepFile 規(guī)則配喳,并最終寫到 build/intermediates/legacymltidexmaindexlist/ 目錄下的 maindexlist.txt 文件。

嘗試新方案

那么 D8MainDexListTransform 能夠被我勾住使用呢凳干? 當然可以晴裹。 找到 D8MainDexListTransform 對應的 Task,可以通過 project.tasks.findByName 來獲取 task 對象救赐,然后在 gradle 腳本中監(jiān)聽這個 task 的執(zhí)行涧团,在 task 結(jié)束之后并返回結(jié)果之前插入我們自定義的 task只磷,可通過 finalizeBy 方法實現(xiàn)。

而 D8MainDexListTransform 對應 Task 的名字的邏輯通過閱讀 TransformManager#getTaskNamePrefix方法 可推斷泌绣。

把上述所有邏輯封裝成一個 gradle 腳本并在 application 模塊中 apply 就行了钮追。

project.afterEvaluate {

    println "handle main-dex by user,start..."
    if (android.defaultConfig.minSdkVersion.getApiLevel() >= 21) {
        return
    }
    println "main-dex阿迈,minSdkVersion is ${android.defaultConfig.minSdkVersion.getApiLevel()}"
    android.applicationVariants.all { variant ->

        def variantName = variant.name.capitalize()
        def multidexTask = project.tasks.findByName("transformClassesWithMultidexlistFor${variantName}")
        def exist = multidexTask != null
        println "main-dex multidexTask(transformClassesWithMultidexlistFor${variantName}) exist: ${exist}"
        
        if (exist) {
            def replaceTask = createReplaceMainDexListTask(variant);
            multidexTask.finalizedBy replaceTask
        }
    }
}

def createReplaceMainDexListTask(variant) {
    def variantName = variant.name.capitalize()

    return task("replace${variantName}MainDexClassList").doLast {

        //從主dex移除的列表
        def excludeClassList = []
        File excludeClassFile = new File("{存放剔除規(guī)則的路徑}/main_dex_exclude_class.txt")
        println "${project.projectDir}/main_dex_exclude_class.txt exist: ${excludeClassFile.exists()}"
        if (excludeClassFile.exists()) {
            excludeClassFile.eachLine { line ->
                if (!line.trim().isEmpty() && !line.startsWith("#")) {
                    excludeClassList.add(line.trim())
                }
            }
            excludeClassList.unique()
        }
        def mainDexList = []
        File mainDexFile = new File("${project.buildDir}/intermediates/legacy_multidex_main_dex_list/${variant.dirName}/transformClassesWithMultidexlistFor${variantName}/maindexlist.txt")
        println "${project.buildDir}/intermediates/legacy_multidex_main_dex_list/${variant.dirName}/transformClassesWithMultidexlistFor${variantName}/maindexlist.txt exist : ${mainDexFile.exists()}"
        //再次判斷兼容 linux/mac 環(huán)境獲取
        if(!mainDexFile.exists()){
            mainDexFile = new File("${project.buildDir}/intermediates/legacy_multidex_main_dex_list/${variant.dirName}/transformClassesWithMultidexlistFor${variantName}/mainDexList.txt")
            println "${project.buildDir}/intermediates/legacy_multidex_main_dex_list/${variant.dirName}/transformClassesWithMultidexlistFor${variantName}/mainDexList.txt exist : ${mainDexFile.exists()}"
        }
        if (mainDexFile.exists()) {
            mainDexFile.eachLine { line ->
                if (!line.isEmpty()) {
                    mainDexList.add(line.trim())
                }
            }
            mainDexList.unique()
            if (!excludeClassList.isEmpty()) {
                def newMainDexList = mainDexList.findResults { mainDexItem ->
                    def isKeepMainDexItem = true
                    for (excludeClassItem in excludeClassList) {
                        if (mainDexItem.contains(excludeClassItem)) {
                            isKeepMainDexItem = false
                            break
                        }
                    }
                    if (isKeepMainDexItem) mainDexItem else null
                }
                if (newMainDexList.size() < mainDexList.size()) {
                    mainDexFile.delete()
                    mainDexFile.createNewFile()
                    mainDexFile.withWriterAppend { writer ->
                        newMainDexList.each {
                            writer << it << '\n'
                            writer.flush()
                        }
                    }
                }
            }
        }
    }
}

main_dex_exclude_class.txt 的內(nèi)容很簡單元媚,規(guī)則和 multiDexKeepFile 是一樣的,比如:

com/facebook/fresco
com/android/activity/BaseLifeActivity.class
...

這樣就可以了苗沧,如果你找不到 D8MainDexListTransform 對應的 Task刊棕,那你應該是用了 r8 ,r8 會合并 mainDexList 的構(gòu)建流程到新的 Task待逞,你可以選擇關閉 r8 或者尋找新的 hook 點甥角,思路是一樣的。

“什么识樱,你講了一遍流程嗤无,但是還是沒有說哪些可以刪 ”

“其實,除了 D8 強制 keep 住的類和 contentProvider, 其他都可以刪怜庸∥涛。”

“但是我看到網(wǎng)上很多文章說,四大組件都要 keep 住哦”

“建議以我為準休雌≡詈”

當然,我已經(jīng)試過了杈曲,你把入口 Activity 刪除驰凛,也只是慢一些而已,只是不建議罷了担扑∏∠欤或者你可以選擇把二級頁面全部移除出去,這樣可能會大大減少 classes.dex 的方法數(shù)涌献。

最終效果: methods: 87855 > 49386胚宦。

上述分析存在錯誤歡迎指正或有更好的處理建議,歡迎評論留言哦燕垃。

解決問題很痛苦枢劝,逼著你去尋找答案,但解決之后真的爽卜壕。

專注 Android 進階技術(shù)分享您旁,記錄架構(gòu)師野蠻成長之路

后續(xù)會針對 “Android 領域的必備進階技術(shù)”,“Android高可用架構(gòu)設計及實踐” 轴捎,“業(yè)務中的疑難雜癥及解決方案” 等實用內(nèi)容進行分享鹤盒。
也會分享作為技術(shù)者如何在公司野蠻成長蚕脏,包括技術(shù)進步,職級及收入的提升侦锯。
歡迎關注來撩驼鞭。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市尺碰,隨后出現(xiàn)的幾起案子终议,更是在濱河造成了極大的恐慌,老刑警劉巖葱蝗,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異细燎,居然都是意外死亡两曼,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門玻驻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悼凑,“玉大人,你說我怎么就攤上這事璧瞬』П瑁” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵嗤锉,是天一觀的道長渔欢。 經(jīng)常有香客問我,道長瘟忱,這世上最難降的妖魔是什么奥额? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮访诱,結(jié)果婚禮上垫挨,老公的妹妹穿的比我還像新娘。我一直安慰自己触菜,他們只是感情好九榔,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著涡相,像睡著了一般哲泊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上催蝗,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天攻旦,我揣著相機與錄音,去河邊找鬼生逸。 笑死牢屋,一個胖子當著我的面吹牛且预,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播烙无,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锋谐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了截酷?” 一聲冷哼從身側(cè)響起涮拗,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎迂苛,沒想到半個月后三热,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡三幻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年就漾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片念搬。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡抑堡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出朗徊,到底是詐尸還是另有隱情首妖,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布爷恳,位于F島的核電站有缆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏温亲。R本人自食惡果不足惜妒貌,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望铸豁。 院中可真熱鬧灌曙,春花似錦、人聲如沸节芥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽头镊。三九已至蚣驼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間相艇,已是汗流浹背颖杏。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留坛芽,地道東北人留储。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓翼抠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親获讳。 傳聞我的和親對象是個殘疾皇子阴颖,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

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