Android 构建打包流程

前言

公司做为游戏发行商,其中有一块业务为融合 SDK,游戏制作者接入我们的 SDK 提供母包给我们,由我们负责将各个渠道包打好给他,然后进行利益分摊。而我以前对打包细节了解的不多,于是就抽了点时间研究了下 Android 构建打包流程。

流程

首先翻看了 Android 官方文档 找到了以下一个流程图:

截屏2021-02-02 下午9.10.42

上图中展示了大致了打包流程,不过不够详细,比如编译流程是什么样的,最后打包流程又是怎么样的,而这些源码都会告诉我。

源码

众所周知,如果需要手动在命令行构建打包 Apk,可以使用以下几个命令:

# 构建全量包
./gradlew :app:assemble
# 构建测试包
./gradlew :app:assembleDebug
# 构建正式包
./gradlew :app:assembleRelease

这里以第二条命令为例,执行了上述命令后,Gradle 会在初始化、配置阶段完成后执行 assembleDebug 这个 Task,这时候第一个疑问就出现了,这个 Task 是哪里来的,难道是 Gradle 自带的?很明显并不是,它来自一个插件其 id 为 com.android.application 。

查看应用程序模块下的 build.gradle 可以发现其应用了该插件。

plugins {
id 'com.android.application'
}
...

那么该插件来自于哪呢?查看根项目 build.gradle 可以发现有如下配置。

buildscript {
repositories {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
}
}

显然 com.android.application 插件来自 com.android.tools.build:gradle:3.0.0 库,而该库又来自 Google 的 maven 仓库。根据自定义插件流程翻看该库源码发现该插件对应的实现类为 AppPlugin。跟进源码,当务之急是找寻到 assembleDebug 这个 Task 是在哪里创建的。

public class AppPlugin extends BasePlugin implements Plugin<Project> {
public void apply(@NonNull Project project) {
super.apply(project);
}
}
public abstract class BasePlugin implements ToolingRegistryProvider {
protected void apply(@NonNull Project project) {
threadRecorder = ThreadRecorder.get();
threadRecorder.record(..., this::configureProject);
threadRecorder.record(..., this::configureExtension);
threadRecorder.record(..., this::createTasks);
}
}

ThreadRecorder#record 方法其实就是起个计时作用,内部会执行传入的方法。而上述三个方法前两个明显是配置相关的先行跳过,直接看看 createTasks。

void createTasks() {
project.afterEvaluate(
project -> threadRecorder.record(..., () -> createAndroidTasks(false))
);
}
void createAndroidTasks(boolean force) {
threadRecorder.record(..., () -> { variantManager.createAndroidTasks(); });
}
void createAndroidTasks() {
for (final VariantScope variantScope : variantScopes) {
recorder.record(..., () -> createTasksForVariantData(tasks, variantScope));
}
}

可以看到其实最终会通过 VariantManager 去创建 Task,而对于每一个 Variant 都会创建自己的 Task。

注:Variant 由 BuildType 和 ProductFlavor 组成。

public void createTasksForVariantData(
final TaskFactory tasks, final VariantScope variantScope) {
BuildTypeData buildTypeData = buildTypes.get(variantConfig.getBuildType().getName());
if (buildTypeData.getAssembleTask() == null) {
buildTypeData.setAssembleTask(taskManager.createAssembleTask(tasks, buildTypeData));
}
}
public AndroidTask<DefaultTask> createAssembleTask(
@NonNull TaskFactory tasks,
@NonNull VariantDimensionData dimensionData) {
final String sourceSetName =
StringHelper.capitalize(dimensionData.getSourceSet().getName());
return androidTasks.create(
tasks,
"assemble" + sourceSetName,
assembleTask -> {
assembleTask.setDescription("Assembles all " + sourceSetName + " builds.");
assembleTask.setGroup(BasePlugin.BUILD_GROUP);
});
}

终于找到了,其最终是通过 TaskManager 创建生成的。在 AS 右端 Gradle 窗口中寻找到 assembleDebug 鼠标悬浮会出现该 Task 的描述信息也能证明这一点。

截屏2021-02-03 上午10.40.34

虽然找到了该 Task 的位置,不过它本身什么也不做,那么问题又来了,为啥执行它就能打包?其实根据 Gradle 规定 Task 可以添加依赖,使得在执行该 Task 前执行其它的 Task,但是由于依赖关系错综复杂,就不一处处的找了。直接执行一下,看看具体哪些 Task 被执行了。

:app:preBuild SKIPPED
:app:preDebugBuild SKIPPED
:app:compileDebugAidl SKIPPED
:app:compileDebugRenderscript SKIPPED
:app:checkDebugManifest SKIPPED
:app:generateDebugBuildConfig SKIPPED
:app:prepareLintJar SKIPPED
:app:generateDebugResValues SKIPPED
:app:generateDebugResources SKIPPED
:app:mergeDebugResources SKIPPED
:app:createDebugCompatibleScreenManifests SKIPPED
:app:processDebugManifest SKIPPED
:app:splitsDiscoveryTaskDebug SKIPPED
:app:processDebugResources SKIPPED
:app:generateDebugSources SKIPPED
:app:javaPreCompileDebug SKIPPED
:app:compileDebugJavaWithJavac SKIPPED
:app:compileDebugNdk SKIPPED
:app:compileDebugSources SKIPPED
:app:mergeDebugShaders SKIPPED
:app:compileDebugShaders SKIPPED
:app:generateDebugAssets SKIPPED
:app:mergeDebugAssets SKIPPED
:app:extractTryWithResourcesSupportJarDebug SKIPPED
:app:transformClassesWithStackFramesFixerForDebug SKIPPED
:app:transformClassesWithDesugarForDebug SKIPPED
:app:transformClassesWithDexBuilderForDebug SKIPPED
:app:transformDexArchiveWithExternalLibsDexMergerForDebug SKIPPED
:app:transformDexArchiveWithDexMergerForDebug SKIPPED
:app:mergeDebugJniLibFolders SKIPPED
:app:transformNativeLibsWithMergeJniLibsForDebug SKIPPED
:app:processDebugJavaRes SKIPPED
:app:transformResourcesWithMergeJavaResForDebug SKIPPED
:app:validateSigningDebug SKIPPED
:app:packageDebug SKIPPED
:app:assembleDebug SKIPPED

可以看到打一次包需要执行大量的 Task,下面一个个进行分析。

preBuild

BasePlugin#createTasks

TaskManager#createTasksBeforeEvaluate

public void createTasksBeforeEvaluate(@NonNull TaskFactory tasks) {
androidTasks.create(tasks, MAIN_PREBUILD, task -> {});
}

空实现,啥也没做,就是个锚点,任何变种构建都会执行。

preDebugBuild

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#createAnchorTasks

TaskManager#createPreBuildTasks

TaskManager#createVariantPreBuildTask

TaskManager#createDefaultPreBuildTask

protected AndroidTask<? extends DefaultTask> createDefaultPreBuildTask(
@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
return getAndroidTasks().create(tasks, scope.getTaskName("pre", "Build"), task -> {
scope.getVariantData().preBuildTask = task;
});
}

空实现,啥也没做,就是个锚点,标志着当前变种开始构建。

compileDebugAidl

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createAidlTask

public AndroidTask<AidlCompile> createAidlTask(TaskFactory tasks, VariantScope scope) {
AndroidTask<AidlCompile> aidlCompileTask = androidTasks
.create(tasks, new AidlCompile.ConfigAction(scope));
scope.setAidlCompileTask(aidlCompileTask);
scope.getSourceGenTask().dependsOn(tasks, aidlCompileTask);
aidlCompileTask.dependsOn(tasks, scope.getPreBuildTask());
return aidlCompileTask;
}

具体看下 AidlCompile.ConfigAction 。

public class AidlCompile extends IncrementalTask {
protected void doFullTaskAction() throws IOException {
compileAllFiles(processor);
}
private void compileAllFiles(DependencyFileProcessor dependencyFileProcessor)
throws InterruptedException, ProcessException, IOException {
getBuilder().compileAllAidlFiles(...);
}
public static class ConfigAction implements TaskConfigAction<AidlCompile> {
public Class<AidlCompile> getType() {
return AidlCompile.class;
}
}
}
public void compileAllAidlFiles(...) {
String aidl = buildToolInfo.getPath(BuildToolInfo.PathId.AIDL);
AidlProcessor processor = new AidlProcessor(aidl);
for (File dir : sourceFolders) {
DirectoryWalker.builder()
.root(dir.toPath())
.extensions("aidl")
.action(processor)
.build()
.walk();
}
}

Task 执行时会首先创建一个处理器,然后遍历 main/aidl 下所有的文件,遍历到后缀为 aidl 的文件后,进行处理。

public class AidlProcessor implements DirectoryWalker.FileAction {
public void call(@NonNull Path startDir, @NonNull Path path) throws IOException {
ProcessInfoBuilder builder = new ProcessInfoBuilder();
builder.setExecutable(mAidlExecutable);
builder.addArgs("-p" + mFrameworkLocation);
builder.addArgs("-o" + mSourceOutputDir.getAbsolutePath());
for (File f : mImportFolders) {
builder.addArgs("-I" + f.getAbsolutePath());
}
File depFile = File.createTempFile("aidl", ".d");
builder.addArgs("-d" + depFile.getAbsolutePath());
builder.addArgs(path.toAbsolutePath().toString());
ProcessResult result = mProcessExecutor.execute(
builder.createProcess(), mProcessOutputHandler);
}
}

处理过程也相当简单,也就是拼接参数然后执行 build-tools 下面的 aidl 工具。

compileDebugRenderscript

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createRenderscriptTask

public void createRenderscriptTask(
@NonNull TaskFactory tasks,
@NonNull VariantScope scope) {
scope.setRenderscriptCompileTask(
androidTasks.create(tasks, new RenderscriptCompile.ConfigAction(scope)));
GradleVariantConfiguration config = scope.getVariantConfiguration();
scope.getRenderscriptCompileTask().dependsOn(tasks, scope.getPreBuildTask());
scope.getResourceGenTask().dependsOn(tasks, scope.getRenderscriptCompileTask());
if (!config.getRenderscriptNdkModeEnabled()) {
scope.getSourceGenTask().dependsOn(tasks, scope.getRenderscriptCompileTask());
}
}

具体看下 RenderscriptCompile.ConfigAction 。

public class RenderscriptCompile extends NdkTask {
@TaskAction
void taskAction() throws IOException, InterruptedException, ProcessException {
getBuilder().compileAllRenderscriptFiles(...);
}
public static class ConfigAction implements TaskConfigAction<RenderscriptCompile> {
public Class<RenderscriptCompile> getType() {
return RenderscriptCompile.class;
}
}
}
public void compileAllRenderscriptFiles(...)
throws InterruptedException, ProcessException, IOException {
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
String renderscript = buildToolInfo.getPath(BuildToolInfo.PathId.LLVM_RS_CC);
RenderScriptProcessor processor = new RenderScriptProcessor(...);
processor.build(mProcessExecutor, processOutputHandler);
}
public void build(...) {
List<File> renderscriptFiles = Lists.newArrayList();
for (File dir : mSourceFolders) {
DirectoryWalker.builder()
.root(dir.toPath())
.extensions("rs", "fs")
.action((start, path) -> renderscriptFiles.add(path.toFile()))
.build()
.walk();
}
doMainCompilation(renderscriptFiles, processExecutor, processOutputHandler, env);
}

Task 执行时会首先创建一个处理器,然后遍历 src/main/rs 下所有的文件,遍历到后缀为 rs 的文件后,进行处理。

private void doMainCompilation(...) {
String renderscript = mBuildToolInfo.getPath(BuildToolInfo.PathId.LLVM_RS_CC);
String rsPath = mBuildToolInfo.getPath(BuildToolInfo.PathId.ANDROID_RS);
String rsClangPath = mBuildToolInfo.getPath(BuildToolInfo.PathId.ANDROID_RS_CLANG);
File rawFolder = new File(mResOutputDir, SdkConstants.FD_RES_RAW);
builder.setExecutable(renderscript);
builder.addEnvironments(env);
builder.addArgs("-O");
builder.addArgs(Integer.toString(mOptimizationLevel));
builder.addArgs("-I");
builder.addArgs(rsPath);
builder.addArgs("-I");
builder.addArgs(rsClangPath);
for (File importPath : mImportFolders) {
if (importPath.isDirectory()) {
builder.addArgs("-I");
builder.addArgs(importPath.getAbsolutePath());
}
}
if (mSupportMode) {
builder.addArgs("-rs-package-name=android.support.v8.renderscript");
}
builder.addArgs("-p");
builder.addArgs(mSourceOutputDir.getAbsolutePath());
builder.addArgs("-o");
builder.addArgs(rawFolder.getAbsolutePath());
builder.addArgs("-target-api");
int targetApi = mTargetApi < 11 ? 11 : mTargetApi;
targetApi = (mSupportMode && targetApi < 18) ? 18 : targetApi;
builder.addArgs(Integer.toString(targetApi));
for (File sourceFile : inputFiles) {
builder.addArgs(sourceFile.getAbsolutePath());
}
ProcessResult result = processExecutor.execute(
builder.createProcess(), processOutputHandler);
result.rethrowFailure().assertNormalExitValue();
}

处理过程也很简单,也就是拼接参数然后执行 build-tools 下面的 llvm-rs-cc 工具,执行完毕后会生成对应的 java 类,在代码中就可以使用了。

注: RenderScript 是用于在 Android 上以高性能运行计算密集型任务的框架。官方文档

checkDebugManifest

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createCheckManifestTask

public void createCheckManifestTask(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
scope.setCheckManifestTask(
androidTasks.create(tasks, getCheckManifestConfig(scope)));
scope.getCheckManifestTask().dependsOn(tasks, scope.getPreBuildTask());
}
protected CheckManifest.ConfigAction getCheckManifestConfig(@NonNull VariantScope scope) {
return new CheckManifest.ConfigAction(scope, false);
}

具体看下 CheckManifest.ConfigAction 。

public class CheckManifest extends DefaultAndroidTask {
@TaskAction
void check() {
if (!isOptional && manifest != null && !manifest.isFile()) {
throw new IllegalArgumentException(
String.format(
"Main Manifest missing for variant %1$s. Expected path: %2$s",
getVariantName(), getManifest().getAbsolutePath()));
}
}
public static class ConfigAction implements TaskConfigAction<CheckManifest> {
public Class<CheckManifest> getType() {
return CheckManifest.class;
}
}
}

这个 Task 很简单,就是检查了下 AndroidManifest.xml 这个文件是否存在,不存在则抛出异常。

generateDebugBuildConfig

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createBuildConfigTask

public void createBuildConfigTask(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
AndroidTask<GenerateBuildConfig> generateBuildConfigTask =
androidTasks.create(tasks, new GenerateBuildConfig.ConfigAction(scope));
scope.setGenerateBuildConfigTask(generateBuildConfigTask);
scope.getSourceGenTask().dependsOn(tasks, generateBuildConfigTask.getName());
generateBuildConfigTask.dependsOn(tasks, scope.getCheckManifestTask());
}

具体看下 GenerateBuildConfig.ConfigAction 。

public class GenerateBuildConfig extends BaseTask {
@TaskAction
void generate() throws IOException {
File destinationDir = getSourceOutputDir();
FileUtils.cleanOutputDir(destinationDir);
BuildConfigGenerator generator = new BuildConfigGenerator(
getSourceOutputDir(),
getBuildConfigPackageName());
generator
.addField(
"boolean",
"DEBUG",
isDebuggable() ? "Boolean.parseBoolean(\"true\")" : "false")
.addField("String", "APPLICATION_ID", '"' + appPackageName.get() + '"')
.addField("String", "BUILD_TYPE", '"' + getBuildTypeName() + '"')
.addField("String", "FLAVOR", '"' + getFlavorName() + '"')
.addField("int", "VERSION_CODE", Integer.toString(getVersionCode()))
.addField(
"String", "VERSION_NAME", '"' + Strings.nullToEmpty(getVersionName()) + '"')
.addItems(getItems());
List<String> flavors = getFlavorNamesWithDimensionNames();
int count = flavors.size();
if (count > 1) {
for (int i = 0; i < count; i += 2) {
generator.addField(
"String", "FLAVOR_" + flavors.get(i + 1), '"' + flavors.get(i) + '"');
}
}
generator.generate();
}
public static final class ConfigAction implements TaskConfigAction<GenerateBuildConfig> {
public Class<GenerateBuildConfig> getType() {
return GenerateBuildConfig.class;
}
}
}
public void generate() throws IOException {
File pkgFolder = getFolderPath();
File buildConfigJava = new File(pkgFolder, BUILD_CONFIG_NAME);
Closer closer = Closer.create();
try {
FileOutputStream fos = closer.register(new FileOutputStream(buildConfigJava));
OutputStreamWriter out = closer.register(new OutputStreamWriter(fos, Charsets.UTF_8));
JavaWriter writer = closer.register(new JavaWriter(out));
writer.emitJavadoc("Automatically generated file. DO NOT MODIFY")
.emitPackage(mBuildConfigPackageName)
.beginType("BuildConfig", "class", PUBLIC_FINAL);
for (ClassField field : mFields) {
emitClassField(writer, field);
}
for (Object item : mItems) {
if (item instanceof ClassField) {
emitClassField(writer, (ClassField) item);
} else if (item instanceof String) {
writer.emitSingleLineComment((String) item);
}
}
writer.endType();
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}

Task 内部添加了以下几个字段,以及用户自定义的字段,然后生成 BuildConfig.java 。

  1. DEBUG
  2. APPLICATION_ID
  3. BUILD_TYPE
  4. FLAVOR
  5. VERSION_CODE
  6. VERSION_NAME
  7. FLAVOR_XXX

自定义 BuildConfig 字段方法如下:

buildTypes {
debug{
buildConfigField "String", "Test", '"Hello, World!"'
}
release {
buildConfigField "String", "Test", '"Hello, World!"'
}
}

prepareLintJar

BasePlugin#createAndroidTasks

TaskManager#configureCustomLintChecks

public void configureCustomLintChecks(@NonNull TaskFactory tasks) {
File lintJar = FileUtils.join(globalScope.getIntermediatesDir(), "lint", FN_LINT_JAR);
AndroidTask<PrepareLintJar> copyLintTask =
getAndroidTasks()
.create(tasks, new PrepareLintJar.ConfigAction(globalScope, lintJar));
globalScope.addTaskOutput(LINT_JAR, lintJar, copyLintTask.getName());
}

具体看下 PrepareLintJar.ConfigAction 。

public class PrepareLintJar extends DefaultTask {
@TaskAction
public void prepare() throws IOException {
Set<File> files = lintChecks.getFiles();
if (files.size() > 1) {
throw new RuntimeException(
"Found more than one jar in the '"
+ VariantDependencies.CONFIG_NAME_LINTCHECKS
+ "' configuration. Only one file is supported. If using a separate Gradle project, make sure compilation dependencies are using compileOnly");
}
if (files.isEmpty()) {
if (outputLintJar.isFile()) {
FileUtils.delete(outputLintJar);
}
} else {
FileUtils.mkdirs(outputLintJar.getParentFile());
Files.copy(Iterables.getOnlyElement(files), outputLintJar);
}
}
public static class ConfigAction implements TaskConfigAction<PrepareLintJar> {
public Class<PrepareLintJar> getType() {
return PrepareLintJar.class;
}
}
}

该 Task 用于处理自定义的 Lint 规则生成的 jar 包,如果有自定义的并且只有一个那么拷贝到目标路径,如果有多个则报错。

注:自定义 Lint 教程

generateDebugResValues

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createGenerateResValuesTask

public void createGenerateResValuesTask(
@NonNull TaskFactory tasks,
@NonNull VariantScope scope) {
AndroidTask<GenerateResValues> generateResValuesTask = androidTasks.create(
tasks, new GenerateResValues.ConfigAction(scope));
scope.getResourceGenTask().dependsOn(tasks, generateResValuesTask);
}

具体看下 GenerateResValues.ConfigAction 。

public class GenerateResValues extends BaseTask {
@TaskAction
void generate() throws IOException, ParserConfigurationException {
File folder = getResOutputDir();
List<Object> resolvedItems = getItems();
if (resolvedItems.isEmpty()) {
FileUtils.cleanOutputDir(folder);
} else {
ResValueGenerator generator = new ResValueGenerator(folder);
generator.addItems(getItems());
generator.generate();
}
}
public static class ConfigAction implements TaskConfigAction<GenerateResValues> {
public Class<GenerateResValues> getType() {
return GenerateResValues.class;
}
}
}
public void generate() throws IOException, ParserConfigurationException {
File pkgFolder = getFolderPath();
File resFile = new File(pkgFolder, RES_VALUE_FILENAME_XML);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(false);
factory.setIgnoringComments(true);
DocumentBuilder builder;
builder = factory.newDocumentBuilder();
Document document = builder.newDocument();
Node rootNode = document.createElement(TAG_RESOURCES);
document.appendChild(rootNode);
rootNode.appendChild(document.createTextNode("\n"));
rootNode.appendChild(document.createComment("Automatically generated file. DO NOT MODIFY"));
rootNode.appendChild(document.createTextNode("\n\n"));
for (Object item : mItems) {
if (item instanceof ClassField) {
ClassField field = (ClassField)item;
ResourceType type = ResourceType.getEnum(field.getType());
boolean hasResourceTag = (type != null && RESOURCES_WITH_TAGS.contains(type));
Node itemNode = document.createElement(hasResourceTag ? field.getType() : TAG_ITEM);
Attr nameAttr = document.createAttribute(ATTR_NAME);
nameAttr.setValue(field.getName());
itemNode.getAttributes().setNamedItem(nameAttr);
if (!hasResourceTag) {
Attr typeAttr = document.createAttribute(ATTR_TYPE);
typeAttr.setValue(field.getType());
itemNode.getAttributes().setNamedItem(typeAttr);
}
if (type == ResourceType.STRING) {
Attr translatable = document.createAttribute(ATTR_TRANSLATABLE);
translatable.setValue(VALUE_FALSE);
itemNode.getAttributes().setNamedItem(translatable);
}
if (!field.getValue().isEmpty()) {
itemNode.appendChild(document.createTextNode(field.getValue()));
}
rootNode.appendChild(itemNode);
} else if (item instanceof String) {
rootNode.appendChild(document.createTextNode("\n"));
rootNode.appendChild(document.createComment((String) item));
rootNode.appendChild(document.createTextNode("\n"));
}
}
String content;
try {
content = XmlPrettyPrinter.prettyPrint(document, true);
} catch (Throwable t) {
content = XmlUtils.toXml(document);
}
Files.write(content, resFile, Charsets.UTF_8);
}

该 Task 逻辑也很清晰,获取到自定义的 resValue 值,然后创建 generated.xml 将自定义的资源信息写入其中。

自定义 resValue 方式如下:

buildTypes {
release {
resValue "string", "Test", "Hello, World!"
}
debug {
resValue "string", "Test", "Hello, World!"
}
}

最终生成的 generated.xml 文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Automatically generated file. DO NOT MODIFY -->
<!-- Values from build type: debug -->
<string name="Test" translatable="false">Hello, World!</string>
</resources>

generateDebugResources

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#createAnchorTasks

public void createAnchorTasks(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
scope.setResourceGenTask(androidTasks.create(tasks,
scope.getTaskName("generate", "Resources"),
Task.class,
task -> {
variantData.resourceGenTask = task;
}));
}

空实现,啥也没做,就是个锚点,标志着资源处理开始了。

mergeDebugResources

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createMergeResourcesTask

TaskManager#basicCreateMergeResourcesTask

public AndroidTask<MergeResources> basicCreateMergeResourcesTask(...) {
AndroidTask<MergeResources> mergeResourcesTask =
androidTasks.create(
tasks,
new MergeResources.ConfigAction(...));
return scope.getMergeResourcesTask();
}

具体看下 MergeResources.ConfigAction 。

public class MergeResources extends IncrementalTask {
protected void doFullTaskAction() throws IOException, ExecutionException {
ResourcePreprocessor preprocessor = getPreprocessor();
File destinationDir = getOutputDir();
FileUtils.cleanOutputDir(destinationDir);
List<ResourceSet> resourceSets = getConfiguredResourceSets(preprocessor); // 1
ResourceMerger merger = new ResourceMerger(minSdk);
try (QueueableResourceCompiler resourceCompiler =
processResources
? makeAapt(...)
: QueueableResourceCompiler.NONE) {
for (ResourceSet resourceSet : resourceSets) {
resourceSet.loadFromFiles(getILogger()); // 2
merger.addDataSet(resourceSet);
}
MergedResourceWriter writer =
new MergedResourceWriter(...);
merger.mergeData(writer, false /*doCleanUp*/); // 3
merger.writeBlobTo(getIncrementalFolder(), writer, false);
} catch (MergingException e) {
throw new ResourceException(e.getMessage(), e);
} finally {
cleanup();
}
}
public static class ConfigAction implements TaskConfigAction<MergeResources> {
public Class<MergeResources> getType() {
return MergeResources.class;
}
}
}

大致流程为:

  1. 获取所有 ResourceSet 实例,其实也就是 App 所有的资源集合,包括自身以及依赖的。
  2. 加载每个 ResourceSet 下面的资源文件,检查并维护进内存中。
  3. 合并多个 ResourceSet 下面的资源信息。

下面一个个来进行分析。

获取所有 ResourceSet

MergeResources#doFullTaskAction

MergeResources#getConfiguredResourceSets

MergeResources#computeResourceSetList

private List<ResourceSet> getConfiguredResourceSets(ResourcePreprocessor preprocessor) {
if (processedInputs == null) {
processedInputs = computeResourceSetList();
List<ResourceSet> generatedSets = Lists.newArrayListWithCapacity(processedInputs.size());
for (ResourceSet resourceSet : processedInputs) { // 4
resourceSet.setPreprocessor(preprocessor);
ResourceSet generatedSet = new GeneratedResourceSet(resourceSet);
resourceSet.setGeneratedSet(generatedSet);
generatedSets.add(generatedSet);
}
for (int i = 0; i < generatedSets.size(); ++i) { // 5
processedInputs.add(2 * i, generatedSets.get(i));
}
}
return processedInputs;
}
List<ResourceSet> computeResourceSetList() {
List<ResourceSet> sourceFolderSets = resSetSupplier.get(); // 1
if (libraries != null) { // 2
size += libraries.getArtifacts().size();
}
int size = sourceFolderSets.size() + 4;
List<ResourceSet> resourceSetList = Lists.newArrayListWithExpectedSize(size);
resourceSetList.addAll(sourceFolderSets);
List<File> generatedResFolders = Lists.newArrayList();
generatedResFolders.addAll(renderscriptResOutputDir.getFiles());
generatedResFolders.addAll(generatedResOutputDir.getFiles());
final ResourceSet mainResourceSet = sourceFolderSets.get(0);
assert mainResourceSet.getConfigName().equals(BuilderConstants.MAIN);
mainResourceSet.addSources(generatedResFolders); // 3
return resourceSetList;
}

在分析代码前首先看看 ResourceSet 相关类图:

截屏2021-02-23 下午4.22.33

根据类图可以看到 DataSet 拥有一个 mSourceFile 字段,该字段表示的就是该 ResourceSet 所拥有的一系列文件。注意其是个 List ,一般条件下只有一个,但是可能会有多个,稍后第三步就将看到。

  1. 获取应用程序的 ResourceSet 列表,这里会获取到两个实例,其 sourceFile 各只有一个文件夹分别是 src/main/res 以及 src/debug/res。注:因为执行的是 assembleDebug,所以会有 src/debug/res。
  2. 获取应用程序依赖库的 ResourceSet 列表,这里的数量根据依赖而定,不过 sourceFile 一般也是一个,暂时不知道什么情况会出现多个。
  3. 给 mainResourceSet 添加以下两个目录,这两个目录分别是上述处理 RenderScript 以及 ResValues 生成的目录,添加完毕后该 ResourceSet 就会拥有三个 sourceFile。
    • build/generated/res/rs/debug
    • build/generated/resValues/debug
  4. 为每个 ResourceSet 创建对应的 GeneratedResourceSet ,然后设置给 ResourceSet。
  5. 将每个 GeneratedResourceSet 间隔插入到 ResourceSet 列表中,最终顺序为 A-Generated、A、B-Generated、B 。

看到这又产生疑问了,为什么需要这个 GeneratedResourceSet,这难道不是多此一举吗?

原因是在资源合并过程中会自动生成一些资源(比如后面会说到的 vector 资源的处理),这些资源就被放入对应的 GeneratedResourceSet 中,这又会引出两个问题。

  1. 为什么不直接放入 ResourceSet ?根据注释这是因为如果直接放入其中,如果增量编译失败那么又需要重新去获取 ResourceSet,影响效率,因为才会这么做。
  2. 为什么需要按这种顺序进行插入?因为最终生成资源时会倒序进行处理,这样能保证 GeneratedResourceSet 比起对应的 ResourceSet 优先级低,但是又比原先优先级低于它的 ResourceSet 优先级高。

加载所有 ResourceSet

MergeResources#doFullTaskAction

MergeResources#getConfiguredResourceSets

ResourcesSet#loadFromFiles

从上述类图可以看出,ResourceSet 没有重写 loadFromFile ,但是 GeneratedResourceSet 重写了。

// DataSet
public void loadFromFiles(ILogger logger) throws MergingException {
List<Message> errors = Lists.newArrayList();
for (File file : mSourceFiles) {
if (file.isDirectory()) {
try {
readSourceFolder(file, logger); // 1
} catch (MergingException e) {
errors.addAll(e.getMessages());
}
} else if (file.isFile()) {}
}
MergingException.throwIfNonEmpty(errors);
checkItems(); // 2
}
// GeneratedResourceSet
public void loadFromFiles(ILogger logger) throws MergingException {
// Do nothing, the original set will hand us the generated files.
}
  1. 读取每个 sourceFile 文件夹。
  2. 检查该 ResourceSet 是否存在重复资源。
读取 sourceFile 文件夹

readSourceFolder 方法由 ResourceSet 进行实现。

protected void readSourceFolder(File sourceFolder, ILogger logger)
throws MergingException {
List<Message> errors = Lists.newArrayList();
File[] folders = sourceFolder.listFiles();
if (folders != null) {
for (File folder : folders) {
if (folder.isDirectory() && !isIgnored(folder)) {
FolderData folderData = getFolderData(folder); // 1
if (folderData != null) {
try {
parseFolder(sourceFolder, folder, folderData, logger); // 2
} catch (MergingException e) {
errors.addAll(e.getMessages());
}
}
}
}
}
MergingException.throwIfNonEmpty(errors);
}
  1. 根据文件夹名称解析文件夹类型以及限定信息(- 后面的),生成 FolderData 实例。(细节忽略)
  2. 对每个 sourceFile 下的每个子文件进行读取。
private void parseFolder(File sourceFolder, File folder, FolderData folderData, ...) {
File[] files = folder.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
if (!file.isFile() || isIgnored(file)) {
continue;
}
ResourceFile resourceFile = createResourceFile(file, folderData, logger); // 1
processNewResourceFile(sourceFolder, resourceFile); // 2
}
}
}
  1. 读取每个资源文件,并据此创建 ResourceFile 实例。
  2. 处理新生成的 ResourceFile 实例。
private ResourceFile createResourceFile(@NonNull File file,
@NonNull FolderData folderData, @NonNull ILogger logger) throws MergingException {
if (getValidateEnabled()) {
FileResourceNameValidator.validate(file, folderData.folderType); // 1
}
if (folderData.type != null) { // 2
if (needsPreprocessing(file)) { // 3
return ResourceFile.generatedFiles(
file,
getResourceItemsForGeneratedFiles(file),
folderData.qualifiers,
folderData.folderConfiguration);
} else {
return new ResourceFile( // 4
file,
new ResourceItem(...),
folderData.qualifiers,
folderData.folderConfiguration);
}
} else {
try {
ValueResourceParser2 parser =
new ValueResourceParser2(file, mNamespace, mLibraryName);
parser.setTrackSourcePositions(mTrackSourcePositions);
List<ResourceItem> items = parser.parseFile(); // 5
return new ResourceFile(...);
} catch (MergingException e) {
throw e;
}
}
}
  1. 校验资源文件后缀名,比如 raw 文件夹,所有文件后缀都可,drawable、mipmap 文件夹,支持后缀为 xml、png、9.png、gif、jpeg、jpg、bmp、webp 文件。
  2. 只有 values 相关文件夹 folderData.type == null ,因此非 values 文件夹下的文件才会进入。
  3. 当 minSdk 小于 21、文件后缀为 xml 、位于 drawable 文件夹、同时根节点为 vector 时需要预处理。具体流程为在每个图片质量文件夹中增加一个对应的 png 文件以及在 anydpi 增加一个该 xml 文件。然后创建 ResourceFile 实例返回,该实例文件类型为 GENERATED_FILES 。
  4. 不需要预处理那么直接创建 ResourceFile 实例返回,该实例文件类型为 SINGLE_FILE 。
  5. 如果是 values 文件夹下的文件,需要对每个文件进行解析,然后创建 ResourceFile 实例返回,该实例文件类型为 XML_VALUES。

主要关注下 values 文件夹下的资源文件的解析。

List<ResourceItem> parseFile() throws MergingException {
Document document = parseDocument(mFile, mTrackSourcePositions);
Node rootNode = document.getDocumentElement();
if (rootNode == null) {
return Collections.emptyList();
}
NodeList nodes = rootNode.getChildNodes();
final int count = nodes.getLength();
List<ResourceItem> resources = Lists.newArrayListWithExpectedSize(count);
Map<ResourceType, Set<String>> map = Maps.newEnumMap(ResourceType.class);
for (int i = 0, n = nodes.getLength(); i < n; i++) {
Node node = nodes.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
ResourceItem resource = getResource(node, mFile, mNamespace, mLibraryName);
if (resource != null) {
checkDuplicate(resource, map, mFile);
resources.add(resource);
if (resource.getType() == ResourceType.DECLARE_STYLEABLE) {
addStyleableItems(node, resources, map, mFile, mNamespace, mLibraryName);
}
}
}
return resources;
}

读取每一个节点,判断单个文件中是否存在重复的,如存在就抛出异常,不存在则存起来,对于 declare-styleable 会将其内部的 attr 节点(需要有 format 属性或者是 enum 或 flag)也加入到 resources 中,然后以此创建 ResourFile 实例。

回到上文 ,对每个资源文件处理生成 ResourceFile 实例后还需要调用 processNewResourceFile 进行处理。

private void processNewResourceFile(File sourceFolder, ResourceFile resourceFile) {
if (resourceFile != null) {
if (resourceFile.getType() == DataFile.FileType.GENERATED_FILES
&& mGeneratedSet != null) {
mGeneratedSet.processNewDataFile(sourceFolder, resourceFile, true); // 1
} else {
processNewDataFile(sourceFolder, resourceFile, true /*setTouched*/); // 2
}
}
}
protected void processNewDataFile(...) throws MergingException {
Collection<I> dataItems = dataFile.getItems();
addDataFile(sourceFolder, dataFile);
for (I dataItem : dataItems) {
mItems.put(dataItem.getKey(), dataItem);
if (setTouched) {
dataItem.setTouched();
}
}
}
private void addDataFile(@NonNull File sourceFile, @NonNull F dataFile) {
mSourceFileToDataFilesMap.put(sourceFile, dataFile);
mDataFileMap.put(dataFile.getFile(), dataFile);
}
  1. 如果该实例文件类型为 GENERATED_FILES 那么由其对应的 GeneratedResourceSet 进行处理。
  2. 如果该实例文件类型非 GENERATED_FILES 那么由当前 ResourceSet 进行处理。

处理过程就是将相关信息维护到以下三个数据结构中,注意 ListMultimap 与普通 Map 不同 put 时如果遇到相同 key 并不会进行覆盖,而是会在该 key 对应的列表中添加它。

  1. mSourceFileToDataFilesMap ListMultimap sourceFile => ResourceFile
  2. mDataFileMap HashMap file => ResourceFile
  3. mItems ListMultimap key(诸如 attr/cardViewStyle) => ResourceItem
检查 ResourceSet 资源

当处理完该 ResourceSet 下所有的文件时,就会调用 checkItems 来检查是否存在重复。

protected void checkItems() throws DuplicateDataException {
if (!mValidateEnabled) {
return;
}
Collection<Collection<I>> duplicateCollections = Lists.newArrayList();
for (Map.Entry<String, Collection<I>> entry : mItems.asMap().entrySet()) {
Collection<I> items = entry.getValue();
I lastItem = null;
for (I item : items) {
if (!item.isRemoved()) {
if (lastItem == null) {
lastItem = item;
} else {
duplicateCollections.add(items);
}
}
}
}
if (!duplicateCollections.isEmpty()) {
throw new DuplicateDataException(DuplicateDataException.createMessages(duplicateCollections));
}
}

item 的状态不用考虑,现在全是 touched,只有当增量编译的时候才会出现 removed,取出每个 key 对应的 ResourceItem 列表,如果发现列表数大于 1 个,那么后续就会抛出异常。

合并多个 ResourceSet 资源

当资源信息全部读取进内存后,需要进行资源合并,合并后的资源将做为 Apk 的资源。

public void mergeData(@NonNull MergeConsumer<I> consumer, boolean doCleanUp)
throws MergingException {
consumer.start(mFactory);
try {
Set<String> dataItemKeys = Sets.newHashSet();
for (S dataSet : mDataSets) { // 1
ListMultimap<String, I> map = dataSet.getDataMap();
dataItemKeys.addAll(map.keySet());
}
for (String dataItemKey : dataItemKeys) {
if (requiresMerge(dataItemKey)) { // 2
List<I> items = Lists.newArrayListWithExpectedSize(mDataSets.size());
for (S dataSet : mDataSets) {
ListMultimap<String, I> itemMap = dataSet.getDataMap();
if (itemMap.containsKey(dataItemKey)) {
List<I> setItems = itemMap.get(dataItemKey);
items.addAll(setItems);
}
}
mergeItems(dataItemKey, items, consumer);
continue;
}
I previouslyWritten = null;
I toWrite = null;
boolean foundIgnoredItem = false;
// 高优先级 到 低优先级
setLoop: for (int i = mDataSets.size() - 1 ; i >= 0 ; i--) {
S dataSet = mDataSets.get(i);
ListMultimap<String, I> itemMap = dataSet.getDataMap();
if (!itemMap.containsKey(dataItemKey)) {
continue;
}
List<I> items = itemMap.get(dataItemKey);
if (items.isEmpty()) {
continue;
}
// 遇到重复的了
for (int ii = items.size() - 1 ; ii >= 0 ; ii--) {
I item = items.get(ii);
if (consumer.ignoreItemInMerge(item)) { // 3
foundIgnoredItem = true;
continue;
}
if (item.isWritten()) { // 忽略,增量编译
assert previouslyWritten == null;
previouslyWritten = item;
}
if (toWrite == null && !item.isRemoved()) { // 4
toWrite = item;
}
if (toWrite != null && previouslyWritten != null) { // 忽略
break setLoop;
}
}
}
// 没有重复进行下一个 ResourceSet
if (previouslyWritten == null && toWrite == null) {
continue;
}
if (toWrite == null) {
assert previouslyWritten.isRemoved();
consumer.removeItem(previouslyWritten, null /*replacedBy*/);
} else if (previouslyWritten == null || previouslyWritten == toWrite) {
consumer.addItem(toWrite); // 5
} else {
toWrite.setTouched();
consumer.addItem(toWrite);
consumer.removeItem(previouslyWritten, toWrite);
}
}
} finally {
consumer.end();
}
if (doCleanUp) {
postMergeCleanUp();
}
}
  1. 遍历所有的 ResourceSet 实例,将其内部所有的 ResourceFile 实例(也就是该 App 所有的资源信息)全部加入到 dataItemKeys 中。

  2. 对于 declare-styleable 资源,需要对其的内部属性进行合并。

  3. 忽略 declare-styleable 下面的有 format 属性或者 enum、flag 的 attr 资源,因为第二步已经加了。

  4. 由于是逆序遍历,在多个 ResourceSet 中遇到重复资源了就只会写入优先级最高的那一个。

  5. 将最终选择资源传递给 consumer 也就是 MergedResourceWriter 进行输出。

合并 declare-styleable

上面 mergeData 每遍历到一个 declare-styleable 资源,都会编译所有 ResourceSet 的资源,收集相同名称的 declare-styleable 资源,然后调用 mergeItems。

protected void mergeItems(...) throws MergingException {
ResourceItem sourceItem = items.get(0);
ResourceItem previouslyWrittenItem = getMergedItem(qualifier, itemName);
try {
DocumentBuilder builder = mFactory.newDocumentBuilder();
Document document = builder.newDocument();
Node declareStyleableNode = document.createElementNS(null, TAG_DECLARE_STYLEABLE);
Attr nameAttr = document.createAttribute(ATTR_NAME);
nameAttr.setValue(itemName);
declareStyleableNode.getAttributes().setNamedItem(nameAttr);
Set<String> attrs = Sets.newHashSet();
for (ResourceItem item : items) {
Node oldDeclareStyleable = item.getValue();
NodeList children = oldDeclareStyleable.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node attrNode = children.item(i);
NamedNodeMap attributes = attrNode.getAttributes();
nameAttr = (Attr) attributes.getNamedItemNS(null, ATTR_NAME);
if (nameAttr == null) {
continue;
}
String name = nameAttr.getNodeValue();
if (attrs.contains(name)) {
continue;
}
attrs.add(name);
Node newAttrNode = NodeUtils.duplicateNode(document, attrNode);
declareStyleableNode.appendChild(newAttrNode);
}
}
MergedResourceItem newItem = new MergedResourceItem(...);
consumer.addItem(newItem);
} catch (ParserConfigurationException e) {
throw MergingException.wrapException(e).build();
}
}

原理大概是遍历所有相同名称的 declare-styleable 资源,收集所有内部的 attr,然后创建一个 MergedResourceItem ,然后传递给 MergedResourceWriter。

使用 MergedResourceWriter 输出

可以看到在 mergeData 方法中首先会调用 consumer.start ,然后中间会调用若干次 consumer.addItem,最终调用 consumer.end。跟进下这三个方法。

public void start(@NonNull DocumentBuilderFactory factory) throws ConsumerException {
super.start(factory);
mValuesResMap = ArrayListMultimap.create();
mQualifierWithDeletedValues = Sets.newHashSet();
mFactory = factory;
}

start 方法没什么特别的就是初始化一些成员属性。

public void addItem(@NonNull final ResourceItem item) throws ConsumerException {
final ResourceFile.FileType type = item.getSourceType();
if (type == ResourceFile.FileType.XML_VALUES) {
mValuesResMap.put(item.getQualifiers(), item); // 1
} else {
File file = item.getFile();
String folderName = getFolderName(item);
if (type == DataFile.FileType.GENERATED_FILES) {
FileGenerationParameters workItem = new FileGenerationParameters(item, mPreprocessor);
if (workItem.resourceItem.getSource() != null) {
getExecutor().submit(workItem); // 2
}
}
mCompileResourceRequests.add(
new CompileResourceRequest(file, getRootFolder(), folderName));
}
}
  1. 对于 values 资源,按照限定符进行分开存放。
  2. 如果文件类型为 GENERATED_FILES,那么需要自动生成对应的文件,最终通过 FileGenerationWorkAction 进行生成,不展开了。
  3. 对于非 values 资源,新建个 CompileResourceRequest 实例存放。
public void end() throws ConsumerException {
super.end(); // 1
Map<Future, String> outstandingRequests = new HashMap<>();
File tmpDir = new File(mTemporaryDirectory, "stripped.dir");
while (!mCompileResourceRequests.isEmpty()) {
CompileResourceRequest request = mCompileResourceRequests.poll();
Future<File> result;
File fileToCompile = request.getInput();
result = mResourceCompiler.compile(new CompileResourceRequest(...)); // 2
outstandingRequests.put(result, request.getInput().getAbsolutePath());
mCompiling.add(result);
}
Future<File> first;
while ((first = mCompiling.pollFirst()) != null) {
// 等待每个资源编译完成
File outFile = first.get();
}
}
  1. 处理 values 资源,以及等待 vector 生成对应的 png 图片生成完成。
  2. 编译资源,内部就是调用 aapt2 工具进行处理。
public void end() throws ConsumerException {
postWriteAction();
getExecutor().await();
}
protected void postWriteAction() throws ConsumerException {
for (String key : mValuesResMap.keySet()) {
List<ResourceItem> items = mValuesResMap.get(key);
String folderName = key.isEmpty() ?
ResourceFolderType.VALUES.getName() :
ResourceFolderType.VALUES.getName() + RES_QUALIFIER_SEP + key;
File valuesFolder = new File(tmpDir, folderName);
File outFile = new File(valuesFolder, folderName + DOT_XML);
FileUtils.mkdirs(valuesFolder);
DocumentBuilder builder = mFactory.newDocumentBuilder();
Document document = builder.newDocument();
final String publicTag = ResourceType.PUBLIC.getName();
List<Node> publicNodes = null;
Node rootNode = document.createElement(TAG_RESOURCES);
document.appendChild(rootNode);
Collections.sort(items);
for (ResourceItem item : items) {
Node nodeValue = item.getValue();
rootNode.appendChild(document.createTextNode("\n "));
ResourceFile source = item.getSource();
Node adoptedNode = NodeUtils.adoptNode(document, nodeValue);
if (source != null) {
XmlUtils.attachSourceFile(
adoptedNode, new SourceFile(source.getFile()));
}
rootNode.appendChild(adoptedNode);
}
rootNode.appendChild(document.createTextNode("\n"));
final String content;
Files.write(content, outFile, Charsets.UTF_8); // 1
CompileResourceRequest request = new CompileResourceRequest(...);
mResourceCompiler.compile(request).get(); // 2
}
}
  1. 在每个限定文件夹下生成 values.xml 。比如 values/values.xml、values-v23/values.xml 等等。
  2. 使用 aapt2 编译生成的每个 values.xml 文件。

至此该 Task 处理完毕,其重要内容就是把所有资源按优先级进行合并,将所有的 values 资源合并生成 values.xml 然后,然后使用 aapt2 进行编译。

createDebugCompatibleScreenManifests

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#createMergeApkManifestsTask

public void createMergeApkManifestsTask(
@NonNull TaskFactory tasks, @NonNull VariantScope variantScope) {
AndroidTask<CompatibleScreensManifest> csmTask =
androidTasks.create(tasks,
new CompatibleScreensManifest.ConfigAction(variantScope, screenSizes));
}

具体看下 CompatibleScreensManifest.ConfigAction。

public class CompatibleScreensManifest extends DefaultAndroidTask {
@TaskAction
public void generateAll() throws IOException {
outputScope.parallelForEach(
VariantScope.TaskOutputType.COMPATIBLE_SCREEN_MANIFEST, this::generate);
outputScope.save(
ImmutableList.of(VariantScope.TaskOutputType.COMPATIBLE_SCREEN_MANIFEST),
outputFolder);
}

public File generate(ApkData apkData) throws IOException {
String density = apkData.getFilter(com.android.build.OutputFile.FilterType.DENSITY);
if (density == null) {
return null;
}
StringBuilder content = new StringBuilder();
content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
.append("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n")
.append(" package=\"\">\n")
.append("\n");
if (minSdkVersion.get() != null) {
content.append(" <uses-sdk android:minSdkVersion=\"")
.append(minSdkVersion.get())
.append("\"/>\n");
}
content.append(" <compatible-screens>\n");
density = convert(density, Density.XXHIGH, Density.XXXHIGH);
for (String size : getScreenSizes()) {
content.append(
" <screen android:screenSize=\"").append(size).append("\" "
+ "android:screenDensity=\"").append(density).append("\" />\n");
}
content.append(
" </compatible-screens>\n" +
"</manifest>");
File splitFolder = new File(outputFolder, apkData.getDirName());
FileUtils.mkdirs(splitFolder);
File manifestFile = new File(splitFolder, SdkConstants.ANDROID_MANIFEST_XML);
Files.write(content.toString(), manifestFile, Charsets.UTF_8);
return manifestFile;
}
public static class ConfigAction implements TaskConfigAction<CompatibleScreensManifest> {
@Override
public Class<CompatibleScreensManifest> getType() {
return CompatibleScreensManifest.class;
}
}
}

这个 Task 主要是配合 splits 分包的,由于一般情况下载 Google Play 发布时才可能用到,暂时先不关注。具体可以看下这里

processDebugManifest

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#createMergeApkManifestsTask

protected AndroidTask<? extends ManifestProcessorTask> createMergeManifestTask(...) {
final File reportFile = computeManifestReportFile(variantScope);
AndroidTask<MergeManifests> mergeManifestsAndroidTask =
androidTasks.create(tasks, new MergeManifests.ConfigAction(
variantScope, optionalFeatures.build(), reportFile)
);
}

很明显这个 Task 是用于合并清单文件的,代码逻辑很复杂暂时就先不看了,先根据 官方文档 了解下原理。

img

首先会将所有清单文件按优先级排序,然后将优先级最低的合并到优先级第二低的,然后再合并到第三低的,依次类推,直到合并到优先级最高的清单文件中。

清单文件优先级如下:

  1. 构建变体的清单文件,以 demoDebug(productFlavor = demo,buildType = debug)为例。

    a. src/demoDebug/AndroidManifest.xml

    b. src/demo/AndroidManifest.xml

    c. src/debug/AndroidManifest.xml

  2. 应用程序的主清单文件

  3. 依赖库中的清单文件(先依赖的优先级高)

如果优先级较低的清单中的某个元素与优先级较高的清单中的所有元素都不匹配,则会将该元素添加到合并后的清单中。如果有匹配的元素,则合并工具会尝试将每个元素的所有属性组合到同一元素中。对于属性合并规则,官方文档又提供了下表来进行描述。

高优先级属性 低优先级属性 属性的合并结果
没有值 没有值 没有值(使用默认值)
值 B 值 B
值 A 没有值 值 A
值 A 值 A
值 B 冲突错误 - 您必须添加合并规则标记

大部分属性合并都是按上表,不过以下情况有些特殊;

  1. <manifest> 元素中的属性绝不会合并在一起,只会使用优先级最高的清单中的属性,也就是说会抛弃低优先级(例如库文件中的清单文件)的该元素属性。
  2. <uses-feature><uses-library> 元素中的 android:required 属性使用 OR 合并,也就是说只要有一个为 true,那么最终就是 true。
  3. 绝不会在清单之间匹配 <intent-filter> 元素。每个该元素都被视为唯一的元素,并添加到合并后的清单中的共同父元素中。

总结下其实就是读取低优先级的清单文件的每个元素,在高优先级的清单文件中查找是否存在该元素,如果不存在那么就直接新增,如果存在那么进行属性合并,整个低优先级清单文件都合并到高优先级的清单文件中后,再与更高优先级的进行合并。

splitsDiscoveryTaskDebug

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createApkProcessResTask

TaskManager#createProcessResTask

public AndroidTask<ProcessAndroidResources> createProcessResTask(...) {
AndroidTask<SplitsDiscovery> splitsDiscoveryAndroidTask = androidTasks.create(
tasks, new SplitsDiscovery.ConfigAction(scope, splitListOutputFile));
}
public class SplitsDiscovery extends BaseTask {
@TaskAction
void taskAction() throws IOException {
Set<File> mergedResourcesFolderFiles =
mergedResourcesFolders != null ? mergedResourcesFolders.getFiles() : null;
Collection<String> resConfigs = resourceConfigs;
if (resConfigAuto) {
resConfigs = discoverListOfResourceConfigsNotDensities();
}
SplitList.save(
getPersistedList(),
getFilters(mergedResourcesFolderFiles, DiscoverableFilterType.DENSITY),
getFilters(mergedResourcesFolderFiles, DiscoverableFilterType.LANGUAGE),
getFilters(ImmutableList.of(), DiscoverableFilterType.ABI),
resConfigs);
}
public static final class ConfigAction implements TaskConfigAction<SplitsDiscovery> {
public Class<SplitsDiscovery> getType() {
return SplitsDiscovery.class;
}
}
}

这个 Task 也是 split 分包相关的,暂时忽略。

processDebugResources

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createApkProcessResTask

TaskManager#createProcessResTask

public AndroidTask<ProcessAndroidResources> createProcessResTask(...) {
AndroidTask<ProcessAndroidResources> processAndroidResources =
androidTasks.create(tasks, createProcessAndroidResourcesConfigAction(...));
}
protected ProcessAndroidResources.ConfigAction createProcessAndroidResourcesConfigAction() {
return new ProcessAndroidResources.ConfigAction(
scope,
symbolLocation,
symbolWithPackageName,
resPackageOutputFolder,
useAaptToGenerateLegacyMultidexMainDexProguardRules,
sourceTaskOutputType,
baseName,
isLibrary());
}

方法内部代码很复杂,大致做的内容就是将前面编译后的资源以及合并后的清单文件做为输入源使用 aapt2 工具进行链接生成 resources-debug.ap_ 以及 R.java 文件。截屏2021-03-08 下午4.47.14

截屏2021-03-08 下午4.48.19

其实这就是 apk 中所有资源,将 ap_ 修改为 apk 就可以看到熟悉的 res 文件夹、resource.arsc 文件、AndroidManifest.xml 文件。

截屏2021-03-03 上午12.12.26

javaPreCompileDebug

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#addCompileTask

ApplicationTaskManager#createJavacTask

public AndroidTask<? extends JavaCompile> createJavacTask(
@NonNull final TaskFactory tasks,
@NonNull final VariantScope scope) {
File processorListFile =
FileUtils.join(
globalScope.getIntermediatesDir(),
"javaPrecompile",
scope.getDirName(),
"annotationProcessors.json");
AndroidTask<JavaPreCompileTask> preCompileTask =
androidTasks.create(
tasks, new JavaPreCompileTask.ConfigAction(scope, processorListFile));
}

具体看下 JavaPreCompileTask.ConfigAction 。

public class JavaPreCompileTask extends BaseTask {
@TaskAction
public void preCompile() throws IOException {
Set<String> classNames = Sets.newHashSet();
classNames.addAll(
convertArtifactsToNames(
collectAnnotationProcessors(annotationProcessorConfiguration)));
classNames.addAll(annotationProcessorOptions.getClassNames());
if (dataBindingEnabled) {
classNames.add(DATA_BINDING_SPEC);
}
FileUtils.deleteIfExists(processorListFile);
Gson gson = new GsonBuilder().create();
try (FileWriter writer = new FileWriter(processorListFile)) {
gson.toJson(classNames, writer);
}
}
public static class ConfigAction implements TaskConfigAction<JavaPreCompileTask> {
public Class<JavaPreCompileTask> getType() {
return JavaPreCompileTask.class;
}
}
}

获取所有 AnnotationProcessor 类名,然后写入

build/intermediates/javaPrecompile/debug/annotationProcessors.json 中。

获取 AnnotationProcessor

JavaPreCompileTask#collectAnnotationProcessors

private static List<ResolvedArtifactResult> collectAnnotationProcessors(
ArtifactCollection configuration) {
List<ResolvedArtifactResult> processors = Lists.newArrayList();
for (ResolvedArtifactResult artifact : configuration) {
File file = artifact.getFile();
if (!file.exists()) {
continue;
}
if (file.isDirectory()) {
if (new File(file, PROCESSOR_SERVICES).exists()) {
processors.add(artifact);
}
} else {
try (JarFile jarFile = new JarFile(file)) {
JarEntry entry = jarFile.getJarEntry(PROCESSOR_SERVICES);
if (entry != null) {
processors.add(artifact);
}
} catch (IOException iox) {}
}
}
return processors;
}

在所有的 classpath 中寻找文件 resources/META-INF.services/javax.annotation.processing.Processor 文件,该文件中会将该库所声明的所有注解处理器类全名列出来。如需学习注解处理器的基本使用,可以参考 这篇文章

compileDebugJavaWithJavac

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#addCompileTask

ApplicationTaskManager#createJavacTask

public AndroidTask<? extends JavaCompile> createJavacTask(...) {
AndroidTask<? extends JavaCompile> javacTask =
androidTasks.create(tasks, new JavaCompileConfigAction(scope, outputFolder));
}

具体看下 JavaCompileConfigAction。

public class JavaCompileConfigAction implements TaskConfigAction<AndroidJavaCompile> {
public String getName() {
return scope.getTaskName("compile", "JavaWithJavac");
}
public Class<AndroidJavaCompile> getType() {
return AndroidJavaCompile.class;
}
@Override
public void execute(@NonNull final AndroidJavaCompile javacTask) {
// 设置编译的 SDK 版本
javacTask.compileSdkVersion = globalScope.getExtension().getCompileSdkVersion();
// 将所有的需要编译的 Java 代码路径设置给 AndroidJavaCompile 这个 Task,稍后就会对其进行编译
for (ConfigurableFileTree fileTree: scope.getVariantData().getJavaSources()) {
javacTask.source(fileTree); // 1
}
final boolean keepDefaultBootstrap = scope.keepDefaultBootstrap();
FileCollection classpath = scope.getJavaClasspath(COMPILE_CLASSPATH, CLASSES);
if (keepDefaultBootstrap) {
classpath =
classpath.plus(
project.files(globalScope.getAndroidBuilder().getBootClasspath(false))); // 2
}
javacTask.setClasspath(classpath);
javacTask.setDestinationDir(outputFolder); // 3
Configuration annotationProcessorConfiguration =
scope.getVariantDependencies().getAnnotationProcessorConfiguration();
javacTask.getOptions().setAnnotationProcessorPath(processorPath);
AnnotationProcessorOptions annotationProcessorOptions =
scope.getVariantConfiguration()
.getJavaCompileOptions()
.getAnnotationProcessorOptions();
// 4
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());
}
}
javacTask.getOptions().getCompilerArgs().add("-s");
javacTask.getOptions().getCompilerArgs().add(
scope.getAnnotationProcessorOutputDir().getAbsolutePath());
javacTask.annotationProcessorOutputFolder = scope.getAnnotationProcessorOutputDir();
javacTask.processorListFile = scope.getOutput(ANNOTATION_PROCESSOR_LIST);
javacTask.variantName = scope.getFullVariantName();
}
}
  1. 设置待编译的源码集,对于 assembleDebug 而言包括:

    • src/main/java
    • src/debug/java
    • xxx/build/generated/source/r/debug 稍后 aapt2 链接生成的 R.java 文件路径
    • xxx/build/generated/source/buildConfig/debug 稍后自动生成的 BuildConfig.java 文件路径
    • xxx/build/generated/source/aidl/debug 稍后编译 Aidl 文件生成的 java 文件路径。
    • xxx/build/generated/source/rs/debug 稍后编译 RenderScript 文件生成的 java 文件路径。
  2. 将 android.jar(~/Library/Android/sdk/platforms/android-30/android.jar) 添加进入 Classpath 中。

    注:对于 android.jar 其内部包含 Android 开放的所有 API,但是每个方法内部都是直接抛出 RuntimeException,最终其不会被打入 apk,因为 api 的具体实现早就在系统中了,其作用只是使编译通过罢了。

  3. 设置编译后字节码的输出路径 xxx/build/intermediates/classes/debug。

  4. 如果存在注解处理器那么添加 -processor 标志,如果注解处理器参数不为空那么添加 -A 标志,同时通过 -s 生成文件的目录。

截屏2021-03-09 下午2.42.59

配置阶段结束后,稍后将会执行 AndroidJavaCompile 这个 Task,而其继承于 Gradle 自带得 JavaCompile,内部就是编译代码。

compileDebugNdk

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#createNdkTasks

public void createNdkTasks(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
AndroidTask<NdkCompile> ndkCompileTask =
androidTasks.create(tasks, new NdkCompile.ConfigAction(scope));
}
public class NdkCompile extends NdkTask {
public static class ConfigAction implements TaskConfigAction<NdkCompile> {
public String getName() {
return variantScope.getTaskName("compile", "Ndk");
}
public Class<NdkCompile> getType() {
return NdkCompile.class;
}
}
}

NdkCompile 这个 Task 主要工作为,借助与 ndk 路径下的 ndk-build 工具来编译构建 C 代码,具体的暂时不看,用到再说。

compileDebugSources

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#createAnchorTasks

TaskManager#createCompileAnchorTask

private void createCompileAnchorTask(...) {
final BaseVariantData variantData = scope.getVariantData();
scope.setCompileTask(androidTasks.create(tasks, new TaskConfigAction<Task>() {
public String getName() {
return scope.getTaskName("compile", "Sources");
}
public Class<Task> getType() {
return Task.class;
}
public void execute(@NonNull Task task) {
variantData.compileTask = task;
variantData.compileTask.setGroup(BUILD_GROUP);
}
}));
scope.getAssembleTask().dependsOn(tasks, scope.getCompileTask());
}

这个 Task 只是一个锚点,标志着处理 Shader 开始。

mergeDebugShaders

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createShaderTask

public void createShaderTask(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
AndroidTask<MergeSourceSetFolders> mergeShadersTask = androidTasks.create(tasks,
new MergeSourceSetFolders.MergeShaderSourceFoldersConfigAction(scope));
}
public class MergeSourceSetFolders extends IncrementalTask {
protected void doFullTaskAction() throws IOException {
File destinationDir = getOutputDir();
FileUtils.cleanOutputDir(destinationDir);
List<AssetSet> assetSets = computeAssetSetList();
AssetMerger merger = new AssetMerger();
for (AssetSet assetSet : assetSets) {
assetSet.loadFromFiles(getILogger());
merger.addDataSet(assetSet);
}
MergedAssetWriter writer = new MergedAssetWriter(destinationDir, workerExecutor);
merger.mergeData(writer, false /*doCleanUp*/);
merger.writeBlobTo(getIncrementalFolder(), writer, false);
}
public static class MergeShaderSourceFoldersConfigAction extends ConfigAction {
public MergeShaderSourceFoldersConfigAction(@NonNull VariantScope scope) {
super(scope, null);
}
public String getName() {
return scope.getTaskName("merge", "Shaders");
}
}
}

这个 Task 是用于处理 Vulkan 的,作用是合并以下两个目录文件到 xxx/build/intermediates/shaders/debug 。

  1. xxx/main/shaders
  2. xxx/debug/shaders

Vulkan 在 Android 7.0 以上支持,可以理解为是 OpenGL 的替代品,入门可以参考这篇文章,暂时用不到先忽略。

compileDebugShaders

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createShaderTask

public void createShaderTask(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
AndroidTask<ShaderCompile> shaderCompileTask = androidTasks.create(
tasks, new ShaderCompile.ConfigAction(scope));
shaderCompileTask.dependsOn(tasks, mergeShadersTask);
}
public class ShaderCompile extends BaseTask {
public void createShaderTask(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
AndroidTask<ShaderCompile> shaderCompileTask = androidTasks.create(
tasks, new ShaderCompile.ConfigAction(scope));
shaderCompileTask.dependsOn(tasks, mergeShadersTask);
}
public static class ConfigAction implements TaskConfigAction<ShaderCompile> {
public String getName() {
return scope.getTaskName("compile", "Shaders");
}
public Class<ShaderCompile> getType() {
return ShaderCompile.class;
}
}
}

这个 Task 紧接上一个,作用是将 xxx/build/intermediates/shaders/debug 文件夹下的 vert、tesc、tese、geom、frag、comp 编译成 spv 文件。

generateDebugAssets

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#createAnchorTasks

public void createAnchorTasks(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
scope.setAssetGenTask(androidTasks.create(tasks,
scope.getTaskName("generate", "Assets"),
Task.class,
task -> {
variantData.assetGenTask = task;
}));
}

这个 Task 只是一个锚点,标志着处理 Assets 开始。

mergeDebugAssets

ApplicationTaskManager#createTasksForVariantScope

TaskManager#createMergeAssetsTask

public AndroidTask<MergeSourceSetFolders> createMergeAssetsTask(...) {
AndroidTask<MergeSourceSetFolders> mergeAssetsTask =
androidTasks.create(
tasks, new MergeSourceSetFolders.MergeAssetConfigAction(scope, outputDir));
}
public class MergeSourceSetFolders extends IncrementalTask {
protected void doFullTaskAction() throws IOException {
File destinationDir = getOutputDir();
FileUtils.cleanOutputDir(destinationDir);
List<AssetSet> assetSets = computeAssetSetList();
AssetMerger merger = new AssetMerger();
for (AssetSet assetSet : assetSets) {
assetSet.loadFromFiles(getILogger());
merger.addDataSet(assetSet);
}
MergedAssetWriter writer = new MergedAssetWriter(destinationDir, workerExecutor);
merger.mergeData(writer, false /*doCleanUp*/); // 3
merger.writeBlobTo(getIncrementalFolder(), writer, false);
}
public void execute(@NonNull MergeSourceSetFolders mergeAssetsTask) {
...
mergeAssetsTask.sourceFolderInputs =
TaskInputHelper.bypassFileSupplier(
() -> variantConfig.getSourceFiles(assetDirFunction)); // 1
mergeAssetsTask.shadersOutputDir = project.files(scope.getShadersOutputDir());
mergeAssetsTask.setOutputDir(outputDir); // 2
}
}
  1. 收集应用程序所有的 assets 目录,对于 assembleDebug 其包含以下两个目录。
    • xxx/src/main/assets
    • xxx/src/debug/assets
  2. 设置合并后的目录,对于 assembleDebug 其为 xxx/build/intermediates/assets/debug。
  3. 执行合并操作,其实和上面合并 Shaders 是一样的,就是直接复制过去。

extractTryWithResourcesSupportJarDebug

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#addCompileTask

ApplicationTaskManager#createPostCompilationTasks

TaskManager#maybeCreateDesugarTask

private void maybeCreateDesugarTask(...) {
String taskName = variantScope.getTaskName(TASK_NAME);
AndroidTask<ExtractTryWithResourcesSupportJar> extractTryWithResources =
androidTasks.create(
tasks,
new ExtractTryWithResourcesSupportJar.ConfigAction(...));
}
public class ExtractTryWithResourcesSupportJar extends DefaultAndroidTask {
@TaskAction
public void run() throws IOException {
try (InputStream in =
DesugarProcessBuilder.class
.getClassLoader()
.getResourceAsStream("libthrowable_extension.jar")) {
FileUtils.cleanOutputDir(outputLocation.getSingleFile().getParentFile());
Files.copy(in, outputLocation.getSingleFile().toPath());
}
}
public static class ConfigAction
implements TaskConfigAction<ExtractTryWithResourcesSupportJar> {
public Class<ExtractTryWithResourcesSupportJar> getType() {
return ExtractTryWithResourcesSupportJar.class;
}
public void execute(@NonNull ExtractTryWithResourcesSupportJar task) {
task.outputLocation = outputLocation;
task.setVariantName(variantName);
}
}
}

这个 Task 主要是将 libthrowable_extension.jar 拷贝到 xxx/build/intermediates/processing-tools/runtime-deps/debug/desugar_try_with_resources.jar ,该 jar 最终会被打包到 apk 中去。通过 jadx 反编译发现其内部只有一个 ThrowableExtension ,好像是用于支持 try-with-resources 语法,暂时不是很明确该类作用。

截屏2021-03-10 下午2.09.51

transformClassesWithStackFramesFixerForDebug

这是个 Transform Task,对应的实现类为 FixStackFramesTransform。注册时机为:

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#addCompileTask

ApplicationTaskManager#createPostCompilationTasks

TaskManager#maybeCreateDesugarTask

private void maybeCreateDesugarTask(...) {
FixStackFramesTransform fixFrames = new FixStackFramesTransform(...);
transformManager.addTransform(tasks, variantScope, fixFrames);
}

大致看了看 FixStackFramesTransform 的逻辑,内部会遍历所有依赖的 jar 包,然后使用 ASM 来重新计算每个类的栈帧信息。目的是当运行 Desugar 时,确保栈帧信息在类文件中是有效的,防止字节码在 JVM 1.7 及以上不合法。

猜想:目的是为了处理依赖库可能存在低版本的 Java 字节码??

transformClassesWithDesugarForDebug

这是个 Transform Task,对应的实现类为 DesugarTransform。注册时机为:

ApplicationTaskManager#createTasksForVariantScope

ApplicationTaskManager#addCompileTask

ApplicationTaskManager#createPostCompilationTasks

TaskManager#maybeCreateDesugarTask

private void maybeCreateDesugarTask(...) {
DesugarTransform desugarTransform = new DesugarTransform(...);
transformManager.addTransform(tasks, variantScope, desugarTransform);
}

首先将 desugar_deploy.jar (保存在 builder.jar 中,和 libthrowable_extension.jar 放在一起) 拷贝到临时目录,当虚拟机退出时会删除该文件,监听方法为:Runtime.getRuntime().addShutdownHook(thread: Thread),通过 jadx 反编译发现其内部包含有 ASM 代码。

截屏2021-03-11 上午9.38.49

DesugarTransform 内部处理文件都是异步执行的(借助于 WaitableExecutor),这样可以提高效率。

:app:transformClassesWithStackFramesFixerForDebug SKIPPED
:app:transformClassesWithDesugarForDebug SKIPPED
:app:transformClassesWithDexBuilderForDebug SKIPPED
:app:transformDexArchiveWithExternalLibsDexMergerForDebug SKIPPED
:app:transformDexArchiveWithDexMergerForDebug SKIPPED
:app:mergeDebugJniLibFolders SKIPPED
:app:transformNativeLibsWithMergeJniLibsForDebug SKIPPED
:app:processDebugJavaRes SKIPPED
:app:transformResourcesWithMergeJavaResForDebug SKIPPED
:app:validateSigningDebug SKIPPED
:app:packageDebug SKIPPED
:app:assembleDebug SKIPPED
0%