【Android修炼手册】Gradle篇 -- Gradle构建生命周期

2020-06-19 Views gradle

前言

自定义gradle插件是Android开发工程师的必备技能之一。通过gradle打包的机制,可以在打包过程中插入自定义的task,实现我们想要的功能。例如对于class文件的hook或者资源文件的修改。

下面以开发的png转webp插件为例,介绍一下自定义gradle插件的开发过程,以及需要注意的点。本文主要从gradle构建的生命周期和Android打包时主要的task讲起。万丈高楼平地起,介绍了基础知识点有助于后续的开发设计。

基础概念

Gradle构建流程

gradle-flow.png

可以看到gradle构建时总共分为三步:初始化、配置、执行。

  1. 解析 settings.gradle 来获取模块信息,这是初始化阶段。

  2. 配置每个模块,配置的时候并不会执行 task。配置完了以后,有一个重要的回调 project.afterEvaluate,它表示所有的模块都已经配置完了,可以准备执行 task 了。

  3. 执行指定的 task 及其依赖的 task,完成打包

生命周期流程

gradle执行有相应的生命周期回调,在回调中可以获取到gradle的生命周期方法。代码如下所示。

mProject.gradle.addBuildListener(new BuildListener() {

            @Override
            void buildStarted(Gradle gradle) {
            //build开始
            }

            @Override
            void settingsEvaluated(Settings settings) {
            //执行setting文件配置,评估完成
            }

            @Override
            void projectsLoaded(Gradle gradle) {
            //项目结构加载完成(初始化阶段结束)
            }

            @Override
            void projectsEvaluated(Gradle gradle) {
            //所有项目评估完成(配置阶段结束)
            }

            @Override
            void buildFinished(BuildResult result) {
            //项目构建结束
            }
        })

后面会根据生命周期方法结合上文的初始化、配置和执行这三个步骤进行讲解。

初始化

gradle-init.png

在这个阶段中,会读取根工程中的 setting.gradle 中的 include 信息,确定有多少工程加入构建,然后,会为每一个项目(build.gradle 脚本文件)创建一个与之对应的 Project 实例,最终形成一个项目的层次结构。

与初始化阶段相关的脚本文件是 settings.gradle,而一个 settings.gradle 脚本对应一个 Settings 对象,我们最常用来声明项目的层次结构的 include 就是 Settings 对象下的一个方法,在 Gradle 初始化的时候会构造一个 Settings 实例对象,以执行各个 Project 的初始化配置。

配置

gradle-evalute.png

配置阶段的任务是 执行各项目下的 build.gradle 脚本,完成 Project 的配置,与此同时,会构造 Task 任务依赖关系图以便在执行阶段按照依赖关系执行 Task。而在配置阶段执行的代码通常来说都会包括以下三个部分的内容:

  1. build.gralde 中的各种语句。
  2. 闭包。
  3. Task 中的配置段语句。

需要注意的是,执行任何 Gradle 命令,在初始化阶段和配置阶段的代码都会被执行。

执行

gradle-handle.png

在配置阶段结束后,Gradle 会根据各个任务 Task 的依赖关系来创建一个有向无环图。Gradle 构建系统会通过调用 gradle <任务名> 来执行相应的各个任务,最终完成打包。

以上就是gradle生命周期的内容,可以看到gradle执行打包流程还是很清晰的。

Android打包task 简介

在介绍 Android Gradle Plugin Task 之前,我们先放一张官方流程图,看看一个 apk 的打包构建流程:

官方介绍的流程如下:

  1. 编译器将您的源代码转换成 DEX(Dalvik Executable) 文件(其中包括 Android 设备上运行的字节码),将所有其他内容转换成已编译资源。

  2. APK 打包器将 DEX 文件和已编译资源合并成单个 APK。

  3. APK 打包器使用调试或发布密钥库签署您的 APK:

  4. 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,减少其在设备上运行时占用的内存。

上面是一个比较粗略的框架图,我们更多看到的是比较详细的图,如下

build_process.png

其中主要流程有:

  1. aapt处理资源文件,生成R文件
  2. 处理aidl文件,生成对应的java文件
  3. 编译项目代码,生成class文件
  4. 转化所有的class文件生成dex文件
  5. 打包生成APK文件
  6. 对APK文件进行签名
  7. 对签名后的APK文件进行对齐处理

上面说的打包流程都可以在Android gradle plugin 的找到对应的task。具体见下文的分析。

全部task

我们可以使用./gradlew -m assembDebug命令来获取当前项目的所有task(但不会真正执行这些task)。创建一个空的Android工程执行该命令后的task截图。

gradle-all-task.png
将上述task进行逐一分析,

Task 对应实现类 作用
preBuild 空 task,只做锚点使用
preDebugBuild 空 task,只做锚点使用,与 preBuild 区别是这个 task 是 variant 的锚点
compileDebugAidl AidlCompile 处理 aidl
compileDebugRenderscript RenderscriptCompile 处理 renderscript
checkDebugManifest CheckManifest 检测 manifest 是否存在
generateDebugBuildConfig GenerateBuildConfig 生成 BuildConfig.java
prepareLintJar PrepareLintJar 拷贝 lint jar 包到指定位置
generateDebugResValues GenerateResValues 生成 resvalues,generated.xml
generateDebugResources 空 task,锚点
mergeDebugResources MergeResources 合并资源文件
createDebugCompatibleScreenManifests CompatibleScreensManifest manifest 文件中生成 compatible-screens,指定屏幕适配
processDebugManifest MergeManifests 合并 manifest 文件
splitsDiscoveryTaskDebug SplitsDiscovery 生成 split-list.json,用于 apk 分包
processDebugResources ProcessAndroidResources aapt 打包资源
generateDebugSources 空 task,锚点
javaPreCompileDebug JavaPreCompileTask 生成 annotationProcessors.json 文件
compileDebugJavaWithJavac AndroidJavaCompile 编译 java 文件
compileDebugNdk NdkCompile 编译 ndk
compileDebugSources 空 task,锚点使用
mergeDebugShaders MergeSourceSetFolders 合并 shader 文件
compileDebugShaders ShaderCompile 编译 shaders
generateDebugAssets 空 task,锚点
mergeDebugAssets MergeSourceSetFolders 合并 assets 文件
transformClassesWithDexBuilderForDebug DexArchiveBuilderTransform class 打包 dex
transformDexArchiveWithExternalLibsDexMergerForDebug ExternalLibsMergerTransform 打包三方库的 dex,在 dex 增量的时候就不需要再 merge 了,节省时间
transformDexArchiveWithDexMergerForDebug DexMergerTransform 打包最终的 dex
mergeDebugJniLibFolders MergeSouceSetFolders 合并 jni lib 文件
transformNativeLibsWithMergeJniLibsForDebug MergeJavaResourcesTransform 合并 jnilibs
transformNativeLibsWithStripDebugSymbolForDebug StripDebugSymbolTransform 去掉 native lib 里的 debug 符号
processDebugJavaRes ProcessJavaResConfigAction 处理 java res
transformResourcesWithMergeJavaResForDebug MergeJavaResourcesTransform 合并 java res
validateSigningDebug ValidateSigningTask 验证签名
packageDebug PackageApplication 打包 apk
assembleDebug 空 task,锚点

可以看到gradle打包涉及到很多task,但我们主要关注的还是资源,代码文件和dex文件处理相关的task。

mergeDebugResources

mergeResources是合并资源文件的Task,它的实现类是MergeResources,继承自IncrementalTask支持增量。主要任务流程如下:

  1. 通过 getConfiguredResourceSets() 获取 resourceSets,包括了自己的 res/ 和 依赖库的 res/ 以及 build/generated/res/rs
// MergeResources.doFullTaskAction()
List<ResourceSet> resourceSets = getConfiguredResourceSets(preprocessor);
  1. 创建 ResourceMerger
// MergeResources.doFullTaskAction()
ResourceMerger merger = new ResourceMerger(minSdk);
  1. 创建 QueueableResourceCompiler,因为 gradle3.x 以后支持了 aapt2,所以这里有两种选择 aapt 和 aapt2。这里默认创建了 QueueableAapt2
// MergeResources.doFullTaskAction()
// makeAapt 中会判断使用 aapt 还是 aapt2,这里以 aapt2 为例,返回的是 QueueableAapt2 对象
QueueableResourceCompiler resourceCompiler =
    makeAapt(
        aaptGeneration,
        getBuilder(),
        fileCache,
        crunchPng,
        variantScope,
        getAaptTempDir(),
        mergingLog)
  1. 将第一步获取的 resourceSet 加入 ResourceMerger 中
for (ResourceSet resourceSet : resourceSets) {
    resourceSet.loadFromFiles(getILogger());
    merger.addDataSet(resourceSet);
}
  1. 创建 MergedResourceWriter
  2. 调用 ResourceMerger.mergeData 合并资源
// MergeResources.doFullTaskAction()
merger.mergeData(writer, false /*doCleanUp*/);
  1. 调用 MergedResourceWriter 的 start(),addItem(),end() 方法,伪代码如下:
// DataMerger.mergeData
consumer.start()
for item in sourceSets:
  // item 包括了需要处理的资源,包括 xml 和 图片资源,每一个 item 对应的文件,会创建一个 CompileResourceRequest 对象,加入到 mCompileResourceRequests 里
  consumer.addItem(item)
consumer.end()
  1. 调用 QueueableAapt2 -> Aapt2QueuedResourceProcessor -> AaptProcess 处理资源
// MergedResourceWriter.end()
Future<File> result = this.mResourceCompiler.compile(new CompileResourceRequest(fileToCompile, request.getOutput(), request.getFolderName(), this.pseudoLocalesEnabled, this.crunchPng));
// AaptProcess.compile
public void compile(
        @NonNull CompileResourceRequest request,
        @NonNull Job<AaptProcess> job,
        @Nullable ProcessOutputHandler processOutputHandler)
        throws IOException {
    // ... 
    // 使用 AaptV2CommandBuilder 生成 aapt2 命令
    mWriter.write(joiner.join(AaptV2CommandBuilder.makeCompile(request)));
    mWriter.flush(); // 输出命令
}

这一步调用 aapt2 命令去处理资源,处理完以后 xxx.xml.flat 格式

前面提到mergeResources支持增量,实际上增量任务过程和全量其实差异不大,只不过是在获取 resourceSets 的时候,使用的是修改后的文件

transformClassesWithDexBuilderForDebug

transformClassesWithDexBuilderForDebug的实现类是DexArchiveBuilderTransform,这个task的主要作用是class转化为dex。在这个Task中对 class 的处理分为两种方式,一种是对 目录下的 class 进行处理,一种是对 .jar 里的 class 进行处理。
为什么要分为这两种方式呢?.jar 中的 class 一般来说都是依赖库,基本上不会改变,gradle 在这里做了一个缓存,但是两种方式最终都会调用到 convertToDexArchive统一处理。

  • convertJarToDexArchive 处理 jar
    处理 .jar 时,会对 jar 包中的每一个 class 都单独打成一个 .dex 文件,之后还是放在 .jar 包中
    private List<File> convertJarToDexArchive(
            @NonNull Context context,
            @NonNull JarInput toConvert,
            @NonNull TransformOutputProvider transformOutputProvider)
            throws Exception {

        File cachedVersion = cacheHandler.getCachedVersionIfPresent(toConvert);
        if (cachedVersion == null) {
                // 如果没有缓存,调用 convertToDexArchive 去生成 dex
            return convertToDexArchive(context, toConvert, transformOutputProvider, false);
        } else {
                // 如果有缓存,直接使用缓存的 jar
            File outputFile = getPreDexJar(transformOutputProvider, toConvert, null);
            Files.copy(
                    cachedVersion.toPath(),
                    outputFile.toPath(),
                    StandardCopyOption.REPLACE_EXISTING);
            // no need to try to cache an already cached version.
            return ImmutableList.of();
        }
    }
  • convertToDexArchive 处理 dir 以及 jar 的后续处理
    对 dir 处理使用 convertToDexArchive
    其中会调用 launchProcessing
    private static void launchProcessing(
            @NonNull DexConversionParameters dexConversionParameters,
            @NonNull OutputStream outStream,
            @NonNull OutputStream errStream)
            throws IOException, URISyntaxException {
        // ...
        boolean hasIncrementalInfo =
                dexConversionParameters.isDirectoryBased() && dexConversionParameters.isIncremental;
        // 判断 class 是否新增或者修改过,如果新增或者修改过,就需要处理
        Predicate<String> toProcess =
                hasIncrementalInfo
                        ? path -> {
                            Map<File, Status> changedFiles =
                                    ((DirectoryInput) dexConversionParameters.input)
                                            .getChangedFiles();

                            File resolved = inputPath.resolve(path).toFile();
                            Status status = changedFiles.get(resolved);
                            return status == Status.ADDED || status == Status.CHANGED;
                        }
                        : path -> true;

        bucketFilter = bucketFilter.and(toProcess);

        try (ClassFileInput input = ClassFileInputs.fromPath(inputPath)) {
            // 内部调用 dx 或者 d8 去打 dex
            dexArchiveBuilder.convert(
                    input.entries(bucketFilter),
                    Paths.get(new URI(dexConversionParameters.output)),
                    dexConversionParameters.isDirectoryBased());
        } catch (DexArchiveBuilderException ex) {
            throw new DexArchiveBuilderException("Failed to process " + inputPath.toString(), ex);
        }
    }

在 launchProcessing 中,有下面几个步骤:

  1. 判断目录下的 class 是否新增或者修改过
  2. 调用 DexArchiveBuilder.build 去处理修改过的 class
  3. DexArchiveBuilder 有两个子类,D8DexArchiveBuilder 和 DxDexArchiveBuilder,分别是调用 d8 和 dx 去打 dex

这里只对这两个Android Gradle Task进行分析,在上述表格中都可以找到对应的Task实现类。

总结

本文主要简述了gradle执行打包时初始化、配置、执行这三个步骤,结合BuildListener进行生命周期的分析。

  • 初始化阶段 执行setting.gradle文件
  • 配置阶段 执行build.gradle文件
  • 执行阶段 运行相关task,完成打包

最后对APK的打包构建流程和Android Gradle Task有个大致的介绍,帮助我们了解Gradle打包APK流程。
对于其中比较重要的任务如mergeResources和transformClassesWithDexBuilderForDebug进行分析。

参考

https://developer.android.google.cn/studio/build?hl=zh_cn

https://mp.weixin.qq.com/s/2Un0ZyNgcfo8fwcNHhNgyg

https://juejin.im/post/5e9c46c8518825737f1a7b4c#heading-50

赏 支付宝扫码打赏 微信打赏

△