自定义gradle插件是Android开发工程师的必备技能之一。通过gradle打包的机制,可以在打包过程中插入自定义的task,实现我们想要的功能。例如对于class文件的hook或者资源文件的修改。 下面以开发的png转webp插件为例,介绍一下自定义gradle插件的开发过程,以及需要注意的点。本文主要从gradle构建的生命周期和Android打包时主要的task讲起。万丈高楼平地起,介绍了基础知识点有助于后续的开发设计。 可以看到gradle构建时总共分为三步:初始化、配置、执行。 解析 settings.gradle 来获取模块信息,这是初始化阶段。 配置每个模块,配置的时候并不会执行 task。配置完了以后,有一个重要的回调 project.afterEvaluate,它表示所有的模块都已经配置完了,可以准备执行 task 了。 执行指定的 task 及其依赖的 task,完成打包 gradle执行有相应的生命周期回调,在回调中可以获取到gradle的生命周期方法。代码如下所示。 后面会根据生命周期方法结合上文的初始化、配置和执行这三个步骤进行讲解。 在这个阶段中,会读取根工程中的 与初始化阶段相关的脚本文件是 配置阶段的任务是 执行各项目下的 需要注意的是,执行任何 Gradle 命令,在初始化阶段和配置阶段的代码都会被执行。 在配置阶段结束后,Gradle 会根据各个任务 Task 的依赖关系来创建一个有向无环图。Gradle 构建系统会通过调用 gradle <任务名> 来执行相应的各个任务,最终完成打包。 以上就是gradle生命周期的内容,可以看到gradle执行打包流程还是很清晰的。 在介绍 Android Gradle Plugin Task 之前,我们先放一张官方流程图,看看一个 apk 的打包构建流程: 官方介绍的流程如下: 编译器将您的源代码转换成 DEX(Dalvik Executable) 文件(其中包括 Android 设备上运行的字节码),将所有其他内容转换成已编译资源。 APK 打包器将 DEX 文件和已编译资源合并成单个 APK。 APK 打包器使用调试或发布密钥库签署您的 APK: 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,减少其在设备上运行时占用的内存。 上面是一个比较粗略的框架图,我们更多看到的是比较详细的图,如下 其中主要流程有: 上面说的打包流程都可以在Android gradle plugin 的找到对应的task。具体见下文的分析。 我们可以使用 可以看到gradle打包涉及到很多task,但我们主要关注的还是资源,代码文件和dex文件处理相关的task。 这一步调用 aapt2 命令去处理资源,处理完以后 xxx.xml.flat 格式 前面提到 在 launchProcessing 中,有下面几个步骤: 这里只对这两个Android Gradle Task进行分析,在上述表格中都可以找到对应的Task实现类。 本文主要简述了gradle执行打包时初始化、配置、执行这三个步骤,结合BuildListener进行生命周期的分析。 最后对APK的打包构建流程和Android Gradle Task有个大致的介绍,帮助我们了解Gradle打包APK流程。 参考 https://developer.android.google.cn/studio/build?hl=zh_cn https://mp.weixin.qq.com/s/2Un0ZyNgcfo8fwcNHhNgyg https://juejin.im/post/5e9c46c8518825737f1a7b4c#heading-50前言
基础概念
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) {
//项目构建结束
}
})
初始化

setting.gradle 中的 include 信息,确定有多少工程加入构建,然后,会为每一个项目(build.gradle 脚本文件)创建一个与之对应的 Project 实例,最终形成一个项目的层次结构。settings.gradle,而一个 settings.gradle 脚本对应一个 Settings 对象,我们最常用来声明项目的层次结构的 include 就是 Settings 对象下的一个方法,在 Gradle 初始化的时候会构造一个 Settings 实例对象,以执行各个 Project 的初始化配置。配置

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

Android打包task 简介


全部task
./gradlew -m assembDebug命令来获取当前项目的所有task(但不会真正执行这些task)。创建一个空的Android工程执行该命令后的task截图。
将上述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,锚点
mergeDebugResources
mergeResources是合并资源文件的Task,它的实现类是MergeResources,继承自IncrementalTask支持增量。主要任务流程如下:
// MergeResources.doFullTaskAction()
List<ResourceSet> resourceSets = getConfiguredResourceSets(preprocessor);
// MergeResources.doFullTaskAction()
ResourceMerger merger = new ResourceMerger(minSdk);
// MergeResources.doFullTaskAction()
// makeAapt 中会判断使用 aapt 还是 aapt2,这里以 aapt2 为例,返回的是 QueueableAapt2 对象
QueueableResourceCompiler resourceCompiler =
makeAapt(
aaptGeneration,
getBuilder(),
fileCache,
crunchPng,
variantScope,
getAaptTempDir(),
mergingLog)
for (ResourceSet resourceSet : resourceSets) {
resourceSet.loadFromFiles(getILogger());
merger.addDataSet(resourceSet);
}
// MergeResources.doFullTaskAction()
merger.mergeData(writer, false /*doCleanUp*/);
// DataMerger.mergeData
consumer.start()
for item in sourceSets:
// item 包括了需要处理的资源,包括 xml 和 图片资源,每一个 item 对应的文件,会创建一个 CompileResourceRequest 对象,加入到 mCompileResourceRequests 里
consumer.addItem(item)
consumer.end()
// 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(); // 输出命令
}
mergeResources支持增量,实际上增量任务过程和全量其实差异不大,只不过是在获取 resourceSets 的时候,使用的是修改后的文件transformClassesWithDexBuilderForDebug
transformClassesWithDexBuilderForDebug的实现类是DexArchiveBuilderTransform,这个task的主要作用是class转化为dex。在这个Task中对 class 的处理分为两种方式,一种是对 目录下的 class 进行处理,一种是对 .jar 里的 class 进行处理。
为什么要分为这两种方式呢?.jar 中的 class 一般来说都是依赖库,基本上不会改变,gradle 在这里做了一个缓存,但是两种方式最终都会调用到 convertToDexArchive统一处理。
处理 .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();
}
}
对 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);
}
}
总结
对于其中比较重要的任务如mergeResources和transformClassesWithDexBuilderForDebug进行分析。
支付宝扫码打赏
微信打赏