How did Android multidex master dex come from?

Keywords: Android Gradle Java SDK

First, mention the key products in the compilation process of gradle assemble Debug
In the build/intermediates/multi-dex/debug directory, you can see the following files


image.png

CompoonentClasses. Jar - > obtained by shrinkWithProguard (jarOfRoots.jar)
components.flags
Maindexlist. txt - > Through some column operations, a list is calculated and recorded all class es put into the main dex. (Say bad, if you can hook off the writing of this file, then who you want in the main dex, who is in the main dex)
manifest_keep.txt

First: Depending on two core modules

1. Gradle plug-in (this article is based on 3.0.0) (compilation project)

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

Two points will be discussed later:

    1. AndroidBuilder.createMainDexList
    1. MultiDexTransform.transform

2. dalvik-dx (processing and dex related logic)

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

A place will be discussed later.

    1. ClassReferenceListBuilder

2: The core process of generating master dex

1. Gradle Plug-in Chapter

  • AndroidBuilder

Collect basic compilation information such as build.gradle for our project configuration and subsequent createMainDexList

  • MultiDexTransform -> transform :

Core Code Entry

     @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 After all the class files are processed by Proguard (run Proguard), jarOfRoots.jar is obtained. 
            shrinkWithProguard(input);
           //--> 2 Calculates the mainDexList by generating rootJars.jar from the previous step
            computeList(input);
        } catch (ParseException | ProcessException e) {
            throw new TransformException(e);
        }
    }
  • MultiDexTransform -> transform -> shrinkWithProguard

After all the class files are processed by Proguard (run Proguard), jarOfRoots.jar, namely variantScope. getProguardComponents JarFile ()

    private void shrinkWithProguard(@NonNull File input) throws IOException, ParseException {
         //--> 1 A bunch of confusing configurations...
        configuration.obfuscate = false;
        configuration.optimize = false;
        configuration.preverify = false;
        dontwarn();
        dontnote();
        forceprocessing();

       //--> 2 Add the contents in manifest_keep.txt
        applyConfigurationFile(manifestKeepListProguardFile);
        if (userMainDexKeepProguard != null) {
             //--> 3 If you have a custom keep (multiDexKeepProguard file('./maindex-rules.pro') that you want to put into the main dex in your project, add it as well.
            applyConfigurationFile(userMainDexKeepProguard);
        }
        //--> 43.0.0 plug-ins help us keep some by default
        // 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 Introduce Android's jar into the path
        // handle inputs
        libraryJar(findShrinkedAndroidJar());
        //--> 6 Introduce all class es into path
        inJar(input, null);
        //--> 7 Setting Product Path
        // outputs.
        outJar(variantScope.getProguardComponentsJarFile());
        printconfiguration(configFileOut);
       
         //--> 8 Confusion of Final Implementation
        // run proguard
        runProguard();
    }
  • MultiDexTransform -> transform -> computeList

After the jarOfRoots.jar generated in the previous step and all classes are processed by callDx, the mainDexClasses are calculated. If you configure the classes that need to be placed in the main dex (MainDexKeepFile) in the project, it will be read out and appended to the main DexClasses, and finally written to a main DexListFile file.

    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, as its name implies, calls Dex to return a list that needs to be placed in the main dex, and in fact eventually calls the Android Builder - > createMainDexList just mentioned.

  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.");
        }
      //This will eventually call the `Android Builder-> createMainDexList just mentioned.`
        return variantScope.getGlobalScope().getAndroidBuilder().createMainDexList(
                allClassesJarFile, jarOfRoots, mainDexListOptions);
    }
  • AndroidBuilder -> createMainDexList

Call the ClassReferenceListBuilder in dex.jar to find out which classes need to be placed in the main dex, and the parameters that need to be passed in are all the class files, the jarOfRoots.jar obtained after shrinkWithProguard, and a MainDexListOption configuration.

    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

The gradle plug-in has jarOfRoots and all the class files ready, and now the dex comes on.

  • ClassReferenceListBuilder

In fact, we can find a script mainDexClasses under build-tools in our SDK environment, such as build-tools/26.0.2. There is a call on the last line, which is similar to the previous gradle plug-in.
java -cp "$jarpath" com.android.multidex.MainDexListBuilder ${disableKeepAnnotated} "${tmpOut}" ${@} || exit 11

What is the function of this class? Find out the reference to class as the name implies. So how do you do it?

  • ClassReferenceListBuilder -> main
    First, look at the main entrance. The main function does three things:
  1. Get the jarOfRoots.jar you just passed in
  2. Get all the class files you just passed in
  3. Starting with work, we built a ClassReference List Builder and called addRoots

The code is as follows:

   public static void main(String[] args) {
         ......
      // 1. Get the jarOfRoots.jar you just passed in
        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. Get all the class files you just passed in
            path = new Path(args[1]);
          // 3. Build a ClassReference List Builder
            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

Keep the class files in jarOfRoots and the class files that these classes depend on directly from all the class files just passed in and keep them. This constitutes a main list of classes that need to be kept to generate the main 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());
            }
        }
    }

Posted by R1der on Thu, 16 May 2019 07:58:04 -0700