Android multidex 主dex是怎么来的?

先提一下(gradle assembleDebug)编译过程中关键产物
build/intermediates/multi-dex/debug 目录中,可以看到如下几个文件


image.png

componentClasses.jar ----> 经过shrinkWithProguard得到(jarOfRoots.jar)
components.flags
maindexlist.txt ----> 通过一些列操作,计算出来的一个列表,记录放入主dex中的所有class (说个不好听的如果你能hook掉这个文件的写入,那么想让谁在主dex, 谁就在主dex)
manifest_keep.txt

一: 依赖两个核心模块

1. gradle 插件 (本文基于3.0.0)(编译项目)

https://android.googlesource.com/platform/tools/gradle/+/gradle_3.0.0

后面会讨论两个地方:

    1. AndroidBuilder.createMainDexList
    1. MultiDexTransform.transform

2. dalvik-dx (处理和dex相关逻辑)

https://android.googlesource.com/platform/dalvik/+/6a8e552/dx

后面会讨论一个地方

    1. ClassReferenceListBuilder

二: 生成主dex的核心流程

1. gradle 插件篇

  • AndroidBuilder

收集我们项目配置的build.gradle等基本编译信息,以及后续的 createMainDexList
路径在: gradle_3.0.0_base-5d179ada33c355edb5c7da0f5f98321116476ce0/build-system/builder/src/main/java/com/android/builder/core/AndroidBuilder.java

  • MultiDexTransform -> transform :

TaskManager会去构建Android编译需要的所需task,这里会初始化MultiDexTransform,路径在:gradle_3.0.0_base-5d179ada33c355edb5c7da0f5f98321116476ce0/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/transforms/MultiDexTransform.java 核心代码入口

     @Override
    public void transform(@NonNull TransformInvocation invocation)
            throws IOException, TransformException, InterruptedException {
        // Re-direct the output to appropriate log levels, just like the official ProGuard task.
        LoggingManager loggingManager = invocation.getContext().getLogging();
        loggingManager.captureStandardOutput(LogLevel.INFO);
        loggingManager.captureStandardError(LogLevel.WARN);

        try {
            File input = verifyInputs(invocation.getReferencedInputs());
            //-->1 把所有的class文件经过 Proguard (runProguard)处理之后,得到 jarOfRoots.jar 
            shrinkWithProguard(input);
           //-->2 通过上一步生成的 rootJars.jar, 计算出mainDexList
            computeList(input);
        } catch (ParseException | ProcessException e) {
            throw new TransformException(e);
        }
    }
  • MultiDexTransform -> transform -> shrinkWithProguard

把所有的class文件经过 Proguard (runProguard)处理之后,得到 jarOfRoots.jar ,即:variantScope.getProguardComponentsJarFile()

    private void shrinkWithProguard(@NonNull File input) throws IOException, ParseException {
         //-->1 一大堆混淆的配置。。。
        configuration.obfuscate = false;
        configuration.optimize = false;
        configuration.preverify = false;
        dontwarn();
        dontnote();
        forceprocessing();

       //-->2 把manifest_keep.txt中的内容加过来
        applyConfigurationFile(manifestKeepListProguardFile);
        if (userMainDexKeepProguard != null) {
             //-->3 如果在项目中有自定义想放入主dex的keep(multiDexKeepProguard file('./maindex-rules.pro')),也追加进来
            applyConfigurationFile(userMainDexKeepProguard);
        }
        //-->4  3.0.0的插件默认就帮我们keep了一些
        // add a couple of rules that cannot be easily parsed from the manifest.
        keep("public class * extends android.app.Instrumentation { <init>(); }");
        keep("public class * extends android.app.Application { "
                + "  <init>(); "
                + "  void attachBaseContext(android.content.Context);"
                + "}");
        keep("public class * extends android.app.backup.BackupAgent { <init>(); }");
        keep("public class * extends java.lang.annotation.Annotation { *;}");
        keep("class com.android.tools.ir.** {*;}"); // Instant run.
       
        //-->5 把 shrinkedAndroid.jar 和刚刚的 input 文件都加入 classpath 里。
        // handle inputs
        libraryJar(findShrinkedAndroidJar());
        //-->6 把所有的class引入进来放入path中
        inJar(input, null);
        //-->7 设置产物的路径
        // outputs.
        outJar(variantScope.getProguardComponentsJarFile());
        printconfiguration(configFileOut);
       
         //-->8 最终执行混淆
        // run proguard
        runProguard();
    }
  • MultiDexTransform -> transform -> computeList

把上一步生成 jarOfRoots.jar 以及所有的class 通过 callDx 处理之后,计算出mainDexClasses, 如果项目中自己配置了需要放在主dex的类(MainDexKeepFile),这里会读取出来追加到mainDexClasses中, 最终写到一个mainDexListFile文件中

    private void computeList(File _allClassesJarFile) throws ProcessException, IOException {
        // manifest components plus immediate dependencies must be in the main dex.
        Set<String> mainDexClasses = callDx(
                _allClassesJarFile,
                variantScope.getProguardComponentsJarFile());

        if (userMainDexKeepFile != null) {
            mainDexClasses = ImmutableSet.<String>builder()
                    .addAll(mainDexClasses)
                    .addAll(Files.readLines(userMainDexKeepFile, Charsets.UTF_8))
                    .build();
        }

        String fileContent = Joiner.on(System.getProperty("line.separator")).join(mainDexClasses);

        Files.write(fileContent, mainDexListFile, Charsets.UTF_8);

    }
  • MultiDexTransform -> transform -> computeList -> callDx

callDex 顾名思义就是调用 Dex 返回一个需要放在主dex的列表, 其实最终又会调用刚才提到的AndroidBuilder -> createMainDexList

  private Set<String> callDx(File allClassesJarFile, File jarOfRoots) throws ProcessException {
        EnumSet<AndroidBuilder.MainDexListOption> mainDexListOptions =
                EnumSet.noneOf(AndroidBuilder.MainDexListOption.class);
        if (!keepRuntimeAnnotatedClasses) {
            mainDexListOptions.add(
                    AndroidBuilder.MainDexListOption.DISABLE_ANNOTATION_RESOLUTION_WORKAROUND);
            Logging.getLogger(MultiDexTransform.class).warn(
                    "Not including classes with runtime retention annotations in the main dex.\n"
                            + "This can cause issues with reflection in older platforms.");
        }
      //这里 最终又会调用刚才提到的`AndroidBuilder -> createMainDexList`
        return variantScope.getGlobalScope().getAndroidBuilder().createMainDexList(
                allClassesJarFile, jarOfRoots, mainDexListOptions);
    }
  • AndroidBuilder -> createMainDexList

调用 dex.jar 中的 ClassReferenceListBuilder ,找出哪些需要放在主dex中的class,需要传入的参数是所有的class文件、通过 shrinkWithProguard 之后得到的jarOfRoots.jar 以及一个MainDexListOption配置

    public Set<String> createMainDexList(
            @NonNull File allClassesJarFile,
            @NonNull File jarOfRoots,
            @NonNull EnumSet<MainDexListOption> options) throws ProcessException {

        BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
        ProcessInfoBuilder builder = new ProcessInfoBuilder();

        String dx = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);
        if (dx == null || !new File(dx).isFile()) {
            throw new IllegalStateException("dx.jar is missing");
        }

        builder.setClasspath(dx);
        builder.setMain("com.android.multidex.ClassReferenceListBuilder");

        if (options.contains(MainDexListOption.DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) {
            builder.addArgs("--disable-annotation-resolution-workaround");
        }

        builder.addArgs(jarOfRoots.getAbsolutePath());
        builder.addArgs(allClassesJarFile.getAbsolutePath());

        CachedProcessOutputHandler processOutputHandler = new CachedProcessOutputHandler();

        mJavaProcessExecutor.execute(builder.createJavaProcess(), processOutputHandler)
                .rethrowFailure()
                .assertNormalExitValue();

        LineCollector lineCollector = new LineCollector();
        processOutputHandler.getProcessOutput().processStandardOutputLines(lineCollector);
        return ImmutableSet.copyOf(lineCollector.getResult());
    }

2. dalvik-dx 篇

gradle 插件中已经准备好了jarOfRoots和所有class文件,现在该dex上场了的。

  • ClassReferenceListBuilder

路径在: dalvik-6a8e552-dx/src/com/android/multidex/ClassReferenceListBuilder.java 其实在我们的SDK环境中的build-tools下能找到一个脚本 mainDexClasses ,比如build-tools/26.0.2。里面最后一行就有调用,调用方式和刚才的gradle插件类似。
java -cp "$jarpath" com.android.multidex.MainDexListBuilder ${disableKeepAnnotated} "${tmpOut}" ${@} || exit 11

这个类的作用是什么呢?顾名思义找出class的引用。那么怎么做到的呢?

  • ClassReferenceListBuilder -> main
    先来看主入口,main函数做了三件事情:
  1. 拿到刚才传入的 jarOfRoots.jar
  2. 拿到刚才传入的所有class文件
  3. 开始干活了的 构建了一个ClassReferenceListBuilder,调用addRoots

代码如下:

   public static void main(String[] args) {
         ......
      // 1. 拿到刚才传入的 jarOfRoots.jar
        ZipFile jarOfRoots;
        try {
            jarOfRoots = new ZipFile(args[0]);
        } catch (IOException e) {
            System.err.println("\"" + args[0] + "\" can not be read as a zip archive. ("
                    + e.getMessage() + ")");
            System.exit(STATUS_ERROR);
            return;
        }

    
        Path path = null;
        try {
          // 2. 拿到刚才传入的所有class文件
            path = new Path(args[1]);
          // 3. 开始干活了的 构建了一个ClassReferenceListBuilder
            ClassReferenceListBuilder builder = new ClassReferenceListBuilder(path);
            builder.addRoots(jarOfRoots);

            printList(builder.toKeep);
        } catch (IOException e) {
            System.err.println("A fatal error occured: " + e.getMessage());
            System.exit(STATUS_ERROR);
            return;
        } finally {
            try {
                jarOfRoots.close();
            } catch (IOException e) {
                // ignore
            }
            if (path != null) {
                for (ClassPathElement element : path.elements) {
                    try {
                        element.close();
                    } catch (IOException e) {
                        // keep going, lets do our best.
                    }
                }
            }
        }
    }
  • ClassReferenceListBuilder -> main -> addRoots

把 jarOfRoots中的class文件都keep住,以及这些class直接依赖的class文件从刚才传入的所有class文件中找出来并且也keep住。这样就构成了一个主的需要keep的class列表用以生成主dex.

    public void addRoots(ZipFile jarOfRoots) throws IOException {

        // keep roots
        for (Enumeration<? extends ZipEntry> entries = jarOfRoots.entries();
                entries.hasMoreElements();) {
            ZipEntry entry = entries.nextElement();
            String name = entry.getName();
            if (name.endsWith(CLASS_EXTENSION)) {
                toKeep.add(name.substring(0, name.length() - CLASS_EXTENSION.length()));
            }
        }

        // keep direct references of roots (+ direct references hierarchy)
        for (Enumeration<? extends ZipEntry> entries = jarOfRoots.entries();
                entries.hasMoreElements();) {
            ZipEntry entry = entries.nextElement();
            String name = entry.getName();
            if (name.endsWith(CLASS_EXTENSION)) {
                DirectClassFile classFile;
                try {
                    classFile = path.getClass(name);
                } catch (FileNotFoundException e) {
                    throw new IOException("Class " + name +
                            " is missing form original class path " + path, e);
                }

                addDependencies(classFile.getConstantPool());
            }
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容