忽视了 Aidl, Shader, Ndk, Jni, Jack, DataBinding, StripNativeLibrary, Split, InstantRun, Lint 这些 Task 之后,我们就只剩下面三个重要的 Task 了:

  • createJavacTask(tasks, variantScope);
  • createPostCompilationTasks(tasks, variantScope);
  • createPackagingTask(tasks, variantScope, true /publishApk/, fullBuildInfoGeneratorTask);

这篇文章我们来分析前面两个 Task

createJavacTask

这个 Task 的方法为:

public AndroidTask<? extends JavaCompile> createJavacTask(
        @NonNull final TaskFactory tasks,
        @NonNull final VariantScope scope) {
    final BaseVariantData<? extends BaseVariantOutputData> variantData = scope.getVariantData();
    // 这是AndroidJavaCompileTask的前置Task,如果生成的源码有变动,则清空AndroidJavaCompile的输出目录,强制重新编译
    // 这个文件为  build/intermediates/incremental-safeguard/tag.txt
    AndroidTask<IncrementalSafeguard> javacIncrementalSafeguard = androidTasks.create(tasks,
            new IncrementalSafeguard.ConfigAction(scope));

    final AndroidTask<? extends JavaCompile> javacTask = androidTasks.create(tasks,
            new JavaCompileConfigAction(scope));
    scope.setJavacTask(javacTask);
    javacTask.dependsOn(tasks, javacIncrementalSafeguard);

    setupCompileTaskDependencies(tasks, scope, javacTask);

    // Create jar task for uses by external modules.
    // 还没遇见过,暂时忽视
    // 生成的文件路径在 build/intermediates/classes-jar/${buildType}/class.jar
    if (variantData.getVariantDependency().getClassesConfiguration() != null) {
        AndroidTask<Jar> packageJarArtifact =
                androidTasks.create(tasks, new PackageJarArtifactConfigAction(scope));
        packageJarArtifact.dependsOn(tasks, javacTask);
    }

    return javacTask;
}

看到这里相当于存在 3 个 Task,但是我们只要关心javacTask即可,

第一个javacIncrementalSafeguard,就是类似标识位的东东,

最后一个packageJarArtifact,因为我还没遇到使用外部 modules 的情况,所以也忽略,

大家如果在打包完成的 build 路径下看到我提到的路径,就可以在这里查看情况。

好的,那么我们来仔细看一下javacTask

public void execute(@NonNull final AndroidJavaCompile javacTask) {
    scope.getVariantData().javacTask = javacTask;
    // compileSdkVersion >= 24,需要用JDK 1.8及以上版本
    javacTask.compileSdkVersion = scope.getGlobalScope().getExtension().getCompileSdkVersion();
    javacTask.mBuildContext = scope.getInstantRunBuildContext();

    // We can't just pass the collection directly, as the instanceof check in the incremental
    // compile doesn't work recursively currently, so every ConfigurableFileTree needs to be
    // directly in the source array.
    for (ConfigurableFileTree fileTree : scope.getVariantData().getJavaSources()) {
        javacTask.source(fileTree);
    }

    // Set boot classpath if we don't need to keep the default.  Otherwise, this is added as
    // normal classpath.
    javacTask.getOptions().setBootClasspath(
            Joiner.on(File.pathSeparator).join(
                    scope.getGlobalScope().getAndroidBuilder()
                            .getBootClasspathAsStrings(false)));


    javacTask.setDestinationDir(scope.getJavaOutputDir()); // build/intermediates/classes/

    javacTask.setDependencyCacheDir(scope.getJavaDependencyCache()); // build/intermediates/dependency-cache/

    CompileOptions compileOptions = scope.getGlobalScope().getExtension().getCompileOptions();

    AbstractCompilesUtil.configureLanguageLevel(
            javacTask,
            compileOptions,
            scope.getGlobalScope().getExtension().getCompileSdkVersion(),
            scope.getVariantConfiguration().getJackOptions().isEnabled());

    javacTask.getOptions().setEncoding(compileOptions.getEncoding());

    GlobalScope globalScope = scope.getGlobalScope();
    Project project = scope.getGlobalScope().getProject();

    CoreAnnotationProcessorOptions annotationProcessorOptions =
            scope.getVariantConfiguration().getJavaCompileOptions()
                    .getAnnotationProcessorOptions();


    checkNotNull(annotationProcessorOptions.getIncludeCompileClasspath());
    Collection<File> processorPath =
            Lists.newArrayList(
                    scope.getVariantData().getVariantDependency()
                            .resolveAndGetAnnotationProcessorClassPath(
                                    annotationProcessorOptions.getIncludeCompileClasspath(),
                                    scope.getGlobalScope().getAndroidBuilder().getErrorReporter()));


    if (!processorPath.isEmpty()) {
        if (Boolean.TRUE.equals(annotationProcessorOptions.getIncludeCompileClasspath())) {
            processorPath.addAll(javacTask.getClasspath().getFiles());
        }
        javacTask.getOptions().getCompilerArgs().add("-processorpath");
        javacTask.getOptions().getCompilerArgs().add(FileUtils.joinFilePaths(processorPath));
    }
    if (!annotationProcessorOptions.getClassNames().isEmpty()) {
        javacTask.getOptions().getCompilerArgs().add("-processor");
        javacTask.getOptions().getCompilerArgs().add(
                Joiner.on(',').join(annotationProcessorOptions.getClassNames()));
    }
    if (!annotationProcessorOptions.getArguments().isEmpty()) {
        for (Map.Entry<String, String> arg :
                annotationProcessorOptions.getArguments().entrySet()) {
            javacTask.getOptions().getCompilerArgs().add(
                    "-A" + arg.getKey() + "=" + arg.getValue());
        }
    }

    // Create directory for output of annotation processor.
    javacTask.doFirst(task -> {
        FileUtils.mkdirs(scope.getAnnotationProcessorOutputDir()); // build/generated/source/apt
    });
    javacTask.getOptions().getCompilerArgs().add("-s");
    javacTask.getOptions().getCompilerArgs().add(
            scope.getAnnotationProcessorOutputDir().getAbsolutePath());
}

看下来这个代码,这里就是在给 javac 命令添加参数,看到setDestinationDir()方法,说明 javac 之后生成的代码路径为build/intermediates/classes/。 先往回看代码:

javacTask.getOptions().setBootClasspath(
        Joiner.on(File.pathSeparator).join(
            scope.getGlobalScope().getAndroidBuilder()
                .getBootClasspathAsStrings(false)));

这个方法就是设置一些很基础的 jar 包的 classpath,比如android.jar

这个代码通过查找 Sdk location 来获取这些 jar 包的位置,至于使用的是哪个版本,则由compileSdkVersion决定,

所以,升级compileSdkVersion,就能使用最新的android.jar的 sdk,

还有,如果你想使用暴露隐藏方法的android.jar,替换对应compileSdkVersionandroid.jar即可。具体路径在${sdkVersion}/platforms/android-${compileSdkVersion}

往下继续看代码,可以看到一个叫做processorPath的列表,processorPath这个列表是怎么来的呢?就是我们在build.gradle里面dependencies里面声明的annotationProcessor

假设你的项目里有butterknife之类的东东,所以这个列表不会为空,所以会为 javac 添加如下参数-processorpath ${processorPath},

继续看,发现又添加了具体的 apt 处理类的参数: -processor ${processorClassName1,processorClassName2...}

最后,如果你为某个annotationProcessor设置了参数,就会为 javac 再加入新的参数-A ${key}=${value},

为某个annotationProcessor设置了参数的方式为(以butterknife为例)

javaCompileOptions.annotationProcessorOptions.arguments['butterknife.debuggable'] = 'false'

所以,这个地方给 javac 添加的参数类似-A butterknife.debuggable=false

最后,要在执行 javac 之前做一些处理,我们可以看到,就是创建build/generated/source/apt这个文件夹,如果存在,删除之后再重新创建。创建好此文件夹后,就可以加入-s ${annotationProcessorOutputDir},

这样,无论是需要 apt 生成的代码,还是最终需要 javac 生成的代码都配置好了,如果想知道每个参数的具体含义,请参考javac -help

剩下的就是调用 javac 命令来执行了,因为这个属于JavaCompile task,这个 Task 是 gradle 自带的,所以到这里createJavacTask方法就结束了。

接下来这个 Task 并不是打包过程的一步,放在这里是因为如果你需要打包一个 Jar 包给别人,那么这里是最合适执行此 Task 的地方,因为这个 Task 要依赖javacTask

这个 Task 的任务就是把 Javac 生成的那些文件,R 文件,BuildConfig 文件打包,所以这里也没有写出这个 Task 的名字,就直接创建了,

创建代码为: getAndroidTasks().create(tasks, new AndroidJarTask.JarClassesConfigAction(variantScope));

这个 Task 执行结果放在build/intermediates/packaged/${buildType}/classes.jar

不过,这里只是包含当前 lib 生成的 class 文件,不包含依赖的 class 文件。

了解到这里,我们就可以继续往下看了。

createPostCompilationTasks

/**
 * Creates the post-compilation tasks for the given Variant.
 *
 * These tasks create the dex file from the .class files, plus optional intermediary steps like
 * proguard and jacoco
 *
 */
public void createPostCompilationTasks(
        @NonNull TaskFactory tasks,
        @NonNull final VariantScope variantScope) {

    checkNotNull(variantScope.getJavacTask());

    final BaseVariantData<? extends BaseVariantOutputData> variantData = variantScope.getVariantData();
    final GradleVariantConfiguration config = variantData.getVariantConfiguration();

    TransformManager transformManager = variantScope.getTransformManager();

    // ---- Code Coverage first -----
    // 如果有单元测试代码,且testCoverageEnabled,则执行JacocoTansform
    boolean isTestCoverageEnabled = config.getBuildType().isTestCoverageEnabled() &&
            !config.getType().isForTesting() &&
            getIncrementalMode(variantScope.getVariantConfiguration()) == IncrementalMode.NONE;
    if (isTestCoverageEnabled) {
        createJacocoTransform(tasks, variantScope);
    }

    boolean isMinifyEnabled = isMinifyEnabled(variantScope);
    boolean isMultiDexEnabled = config.isMultiDexEnabled();
    // Switch to native multidex if possible when using instant run.
    boolean isLegacyMultiDexMode = isLegacyMultidexMode(variantScope);

    AndroidConfig extension = variantScope.getGlobalScope().getExtension();

    // ----- External Transforms -----
    // apply all the external transforms.
    List<Transform> customTransforms = extension.getTransforms();
    List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();

    for (int i = 0, count = customTransforms.size() ; i < count ; i++) {
        Transform transform = customTransforms.get(i);
        AndroidTask<TransformTask> task = transformManager
                .addTransform(tasks, variantScope, transform);
        // task could be null if the transform is invalid.
        if (task != null) {
            List<Object> deps = customTransformsDependencies.get(i);
            if (!deps.isEmpty()) {
                task.dependsOn(tasks, deps);
            }

            // if the task is a no-op then we make assemble task depend on it.
            if (transform.getScopes().isEmpty()) {
                variantScope.getAssembleTask().dependsOn(tasks, task);
            }

        }
    }

    // ----- Minify next -----

    if (isMinifyEnabled) {
        boolean outputToJarFile = isMultiDexEnabled && isLegacyMultiDexMode;
        createMinifyTransform(tasks, variantScope, outputToJarFile);
    }


    // ----- Multi-Dex support

    AndroidTask<TransformTask> multiDexClassListTask = null;
    // non Library test are running as native multi-dex
    if (isMultiDexEnabled && isLegacyMultiDexMode) {
        if (!variantData.getVariantConfiguration().getBuildType().isUseProguard()) {
            throw new IllegalStateException(
                    "Build-in class shrinker and multidex are not supported yet.");
        }

        // ----------
        // create a transform to jar the inputs into a single jar.
        if (!isMinifyEnabled) {
            // merge the classes only, no need to package the resources since they are
            // not used during the computation.
            JarMergingTransform jarMergingTransform = new JarMergingTransform(
                    TransformManager.SCOPE_FULL_PROJECT);
            variantScope.addColdSwapBuildTask(
                    transformManager.addTransform(tasks, variantScope, jarMergingTransform));
        }

        // ---------
        // create the transform that's going to take the code and the proguard keep list
        // from above and compute the main class list.
        MultiDexTransform multiDexTransform = new MultiDexTransform(
                variantScope,
                extension.getDexOptions(),
                null);
        multiDexClassListTask = transformManager.addTransform(
                tasks, variantScope, multiDexTransform);
        multiDexClassListTask.optionalDependsOn(tasks, manifestKeepListTask);
        variantScope.addColdSwapBuildTask(multiDexClassListTask);
    }
    // create dex transform
    DefaultDexOptions dexOptions = DefaultDexOptions.copyOf(extension.getDexOptions());

    DexTransform dexTransform = new DexTransform(
            dexOptions,
            config.getBuildType().isDebuggable(),
            isMultiDexEnabled,
            isMultiDexEnabled && isLegacyMultiDexMode ? variantScope.getMainDexListFile() : null,
            variantScope.getPreDexOutputDir(),
            variantScope.getGlobalScope().getAndroidBuilder(),
            getLogger(),
            variantScope.getInstantRunBuildContext(),
            AndroidGradleOptions.getBuildCache(variantScope.getGlobalScope().getProject()));
    AndroidTask<TransformTask> dexTask = transformManager.addTransform(
            tasks, variantScope, dexTransform);
    // need to manually make dex task depend on MultiDexTransform since there's no stream
    // consumption making this automatic
    dexTask.optionalDependsOn(tasks, multiDexClassListTask);
}

看到最外层的注释,这里就是把 class 文件转化为 dex 文件的地方。那我们来详细跟踪一下代码。

这里应该有很多熟悉的参数名称,比如minifyEnabled,multiDexEnabled等,这些参数之后接下来就获取customTransforms这样一个Transform对象列表,这样一个列表是怎么来的呢?

就是通过Android PluginextensionregisterTransform注册的 transfrom。

这个 Transform 是个什么东东呢?如果你写过 Gradle 插件,对这个应该不陌生,

这个就是Android Plugin暴露的 Hook Api,让你可以自定义操作参与打包过程之中。

从这里也可以看出,gradle plugin 引入的顺序,影响 plugin 的Transform被调用的顺序。

这些Transform就会被加入到TransformManager中,按顺序执行。

跳过这个将Transfrom加入TransformManager的过程,下面就轮到了// ----- Minify next -----

看这个方法名createMinifyTransform,应该是创建Minify这个Transform,并加入TransformManager中。接下来我们跟进去看看细节。

简单几层跟进之后,如果useProguard属性设置为 true,则执行如下两个方法:

// createJarFile是bool值,Application中是true,Library中是false
createProguardTransform(taskFactory, variantScope, mappingConfiguration, createJarFile);
createShrinkResourcesTransform(taskFactory, variantScope);

我们来一一查看,先看

createProguardTransform

private void createProguardTransform(
        @NonNull TaskFactory taskFactory,
        @NonNull VariantScope variantScope,
        @Nullable Configuration mappingConfiguration,
        boolean createJarFile) {

    final BaseVariantData<? extends BaseVariantOutputData> variantData = variantScope
            .getVariantData();
    final GradleVariantConfiguration variantConfig = variantData.getVariantConfiguration();
    final BaseVariantData testedVariantData = variantScope.getTestedVariantData();

    ProGuardTransform transform = new ProGuardTransform(variantScope, createJarFile);

    applyProguardConfig(transform, variantData);

    AndroidTask<?> task = variantScope.getTransformManager().addTransform(taskFactory,
            variantScope, transform,
            (proGuardTransform, proGuardTask) ->
                    variantData.mappingFileProviderTask = new FileSupplier() {
                        @NonNull
                        @Override
                        public Task getTask() {
                    return proGuardTask;
                }

                        @Override
                        public File get() {
                    return proGuardTransform.getMappingFile();
                }
                    });

}

这里最重要的就是两个语句,

一个是初始化ProGuardTransform并将其加入TransformManager中,

第二个是给ProGuardTransform设置配置,

在这里我们关注的是流程,所以ProGuardTransform是如何执行的就不在这里细讲了,

如果你熟悉Transform Api,那么就会知道这里执行的是ProGuardTransform里的transform方法,如果你有兴趣,看这个方法了解 ProGuard 执行的细节。

好了,那我们来看一看applyProguardConfig方法

applyProguardConfig
final GradleVariantConfiguration variantConfig = variantData.getVariantConfiguration();
transform.setConfigurationFiles(() -> {
    Set<File> proguardFiles = variantConfig.getProguardFiles(
            true,
            Collections.singletonList(ProguardFiles.getDefaultProguardFile(
                    TaskManager.DEFAULT_PROGUARD_CONFIG_FILE, project)));

    // use the first output when looking for the proguard rule output of
    // the aapt task. The different outputs are not different in a way that
    // makes this rule file different per output.
    BaseVariantOutputData outputData = variantData.getOutputs().get(0);
    proguardFiles.add(outputData.processResourcesTask.getProguardOutputFile());
    return proguardFiles;
});

if (variantData.getType() == LIBRARY) {
    transform.keep("class **.R");
    transform.keep("class **.R$*");
}

不知道你们有没有过好奇,我们在新建项目的时候,build.gradle 文件中会有这么一行配置proguardFiles getDefaultProguardFile('proguard-android.txt'),这个文件在哪里呢,会不会可以改名字呢?这个的答案就在这里就可以找到。

就是下面这行代码

ProguardFiles.getDefaultProguardFile(TaskManager.DEFAULT_PROGUARD_CONFIG_FILE, project))

首先,它会检测参数值是否为”proguard-android.txt”或”proguard-android-optimize.txt”,也就说明了,getDefaultProguardFile只能填这两个参数。

接下来就是返回这个文件的具体路径在根目录的 build 文件夹下,在build/intermediates/proguard-files/这个目录下,文件名最后为当前 gradle plugin 的版本,这两个文件最初的位置在哪里呢?原来是${sdkPath}/tools/proguard/这个目录下的那两个文件.

还有,是不是好奇为什么出现在 layout 和 Manifest 里的东东都不会混淆,就是因为这行代码

proguardFiles.add(outputData.processResourcesTask.getProguardOutputFile());

这个就是给 proguardFiles 加入build.intermediates/proguard-rules/${buildType}/aapt_rules.txt,这个文件就是在前面的过程中生成的 proguard 的规则。

最后就是为 Library 单独配置的规则了,如果是 Library,不混淆它的 R 文件。

到这里createProguardTransform方法就结束了。继续看下一个方法createShrinkResourcesTransform

createShrinkResourcesTransform

private void createShrinkResourcesTransform(
        @NonNull TaskFactory taskFactory,
        @NonNull VariantScope scope) {
    CoreBuildType buildType = scope.getVariantConfiguration().getBuildType();

    if (!buildType.isShrinkResources()) {
        // The user didn't enable resource shrinking, silently move on.
        return;
    }

    // if resources are shrink, insert a no-op transform per variant output
    // to transform the res package into a stripped res package
    for (final BaseVariantOutputData variantOutputData : scope.getVariantData().getOutputs()) {
        VariantOutputScope variantOutputScope = variantOutputData.getScope();

        ShrinkResourcesTransform shrinkResTransform = new ShrinkResourcesTransform(
                variantOutputData,
                variantOutputScope.getProcessResourcePackageOutputFile(),
                variantOutputScope.getShrinkedResourcesFile(),
                androidBuilder,
                logger);
        AndroidTask<TransformTask> shrinkTask = scope.getTransformManager()
                .addTransform(taskFactory, variantOutputScope, shrinkResTransform);
        // need to record this task since the package task will not depend
        // on it through the transform manager.
        variantOutputScope.setShrinkResourcesTask(shrinkTask);
    }
}

这个方法很简短,老规矩,不分析这个Transform到底是怎么做的,这里将两点:

  1. shrinkResources 默认是 false,如果没有在 buildType 中主动声明为 true,则此方法会直接返回。
  2. 这个shrinkResTransform是在ProGuardTransform之后加入的,也就意味着在最后处理代码的时候,先执行 ProGuard,后执行 ShrinkResources。

这个方法要讲的就这么多,我们再返回createPostCompilationTasks方法继续看下去。

接下里分析// ----- Multi-Dex support这一段代码

AndroidTask<TransformTask> multiDexClassListTask = null;
// non Library test are running as native multi-dex
if (isMultiDexEnabled && isLegacyMultiDexMode) {

    // ----------
    // create a transform to jar the inputs into a single jar.
    if (!isMinifyEnabled) {
        // merge the classes only, no need to package the resources since they are
        // not used during the computation.
        // 把所有的.class打包成一个combined.jar
        JarMergingTransform jarMergingTransform = new JarMergingTransform(
                TransformManager.SCOPE_FULL_PROJECT);
        variantScope.addColdSwapBuildTask(
                transformManager.addTransform(tasks, variantScope, jarMergingTransform));
    }

    // ---------
    // create the transform that's going to take the code and the proguard keep list
    // from above and compute the main class list.
    MultiDexTransform multiDexTransform = new MultiDexTransform(
            variantScope,
            extension.getDexOptions(),
            null);
    multiDexClassListTask = transformManager.addTransform(
            tasks, variantScope, multiDexTransform);
    multiDexClassListTask.optionalDependsOn(tasks, manifestKeepListTask);
    variantScope.addColdSwapBuildTask(multiDexClassListTask);
}

可以看到,这里的重点就是两个Transform:

  • JarMergingTransform
  • MultiDexTransform

虽然如此,但是JarMergingTransform本身的任务很简单,就是将 class 文件打成 jar 包,这里就不仔细分析了,

熟悉Transform Api的话很容易就能看懂, 这个的最终结果是: build/intermediates/transform/${outputTypes}/${scope}/combined.jar这样一个 jar 包, 对着结果来理解应该会更容易了。

好了,这样我们来仔细关注MultiDexTransform,我们来看看它的transform方法

MultiDexTransform transform

//校验referencedInputs只能有一个,要么是jar,要么是directory
File input = verifyInputs(invocation.getReferencedInputs());
shrinkWithProguard(input);
computeList(input);

看起来就三个方法,第一个方法就是做一个校验;但是后面两个比较复杂,我们仔细研究一下。

MultiDexTransform shrinkWithProguard
private void shrinkWithProguard(@NonNull File input) throws IOException, ParseException {
    dontobfuscate();  // 设置obfuscate = false
    dontoptimize(); // 设置 optimize = false
    dontpreverify(); // 设置 preverify = false
    dontwarn(); // 设置 warn = Lists.newArrayList("**")
    dontnote(); // 设置 note = Lists.newArrayList("**")
    forceprocessing(); // 设置 lastModified = Long.MAX_VALUE

    applyConfigurationFile(manifestKeepListProguardFile);

    if (userMainDexKeepProguard != null) {
        applyConfigurationFile(userMainDexKeepProguard);
    }

    // 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.fd.** {*;}"); // Instant run.

    // handle inputs

    libraryJar(findShrinkedAndroidJar());
    inJar(input);

    // outputs.
    outJar(variantScope.getProguardComponentsJarFile());
    printconfiguration(configFileOut);

    // run proguard
    runProguard();
}

看到最后一个方法runProguard(),先猜测一下这个方法前面的过程是给Proguard设置参数的过程。经过分析,确实如此。

这里就点几点,userMainDexKeepProguard != null

这个在什么情况下不为 null 呢?在bulidType中设置过这个参数multiDexKeepProguard的情况下不为null

作为librayJar的参数到底是哪个 Jar 呢? 是 build-tools/${buildToolsVersion}/lib/shrinkedAndroid.jar这个 Jar。

outJar的最终位置在哪里呢? 在build/intermediates/multi-dex/${buildType}/componentClasses.jar

好了,最后一个这个配置的输出是在 build/intermediates/multi-dex/${buildType}/components.flags, 看这个文件的内容,就是这里的配置。

配置完成了,接下来就是跑Proguard输出componentClasses.jar的过程,这个过程是Proguard的任务,就不在我们流程之中了。详细了解这个过程,请参考Proguard的源码。

然后我们就可以看transform方法内的第三个方法了。

MultiDexTransform computeList
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);

}

这个最难的方法是callDx方法,大概讲一下这个方法的流程:

这个方法最终会调用AndroidBuildercreateMainDexList方法,此方法是调用build-tools/${buildToolsVersion}/lib/dx.jarcom.android.multidex.ClassReferenceListBuilder这个类的main方法,生成需要放入主 dex 的 class 集合;

接下来判断userMainDexKeepFile这个值,这个值的设置在哪里呢?是在bulidType中的参数multiDexKeepFile决定的。如果存在这样一个文件,就把这个文件里的内容也作为一个集合加入其中。

最后,将前面那个集合的内容写入maindexlist.txt这个文件中,这个文件位于build/intermediates/multi-dex/${buildType}/目录下。

到这里,MultiDexTransform的处理逻辑就分析完了,也就意味着,我们可以再返回createPostCompilationTasks方法继续看下去了。

接下来就是这个方法的最后一部分,dex处理部分。

DefaultDexOptions dexOptions = DefaultDexOptions.copyOf(extension.getDexOptions());

DexTransform dexTransform = new DexTransform(
        dexOptions,
        config.getBuildType().isDebuggable(),
        isMultiDexEnabled,
        isMultiDexEnabled && isLegacyMultiDexMode ? variantScope.getMainDexListFile() : null,
        variantScope.getPreDexOutputDir(),
        variantScope.getGlobalScope().getAndroidBuilder(),
        getLogger(),
        variantScope.getInstantRunBuildContext(),
        AndroidGradleOptions.getBuildCache(variantScope.getGlobalScope().getProject()));
AndroidTask<TransformTask> dexTask = transformManager.addTransform(
        tasks, variantScope, dexTransform);
// need to manually make dex task depend on MultiDexTransform since there's no stream
// consumption making this automatic
dexTask.optionalDependsOn(tasks, multiDexClassListTask);
variantScope.addColdSwapBuildTask(dexTask);

浏览了这部分源码过后,重点也是一个Transform的执行过程, 那么我们就来看看这个DexTransform

DexTransform transform

public void transform(@NonNull TransformInvocation transformInvocation)
        throws TransformException, IOException, InterruptedException {
    TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

    // Gather a full list of all inputs.
    List<JarInput> jarInputs = Lists.newArrayList();
    List<DirectoryInput> directoryInputs = Lists.newArrayList();
    for (TransformInput input : transformInvocation.getInputs()) {
        jarInputs.addAll(input.getJarInputs());
        directoryInputs.addAll(input.getDirectoryInputs());
    }

    ProcessOutputHandler outputHandler = new ParsingProcessOutputHandler(
            new ToolOutputParser(new DexParser(), Message.Kind.ERROR, logger),
            new ToolOutputParser(new DexParser(), logger),
            androidBuilder.getErrorReporter());

    outputProvider.deleteAll();

    try {
        // if only one scope or no per-scope dexing, just do a single pass that
        // runs dx on everything.
        if ((jarInputs.size() + directoryInputs.size()) == 1
                || !dexOptions.getPreDexLibraries()) {
            File outputDir = outputProvider.getContentLocation("main",
                    getOutputTypes(), getScopes(),
                    Format.DIRECTORY);
            FileUtils.mkdirs(outputDir);

            // first delete the output folder where the final dex file(s) will be.
            FileUtils.cleanOutputDir(outputDir);

            // gather the inputs. This mode is always non incremental, so just
            // gather the top level folders/jars
            final List<File> inputFiles =
                    Stream.concat(
                            jarInputs.stream().map(JarInput::getFile),
                            directoryInputs.stream().map(DirectoryInput::getFile))
                    .collect(Collectors.toList());

            androidBuilder.convertByteCode(
                    inputFiles,
                    outputDir,
                    multiDex,
                    mainDexListFile,
                    dexOptions,
                    getOptimize(),
                    outputHandler);

        } else {
            // Figure out if we need to do a dx merge.
            // The ony case we don't need it is in native multi-dex mode when doing debug
            // builds. This saves build time at the expense of too many dex files which is fine.
            // FIXME dx cannot receive dex files to merge inside a folder. They have to be in a
            // jar. Need to fix in dx.
            boolean needMerge = !multiDex || mainDexListFile != null;// || !debugMode;

            // where we write the pre-dex depends on whether we do the merge after.
            // If needMerge changed from one build to another, we'll be in non incremental
            // mode, so we don't have to deal with changing folder in incremental mode.
            File perStreamDexFolder = null;
            if (needMerge) {
                perStreamDexFolder = intermediateFolder;
                FileUtils.deletePath(perStreamDexFolder);
            }

            // dex all the different streams separately, then merge later (maybe)
            // hash to detect duplicate jars (due to isse with library and tests)
            final Set<String> hashs = Sets.newHashSet();
            // input files to output file map
            final Map<File, File> inputFiles = Maps.newHashMap();
            // set of input files that are external libraries
            final Set<File> externalLibs = Sets.newHashSet();
            // stuff to delete. Might be folders.
            final List<File> deletedFiles = Lists.newArrayList();

            // first gather the different inputs to be dexed separately.
            for (DirectoryInput directoryInput : directoryInputs) {
                File rootFolder = directoryInput.getFile();
                // The incremental mode only detect file level changes.
                // It does not handle removed root folders. However the transform
                // task will add the TransformInput right after it's removed so that it
                // can be detected by the transform.
                if (!rootFolder.exists()) {
                    // if the root folder is gone we need to remove the previous
                    // output
                    File preDexedFile = getPreDexFile(
                            outputProvider, needMerge, perStreamDexFolder, directoryInput);
                    if (preDexedFile.exists()) {
                        deletedFiles.add(preDexedFile);
                    }
                } else if (!isIncremental || !directoryInput.getChangedFiles().isEmpty()) {
                    // add the folder for re-dexing only if we're not in incremental
                    // mode or if it contains changed files.
                    logger.info("Changed file for %s are %s",
                            directoryInput.getFile().getAbsolutePath(),
                            Joiner.on(",").join(directoryInput.getChangedFiles().entrySet()));
                    File preDexFile = getPreDexFile(
                            outputProvider, needMerge, perStreamDexFolder, directoryInput);
                    inputFiles.put(rootFolder, preDexFile);
                    if (isExternalLibrary(directoryInput)) {
                        externalLibs.add(rootFolder);
                    }
                }
            }

            for (JarInput jarInput : jarInputs) {
                switch (jarInput.getStatus()) {
                    case NOTCHANGED:
                        // intended fall-through
                    case CHANGED:
                    case ADDED: {
                        File preDexFile = getPreDexFile(
                                outputProvider, needMerge, perStreamDexFolder, jarInput);
                        inputFiles.put(jarInput.getFile(), preDexFile);
                        if (isExternalLibrary(jarInput)) {
                            externalLibs.add(jarInput.getFile());
                        }
                        break;
                    }
                    case REMOVED: {
                        File preDexedFile = getPreDexFile(
                                outputProvider, needMerge, perStreamDexFolder, jarInput);
                        if (preDexedFile.exists()) {
                            deletedFiles.add(preDexedFile);
                        }
                        break;
                    }
                }
            }

            WaitableExecutor<Void> executor = WaitableExecutor.useGlobalSharedThreadPool();

            for (Map.Entry<File, File> entry : inputFiles.entrySet()) {
                Callable<Void> action = new PreDexTask(
                        entry.getKey(),
                        entry.getValue(),
                        hashs,
                        outputHandler,
                        externalLibs.contains(entry.getKey()) ? buildCache : FileCache.NO_CACHE);
                logger.info("Adding PreDexTask for %s : %s", entry.getKey(), action);
                executor.execute(action);
            }

            for (final File file : deletedFiles) {
                executor.execute(() -> {
                    FileUtils.deletePath(file);
                    return null;
                });
            }

            executor.waitForTasksWithQuickFail(false);
            logger.info("Done with all dexing");

            if (needMerge) {
                File outputDir = outputProvider.getContentLocation("main",
                        TransformManager.CONTENT_DEX, getScopes(),
                        Format.DIRECTORY);
                FileUtils.mkdirs(outputDir);

                // first delete the output folder where the final dex file(s) will be.
                FileUtils.cleanOutputDir(outputDir);
                FileUtils.mkdirs(outputDir);

                // find the inputs of the dex merge.
                // they are the content of the intermediate folder.
                List<File> outputs = null;
                if (!multiDex) {
                    // content of the folder is jar files.
                    File[] files = intermediateFolder.listFiles((file, name) -> {
                        return name.endsWith(SdkConstants.DOT_JAR);
                    });
                    if (files != null) {
                        outputs = Arrays.asList(files);
                    }
                } else {
                    File[] directories = intermediateFolder.listFiles(File::isDirectory);
                    if (directories != null) {
                        outputs = Arrays.asList(directories);
                    }
                }

                if (outputs == null) {
                    throw new RuntimeException("No dex files to merge!");
                }

                androidBuilder.convertByteCode(
                        outputs,
                        outputDir,
                        multiDex,
                        mainDexListFile,
                        dexOptions,
                        getOptimize(),
                        outputHandler);
            }
        }
    } catch (Exception e) {
        throw new TransformException(e);
    }
}

这个方法看起来很长, 因为要处理 multidex 和非 multidex 的情况。我们慢慢分析。

首先看第一个 if 里的代码if ((jarInputs.size() + directoryInputs.size()) == 1|| !dexOptions.getPreDexLibraries()),

这个代码中的outputDir路径为: build/intermediates/transforms/dex/folders/${outputTypes}/${scope}/main

这也就是我们看到的最终的classes.dex这个文件的地方,这里面调用了一个很重要的方法,但是在这篇文章我们不会去跟进里面的具体实现,可以等待后续文章分析它的具体实现,就是下面这个方法。

androidBuilder.convertByteCode(
    inputFiles,
    outputDir,
    multiDex,
    mainDexListFile,
    dexOptions,
    getOptimize(),
    outputHandler);

这个方法的作用说起来很简单,但是里面的过程很复杂,就是将字节码转化为 Dalvik 格式的字节码。

这样,这个 if 里的内容就分析完了,接下来我们看对应的 else 里面的内容。

else 里面的代码什么时候触发呢?最简单的方式就是符合开启multidex的条件,并开启multidex

这里的代码和 if 里的一样,收集最终需要转化的 inputs 文件,最终输出文件地址为build/intermediates/transforms/dex/folders/${outputTypes}/${scope}/main

通过androidBuilder.convertByteCode转化这些字节码。

在这里讲一段代码,也是 AS 为 gradle 打包提速的一个方法: 生成 cache

WaitableExecutor<Void> executor = WaitableExecutor.useGlobalSharedThreadPool();

for (Map.Entry<File, File> entry : inputFiles.entrySet()) {
    Callable<Void> action = new PreDexTask(
            entry.getKey(),
            entry.getValue(),
            hashs,
            outputHandler,
            externalLibs.contains(entry.getKey()) ? buildCache : FileCache.NO_CACHE);
    logger.info("Adding PreDexTask for %s : %s", entry.getKey(), action);
    executor.execute(action);
}

这个地方的代码为参与最终转化的代码生成了 cache,cache 的具体位置在$Users/.android/build-cache/这个目录下,用包的 sha1 值作为目录名称,如果对应的包没改过,则 sha1 值不会变,就使用 cache 中的包。

这样我们就把DexTransform讲完了。

回到createPostCompilationTasks这个方法,讲完了DexTransform也就意味着这个方法结束了,也就是说PostCompilationTasks到这里也结束了。

那么我们就需要去理解整个打包过程最后一个方法了。