background

Gradle is an open source build automation tool designed to be flexible for different platforms. It is realized by writing different plug-ins for different platforms, and its own architecture is not dealt with. It maintains good scalability and flexibility. For android platform, it uses the Android Gradle Plugin tool.

The preparatory work

To analyze the entire process, we first need to prepare the corresponding source code. There are two ways to do this. One is to download gradle source code directly, which contains AGP. Today we introduce the second option, which is to download only AGP.

AGP source code download

  1. Create a new project with Android Studio and delete all files in your app except build.gradle
  2. Modify build.gradle in app, remove other configurations and change to the following
plugins {
    id 'java'
}
sourceCompatibility = 1.8

dependencies {
    implementation gradleApi(a)Implementation "com. Android. Tools. Build: gradle: 3.0.0"}Copy the code
  1. Modify build.gradle configuration of RootProject and remove classPath configuration
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
     / / the classpath "com. Android. Tools. Build: gradle: 7.0.3." "

    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}
Copy the code

Here we can find the source code of AGP in External Libraries. Here we use the 3.0.0 source code. The latest changes are quite large and the process is quite complicated. Because we are going through the process, and the development project is not going to use the latest plug-ins right now. Therefore, the 3.0.0 version is analyzed here.



Here we can see several plugins, among which there are two AppPlugin and LibraryPlguin. Since AppPlugin is used in the directory of our app development, we will analyze the overall construction process of APK as follows.

APK build

Apk build source flow chart





Through the sorting of packaging process, it can be roughly divided into three simple

  • configProject
  • configExtension
  • createTasks

The first two phases are mainly to complete the initial configuration. The focus is on createTasks, which tasks are created and what each task does. We will focus on this aspect below, and the other two stages will not be further studied in this paper.

Apk packaging task analysis

createTasks

The method call createTasksBeforeEvaluate, before AppPlugin execution, build relation with create some tasks, before must be created

createTasksBeforeEvaluate

public void createTasksBeforeEvaluate(@NonNull TaskFactory tasks) {

    androidTasks.create(tasks, UNINSTALL_ALL, uninstallAllTask -> {
            uninstallAllTask.setDescription("Uninstall all applications.");
            uninstallAllTask.setGroup(INSTALL_GROUP);
        });
    androidTasks.create(tasks, DEVICE_CHECK, deviceCheckTask -> {
            deviceCheckTask.setDescription(
                    "Runs all device checks using Device Providers and Test Servers.");
            deviceCheckTask.setGroup(JavaBasePlugin.VERIFICATION_GROUP);
        });
    androidTasks.create(tasks, CONNECTED_CHECK, connectedCheckTask -> {
            connectedCheckTask.setDescription(
                    "Runs all device checks on currently connected devices.");
            connectedCheckTask.setGroup(JavaBasePlugin.VERIFICATION_GROUP);
        });
    // Create MAIN_PREBUILD task
    androidTasks.create(tasks, MAIN_PREBUILD, task -> {});
    // Create a task to extract obfuscated filesAndroidTask<ExtractProguardFiles> extractProguardFiles = androidTasks.create( tasks, EXTRACT_PROGUARD_FILES, ExtractProguardFiles.class, task -> {}); .LinkTask is configured last, but its creation is here, before subsequent Anchor Task use
createGlobalLintTask(tasks);    

}
Copy the code

createAndroidTasks

Next, after Project completes the configuration, call createAndroidTasks to start creating Android build-related tasks

project.afterEvaluate(
                project ->
                        threadRecorder.record(
                                ExecutionType.BASE_PLUGIN_CREATE_ANDROID_TASKS,
                                project.getPath(),
                                null,
                                () -> createAndroidTasks(false)));
Copy the code

Because Android supports configuring multi-channel version builds, the specific build tasks are done through VariantManager, which is created in the second phase.

 threadRecorder.record(
                ExecutionType.VARIANT_MANAGER_CREATE_ANDROID_TASKS,
                project.getPath(),
                null,
                () -> {
                    variantManager.createAndroidTasks();
                    ApiObjectFactory apiObjectFactory =
                            new ApiObjectFactory(
                                    androidBuilder,
                                    extension,
                                    variantFactory,
                                    instantiator,
                                    project.getObjects());
                    for(VariantScope variantScope : variantManager.getVariantScopes()) { BaseVariantData variantData = variantScope.getVariantData(); apiObjectFactory.create(variantData); }});Copy the code

So the actual AndroidTasks are created in VariantManager

VariantManager creates tasks

PopulateVariantDataList processes channel data

Let’s first look at how the configuration works in a card project, so it’s easier to understand how it works. Okay

 flavorDimensions "api"."mode"."corp"
     
 productFlavors {
   Compainion {
     dimension:"mode"
   }
     
   apiVersion {
   dimension:"api"
   }  
     
   corp {
     dimension:"corp"}}Copy the code
 /** * Create all variants. */
 public void populateVariantDataList(a) {
   // Get channel data
   List<String> flavorDimensionList = extension.getFlavorDimensionList();
   // determine whether the flavorDimensionList exists, or if there is only one
   // If there is only one channel, set it to all channels.else if (flavorDimensionList.size() == 1) {
   // if there's only one dimension, auto-assign the dimension to all the flavors.
                String dimensionName = flavorDimensionList.get(0);
 for (ProductFlavorData<CoreProductFlavor> flavorData :         productFlavors.values()) {
CoreProductFlavor flavor = flavorData.getProductFlavor();
if (flavor.getDimension() == null && flavor instanceofDefaultProductFlavor) { ((DefaultProductFlavor) flavor).setDimension(dimensionName); }}}// Create a traverser to traverse all channels
  Iterable<CoreProductFlavor> flavorDsl =
   Iterables.transform(productFlavors.values(),
   ProductFlavorData::getProductFlavor);
 // Create all channels according to the dimen combination
 List<ProductFlavorCombo<CoreProductFlavor>> flavorComboList =
                    ProductFlavorCombo.createCombinations(
                            flavorDimensionList,
                            flavorDsl);
   // Create channel data for the corresponding channel
  for (ProductFlavorCombo<CoreProductFlavor>  flavorCombo : flavorComboList) {
                //noinspection uncheckedcreateVariantDataForProductFlavors( (List<ProductFlavor>) (List) flavorCombo.getFlavorList()); }}Copy the code

createTasksForVariantData

This method is called to create a corresponding build task for each variant

for (final VariantScope variantScope : variantScopes) {
            recorder.record(
                    ExecutionType.VARIANT_MANAGER_CREATE_TASKS_FOR_VARIANT,
                    project.getPath(),
                    variantScope.getFullVariantName(),
                    () -> createTasksForVariantData(tasks, variantScope));
        }

Copy the code

This approach creates tasks for specific variants, which can be divided into two main parts.

Create the Assemble task corresponding to BuildType
Create an assmeble task
if (buildTypeData.getAssembleTask() == null) {        buildTypeData.setAssembleTask(taskManager.createAssembleTask(tasks, buildTypeData));
 }
/ / create different BuildType assembe task: assembleDebug, assembleRelease
tasks.named("assemble".new Action<Task>() {
            @Override
            public void execute(Task task) {
                assertbuildTypeData.getAssembleTask() ! =null; task.dependsOn(buildTypeData.getAssembleTask().getName()); }});// Create the Assemble task for each variant
createAssembleTaskForVariantData(tasks, variantData)

Copy the code

The second part is called taskManager createTasksForVariantScope began to build real task

.else {
taskManager.createTasksForVariantScope(tasks, variantScope);
}

Copy the code

ApplicationTaskManager Creates tasks

After processing the multi-channel data, creating a task for a specific channel goes into ApplicationTaskManager

createAnchorTasks

It creates the preBuild task and the corresponding resource anchor task

  • generateSource
  • generateResouce
  • generateAssets
  • compileSource

Its tasks are empty tasks, mainly to establish dependencies

public void createAnchorTasks(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
// Create a preBuild task and rely on the original extract obfuscation task
// Specify the internal code of its method
createPreBuildTasks(tasks, scope);

scope.setSourceGenTask(androidTasks.create(
  tasks,scope.getTaskName("generate"."Sources"),
       Task.class,
       task -> {
             variantData.sourceGenTask = task;
             task.dependsOn(PrepareLintJar.NAME);
  }));
 
 // Res resource generation task
 scope.setResourceGenTask(androidTasks.create(   
 tasks,scope.getTaskName("generate"."Resources"),
       Task.class,
       task -> {
             variantData.resourceGenTask = task;

  }));
    
scope.setAssetGenTask(androidTasks.create(tasks,
   scope.getTaskName("generate"."Assets"),
       Task.class,
       task -> {
          variantData.assetGenTask = task;
    })); 
    
 // Create the compile Source task and assemble depends on it
 createCompileAnchorTask(...)   
} 

private void createPreBuildTasks(@NonNull TaskFactory tasks, @NonNull VariantScope scope) {
        scope.setPreBuildTask(createVariantPreBuildTask(tasks, scope));

        scope.getPreBuildTask().dependsOn(tasks, MAIN_PREBUILD);

        if(runJavaCodeShrinker(scope)) { scope.getPreBuildTask().dependsOn(tasks, EXTRACT_PROGUARD_FILES); }}/ / call createDefalutPreBuildTask createVariantPreBuildTask eventually
protected AndroidTask<? extends DefaultTask> createDefaultPreBuildTask(
            @NonNull TaskFactory tasks, @NonNull VariantScope scope) {
  return getAndroidTasks()
      .create(
           tasks,
           scope.getTaskName("pre"."Build"),
           task -> {
            scope.getVariantData().preBuildTask = task;
      });
    }



Copy the code

createCheckManifestTask

It detects the presence of MainfestTask and establishes a dependency with preBuild

CheckMainfest task

public class CheckManifest extends DefaultAndroidTask {...@TaskAction
    void check(a) {
        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())); }}... }Copy the code

createDependencyStreams

It basically adds various stream operations, including dataBing, to the TransformManager.

createApplicationIdWriterTask

The main task is to deal with the applicationID directory, which applications must have

private void createApplicationIdWriterTask(
            @NonNull TaskFactory tasks, @NonNull VariantScope variantScope) {
// Create a directory under the corresponding application ID
 File applicationIdOutputDirectory =
                FileUtils.join(
                        globalScope.getIntermediatesDir(),
                        "applicationId"./ / create the corresponding applicationID writing task variantScope. GetVariantConfiguration () getDirName ());

        AndroidTask<ApplicationIdWriterTask> writeTask =
                androidTasks.create(
                        tasks,
                        new ApplicationIdWriterTask.ConfigAction(
                                variantScope, applicationIdOutputDirectory));

        variantScope.addTaskOutput(
                TaskOutputHolder.TaskOutputType.METADATA_APP_ID_DECLARATION,
                ApplicationId.getOutputFile(applicationIdOutputDirectory),
                writeTask.getName());
    }
Copy the code

createMergeApkManifestsTask

It is primarily the manifest.xml document that merges applications and dependencies

 recorder.record(
   ExecutionType.APP_TASK_MANAGER_CREATE_MERGE_MANIFEST_TASK,
   project.getPath(),
   variantScope.getFullVariantName(),
() -> createMergeApkManifestsTask(tasks, variantScope));
Copy the code

CompatibleScreensManifest

The Manifest device is compatible with the class corresponding to the task, and its generareAll method handles operations such as merges, internally calling Generate

@TaskAction
    public void generateAll(a) throws IOException {
        // process all outputs.
        outputScope.parallelForEach(
                VariantScope.TaskOutputType.COMPATIBLE_SCREEN_MANIFEST, this::generate);
        // now write the metadata file.
        outputScope.save(
                ImmutableList.of(VariantScope.TaskOutputType.COMPATIBLE_SCREEN_MANIFEST),
                outputFolder);
    }
Copy the code

MergeManifests

It merges the Mainifest task in its doFullTaskAction to do the processing

 @Override
 protected void doFullTaskAction(a) throws IOException {
    
 if(packageManifest ! =null && !packageManifest.isEmpty()) {
            packageOverride =
                         ApplicationId.load(packageManifest.getSingleFile()).getApplicationId();
} else{ packageOverride = getPackageOverride(); }...for(ApkData apkData : splitsToGenerate) { File manifestOutputFile = FileUtils.join(getManifestOutputDirectory(), apkData.getDirName(), SdkConstants.ANDROID_MANIFEST_XML); . XmlDocument mergedXmlDocument = mergingReport.getMergedXmlDocument(MergingReport.MergedManifestKind.MERGED); . outputScope.save( ImmutableList.of(VariantScope.TaskOutputType.MERGED_MANIFESTS), getManifestOutputDirectory()); }... }Copy the code

createGenerateResValuesTask

It basically creates a task that generates resValues

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

I won’t expand the details here, but the corresponding class is GenerateResValues

createRenderscriptTask

It creates the task of compiling the render script file


public void createRenderscriptTask(
            @NonNull TaskFactory tasks,
            @NonNull VariantScope scope) {
    // Create a task
        scope.setRenderscriptCompileTask(
                androidTasks.create(tasks, new RenderscriptCompile.ConfigAction(scope)));

        GradleVariantConfiguration config = scope.getVariantConfiguration();
// Create different dependencies depending on whether it is a test or not
 if (config.getType().isForTesting()) {
            scope.getRenderscriptCompileTask().dependsOn(tasks, scope.getManifestProcessorTask());
        } else {
            scope.getRenderscriptCompileTask().dependsOn(tasks, scope.getPreBuildTask());
        }
scope.getResourceGenTask().dependsOn(tasks, scope.getRenderscriptCompileTask());
        // only put this dependency if rs will generate Java code
if(! config.getRenderscriptNdkModeEnabled()) { scope.getSourceGenTask().dependsOn(tasks, scope.getRenderscriptCompileTask());  }Copy the code

createMergeResourcesTask

It creates a task for merging resources

recorder.record(
                ExecutionType.APP_TASK_MANAGER_CREATE_MERGE_RESOURCES_TASK,
                project.getPath(),
                variantScope.getFullVariantName(),
                (Recorder.VoidBlock) () -> createMergeResourcesTask(tasks, variantScope, true))
Copy the code

createMergeAssetsTask

It creates tasks for merging Assets

recorder.record(
 ExecutionType.APP_TASK_MANAGER_CREATE_MERGE_ASSETS_TASK,
project.getPath(),
variantScope.getFullVariantName(),
() -> createMergeAssetsTask(tasks, variantScope, null));
Copy the code

createBuildConfigTask

This creates the classes that we apply to generate the BuildConfig.java file

recorder.record(
ExecutionType.APP_TASK_MANAGER_CREATE_BUILD_CONFIG_TASK,
  project.getPath(),
     variantScope.getFullVariantName(),
     () -> createBuildConfigTask(tasks, variantScope));                                         
Copy the code

createApkProcessResTask

It processes the corresponding resource files through AAPT. This includes the generation of R files. Because its process is relatively complex, we will list a separate chapter for analysis later.

createProcessJavaResTask

It handles Java resource files. I won’t go into the details. There are two main steps to go

  • Through ProcessJavaResConfigAction synchronization configuration, its synthesize all corresponding resource files into one file
  • Then use the MergeJavaResourcesTransform through our PackageingOptions create a merge

createAidlTask

It is mainly used to handle the use of aiDL files that we communicate across processes and convert them into corresponding classes

recorder.record(
                ExecutionType.APP_TASK_MANAGER_CREATE_AIDL_TASK,
                project.getPath(),
                variantScope.getFullVariantName(),
                () -> createAidlTask(tasks, variantScope));
Copy the code

Its internal will set the corresponding compilation and code generation tasks, and it depends on the preBuild task, so it is carried out at the same time with the resource processing, then we sort out the general APK packaging flow chart, also understand its steps.


  public AndroidTask<AidlCompile> createAidlTask(@NonNull TaskFactory tasks, @NonNull 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;
    }
Copy the code

The following are some Ndk development related tasks, here is usually not used for development, I will not list

createMergeJniLibFoldersTasks

It is primarily used to process resources for jNI folders

 recorder.record(
                ExecutionType.APP_TASK_MANAGER_CREATE_MERGE_JNILIBS_FOLDERS_TASK,
                project.getPath(),
                variantScope.getFullVariantName(),
                () -> createMergeJniLibFoldersTasks(tasks, variantScope));
Copy the code

addCompileTask

It creates and compiles Java, Kotlin files, and then turns them into dex-related work

private void addCompileTask(@NonNull TaskFactory tasks, @NonNull VariantScope variantScope) {
// Create javacTask, where javac is Java, compile.java
// The program to class
AndroidTask<? extends JavaCompile> javacTask = createJavacTask(tasks, variantScope);
// check support for java8 syntax above
 VariantScope.Java8LangSupport java8LangSupport = variantScope.getJava8LangSupportType();
        if (java8LangSupport == VariantScope.Java8LangSupport.INVALID) {
            return;
        }    
// Add Class stream operations generated by Javac compilation
addJavacClassesStream(...)
// Generate a compilation task
setJavaCompilerTask(javacTask, tasks, variantScope);
// After compiling, convert to dex
createPostCompilationTasks(tasks, variantScope);    
}
Copy the code

addJavaClassesStream

It creates a stream for JAVAC output to facilitate bytecode hook processing before and after compilation

public void addJavacClassesStream(VariantScope scope) {

scope.getTransformManager()
                .addStream(
                        OriginalStream.builder(project, "javac-output")
                                .addContentTypes(
                                        DefaultContentType.CLASSES, DefaultContentType.RESOURCES)
                                .addScope(Scope.PROJECT)
                                .setFileCollection(scope.getOutput(JAVAC))
                                .build());    
scope.getTransformManager()
                .addStream(
                        OriginalStream.builder(project, "pre-javac-generated-bytecode")
                                .addContentTypes(
                                        DefaultContentType.CLASSES, DefaultContentType.RESOURCES)
                                .addScope(Scope.PROJECT)
                                .setFileCollection(
                                        scope.getVariantData().getAllPreJavacGeneratedBytecode())
                                .build());
scope.getTransformManager()
                .addStream(
                        OriginalStream.builder(project, "post-javac-generated-bytecode")
                                .addContentTypes(
                                        DefaultContentType.CLASSES, DefaultContentType.RESOURCES)
                                .addScope(Scope.PROJECT)
                                .setFileCollection(
                                        scope.getVariantData().getAllPostJavacGeneratedBytecode())
                                .build());
}
Copy the code

setJavaCompileTask

The main thing is to rely on the JavacTask we created to compile the anchor task we created earlier

 public static void setJavaCompilerTask(
            @NonNull AndroidTask<? extends Task> javaCompilerTask,
            @NonNull TaskFactory tasks,
            @NonNull VariantScope scope) {
        scope.getCompileTask().dependsOn(tasks, javaCompilerTask);
    }
Copy the code

createPostCompilationTasks

This task is mainly used to create the task of converting a. Class file into a dex file. The specific process is added to the corresponding code comments

public void createPostCompilationTasks(...).{ TransformManager transformManager = variantScope.getTransformManager(); .// Get all external transforms
List<Transform> customTransforms = extension.getTransforms();
        List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();
// Loop through transrom to add to transformManager
for (int i = 0, count = customTransforms.size(); i < count; i++) {
   Transform transform = customTransforms.get(i);
  List<Object> deps = customTransformsDependencies.get(i);
  transformManager.addTransform(tasks, variantScope, transform)
   .ifPresent(t -> {
            if(! deps.isEmpty()) { t.dependsOn(tasks, deps); }if(transform.getScopes().isEmpty()) { variantScope.getAssembleTask().dependsOn(tasks, t); }}); }// Record the performance of the processed transform
 for (String jar : getAdvancedProfilingTransforms(projectOptions)) {
            if(variantScope.getVariantConfiguration().getBuildType().isDebuggable() && variantData.getType().equals(VariantType.DEFAULT) && jar ! =null) {
                transformManager.addTransform(tasks, variantScope, newCustomClassTransform(jar)); }}...// Obtain the Dex type
DexingType dexingType = variantScope.getDexingType();
// If the device supports Native multi-dex, switch to native
if (dexingType == DexingType.LEGACY_MULTIDEX) {
            if (variantScope.getVariantConfiguration().isMultiDexEnabled()
                    && variantScope
                                    .getVariantConfiguration()
                                    .getMinSdkVersionWithTargetDeviceApi()
                                    .getFeatureLevel()
                            >= 21) { dexingType = DexingType.NATIVE_MULTIDEX; }}// Create the multiDex task store array
 Optional<AndroidTask<TransformTask>> multiDexClassListTask;
 // Create a folder that handles all input and jars into a single JAR, mainly merging third-party library class files. JarMergingTransform jarMergingTransform =newJarMergingTransform(TransformManager.SCOPE_FULL_PROJECT); .// Create the transForm of MainDex or MultiDex as required
 if (usingIncrementalDexing(variantScope)) {
  multiDexTransform = new MainDexListTransform(
         variantScope,
        extension.getDexOptions());
  } else {
 multiDexTransform = new MultiDexTransform(variantScope, extension.getDexOptions());
 }
 / / add multiDexTransform
 multiDexClassListTask =
 transformManager.addTransform(tasks, variantScope, multiDexTransform);
            multiDexClassListTask.ifPresent(variantScope::addColdSwapBuildTask);    
 // Create a multiDex task
 if (usingIncrementalDexing(variantScope)) {
            createNewDexTasks(tasks, variantScope, multiDexClassListTask.orElse(null), dexingType);
        } else {
            createDexTasks(tasks, variantScope, multiDexClassListTask.orElse(null), dexingType); }}Copy the code

createDexTasks

This method is used to generate the final Dex file without analyzing the details

createSplitTasks

This task is mainly used to create tasks that output different apKs for different architectures, which is handled when we need to output different APKs for different architectures. It has two parts to it,

  • Create the corresponding resource,
  • Create an abi
public void createSplitTasks(@NonNull TaskFactory tasks, @NonNull VariantScope variantScope) {
        PackagingScope packagingScope = new DefaultGradlePackagingScope(variantScope);
    createSplitResourcesTasks(tasks, variantScope, packagingScope);
    createSplitAbiTasks(tasks, variantScope, packagingScope);
 }
Copy the code

createPackageingTask

The task is to create the final APK. According to the configuration, if the APK is configured with signatures, zipaligin can be performed.

public void createPackingTask(...).{
// Corresponding channel information
ApkVariantData variantData = (ApkVariantData) variantScope.getVariantData();
// Whether to sign
boolean signedApk = variantData.isSigned();
// Whether API >23 can support InstantRunPatch, that is, APK is not fully updated during debugging
InstantRunPatchingPolicy patchingPolicy =
                variantScope.getInstantRunBuildContext().getPatchingPolicy();
// Get the type of Mainfest file, InstanRun, or Merge
VariantScope.TaskOutputType manifestType =
                variantScope.getInstantRunBuildContext().isInInstantRunMode()
                        ? INSTANT_RUN_MERGED_MANIFESTS
                        : MERGED_MANIFESTS;
// To determine whether the full apK or split schema APK, set a different build directory
File outputDirectory =
splitsArePossible ? variantScope.getFullApkPackagesOutputDirectory(): finalApkLocation; 
// Create the task PackageApplication
AndroidTask<PackageApplication> packageApp =androidTasks.create(tasks,
   newPackageApplication.StandardConfigAction(...) );// Whether InstantRunMode creates different resource processing tasks
if(InstanceRunMode){
// Check if it is MULTI_APK_SEPARATE_RESOURCES
/ / is
packageInstantRunResources = androidTasks.create(tasks,
 newInstantRunResourcesApkBuilder.ConfigAction(...) );/ / no
packageInstantRunResources = androidTasks.create(tasks,
 newPackageApplication.InstantRunResourcesConfigAction(...) ); }// The packageApp task depends on the task created above
packageApp.dependsOn(tasks, packageInstantRunResources);    
// Package is signature-dependent, that is, it is signed before apK is synthesized
CoreSigningConfig signingConfig = packagingScope.getSigningConfig();
//noinspection VariableNotUsedInsideIf - we use the whole packaging scope below.
  if(signingConfig ! =null) {
     packageApp.dependsOn(tasks, getValidateSigningTask(tasks, packagingScope));
  }
// Rely on resource compression to cull tasks
packageApp.optionalDependsOn(tasks,
   variantScope.getJavacTask(),
   variantData.packageSplitResourcesTask,
   variantData.packageSplitAbiTask);
// After the configuration is complete, set packageApp to the corresponding task of the channel
 variantScope.setPackageApplicationTask(packageApp);
Assemble task dependency with packageAPPvariantScope.getAssembleTask().dependsOn(tasks, packageApp.getName()); .// Create real tasks in the Install and UnInstall anchor tasks created above
 if (signedApk) {
   AndroidTask<InstallVariantTask> installTask = androidTasks.create(
   tasks, new InstallVariantTask.ConfigAction(variantScope));
  installTask.dependsOn(tasks, variantScope.getAssembleTask());
  }   
  final AndroidTask<UninstallTask> uninstallTask = androidTasks.create(
                tasks, new UninstallTask.ConfigAction(variantScope));

  tasks.named(UNINSTALL_ALL, uninstallAll -> uninstallAll.dependsOn(uninstallTask.getName()));  
}
Copy the code

Above we have reviewed the general flow of createPackingApp. Next we will look at the internal logic of the packageApp task.

PackageApplication

It is mainly responsible for packaging Apk

StandardConfigAction

Responsible for configuration tasks, performing standard packaging, and exporting the included files to APK

InstantRunResourcesConfigAction

Only the files under resource and assets directories are exported to the APK

In the PackageApplication class, instead of looking at the specific methods, we look at the parent class which has implementations of doFullTaskAction and doIncrementalTaskAction. This is an implementation of the @TaskAction method that the Task calls because they are incrementalTasks

    @TaskAction
    void taskAction(IncrementalTaskInputs inputs) throws Exception {
        if(! isIncremental() || ! inputs.isIncremental()) { getProject().getLogger().info("Unable do incremental execution: full task run");
            doFullTaskAction();
            return;
        }

        doIncrementalTaskAction(getChangedInputs(inputs));
    }
Copy the code

doFullTaskAction

Its main job is to deal with resources

  1. Obtain the merged Resoure file
  • It creates a collection of resources by BuildOutputs to load the corresponding files.
  • The parallel processing is then traversed by splitFullAction

The thread pool used internally

@Override
    protected void doFullTaskAction(a) throws IOException {

        Collection<BuildOutput> mergedResources =
                BuildOutputs.load(getTaskInputType(), resourceFiles);
        outputScope.parallelForEachOutput(
                mergedResources, getTaskInputType(), getTaskOutputType(), this::splitFullAction);
        outputScope.save(getTaskOutputType(), outputDirectory);
    }
Copy the code

DoIncrementalTaskAction is similar to doFullTaskAction in that both end up calling doTask. The difference between doFullTaskAction is that it incrementally processes the file, while doFullTaskAction deletes the previous file. Let’s look at splitFullAction for doFullTaskAction

splitFullAction
publis File splitFullAction(...).{
 if (incrementalDirForSplit.exists()) {
            FileUtils.deleteDirectoryContents(incrementalDirForSplit);
 } else{ FileUtils.mkdirs(incrementalDirForSplit); }... File outputFile = getOutputFiles().get(apkData); FileUtils.deleteIfExists(outputFile);// Update the file to be packaged to APKImmutableMap<RelativeFile, FileStatus> updatedDex = IncrementalRelativeFileSets.fromZipsAndDirectories(getDexFolders());  ImmutableMap<RelativeFile, FileStatus> updatedJavaResources = getJavaResourcesChanges(); ImmutableMap<RelativeFile, FileStatus> updatedAssets = IncrementalRelativeFileSets.fromZipsAndDirectories(assets.getFiles()); ImmutableMap<RelativeFile, FileStatus> updatedAndroidResources = IncrementalRelativeFileSets.fromZipsAndDirectories(androidResources); ImmutableMap<RelativeFile, FileStatus> updatedJniResources = IncrementalRelativeFileSets.fromZipsAndDirectories(getJniFolders()); Collection<BuildOutput> manifestOutputs = BuildOutputs.load(manifestType, manifests);/ / doTask processingdoTask(apkData,...) ; . }Copy the code

doTask

It packages the files processed above into apK

private void doTask(...).{
// Java resources merged into APK, not classes, such as MATAINfoImmutableMap.Builder<RelativeFile, FileStatus> javaResourcesForApk = ImmutableMap.builder(); Will handle changedJavaResources join javaResourcesForApk. PutAll (changedJavaResources); .// ApK dex resources to be packaged
final ImmutableMap<RelativeFile, FileStatus> dexFilesToPackage = changedDex;
// Find the corresponding Manifest file
BuildOutput manifestForSplit =
                OutputScope.getOutput(manifestOutputs, manifestType, apkData);
// Create the corresponding packager
try(IncrementalPackager packager =
                new IncrementalPackagerBuilder()
   
   ...
   build() 
   )
/* * Save the file used for packaging above in the cache */
Stream.concat(dexFilesToPackage.keySet().stream(),
       Stream.concat(
              changedJavaResources.keySet().stream(),
                    Stream.concat(
                           changedAndroidResources.keySet().stream(),
                           changedNLibs.keySet().stream())))
                .map(RelativeFile::getBase)
                .filter(File::isFile)
                .distinct()
                .forEach(
                        (File f) -> {
                            try {
                                cacheByPath.add(f);
                            } catch (IOException e) {
                                throw newIOExceptionWrapper(e); }}); }Copy the code

IncrementalPackager

The IncrementalPackager created above does the final operation of merging APK with resources from aAPT output files, Java resource files, dex files, and JNI files and internally creates APK files through ApkCreator

// Creates or updates APKs based on provided entries.
public interface ApkCreator extends Closeable {... }public IncrementalPackager(@NonNull ApkCreatorFactory.CreationData creationData,
            @NonNull File intermediateDir, @NonNull ApkCreatorFactory factory,
            @NonNull Set<String> acceptedAbis, boolean jniDebugMode)
            throws PackagerException, IOException {
        if(! intermediateDir.isDirectory()) {throw new IllegalArgumentException(
                    ! "" intermediateDir.isDirectory(): " + intermediateDir);
        }
        checkOutputFile(creationData.getApkPath());
        // Creating IncrementalPackager creates ApkCreator
        mApkCreator = factory.make(creationData);
        mDexRenamer = new DexIncrementalRenameManager(intermediateDir);
        mAbiPredicate = new NativeLibraryAbiPredicate(acceptedAbis, jniDebugMode);
    }
Copy the code

The corresponding implementation of Apk is ApkZFileCreateor, using ZipOption to generate Apk, in its constructor.

 ApkZFileCreator(
            @Nonnull ApkCreatorFactory.CreationData creationData,
            @Nonnull ZFileOptions options)
            throws IOException {

        switch (creationData.getNativeLibrariesPackagingMode()) {
            case COMPRESSED:
                noCompressPredicate = creationData.getNoCompressPredicate();
                break;
            case UNCOMPRESSED_AND_ALIGNED:
                noCompressPredicate =
                        creationData.getNoCompressPredicate().or(
                                name -> name.endsWith(NATIVE_LIBRARIES_SUFFIX));
                options.setAlignmentRule(
                        AlignmentRules.compose(SO_RULE, options.getAlignmentRule()));
                break;
            default:
                throw new AssertionError();
        }

        zip = ZFiles.apk(
                creationData.getApkPath(),
                options,
                creationData.getPrivateKey(),
                creationData.getCertificate(),
                creationData.isV1SigningEnabled(),
                creationData.isV2SigningEnabled(),
                creationData.getBuiltBy(),
                creationData.getCreatedBy(),
                creationData.getMinSdkVersion());
        closed = false;
    }
Copy the code

ZFile Generates Apk files

It synthesizes apK from the given file

public static ZFile apk(...).{ ZFile zfile = apk(f, options); .return zfile

}
 /**
     * Creates a new zip file configured as an apk, based on a given file.
     *
     * @param f the file, if this path does not represent an existing path, will create a
     * {@link ZFile} based on an non-existing path (a zip will be created when
     * {@link ZFile#close()} is invoked)
     * @param options the options to create the {@link ZFile}
     * @return the zip file
     * @throws IOException failed to create the zip file
     */
public static ZFile apk(@Nonnull File f, @Nonnull ZFileOptions options) throws IOException {
        options.setAlignmentRule(
                AlignmentRules.compose(options.getAlignmentRule(), APK_DEFAULT_RULE));
        return new ZFile(f, options);
}
Copy the code

Here we have analyzed the creation of the task that creates the Apk, that is, the general flow of how it synthesizes the Apk. Then we compared with the apK flow chart provided by the official, it was very clear and intuitive to understand its process

Apk packaging process

createLinkTasks

The simple thing is to update the LinkTask anchor we created earlier

 public void createLintTasks(TaskFactory tasks, final VariantScope scope) {
        if(! isLintVariant(scope)) {return;
        }

        androidTasks.create(tasks, new LintPerVariantTask.ConfigAction(scope));
    }
Copy the code

Aapt resource file generation

createApkProcessResTask

Its generation of R files and other file processing under RES is initiated by it. Internally, the createProcessResTask method is called to create the corresponding file

 public void createApkProcessResTask(
            @NonNull TaskFactory tasks,
            @NonNull VariantScope scope) {

        createProcessResTask(
                tasks,
                scope,
                () ->
                 // Create the corresponding file: intermediates/symbols...
                        new File(
                                globalScope.getIntermediatesDir(),
                                "symbols/"
                                        + scope.getVariantData()
                                                .getVariantConfiguration()
                                                .getDirName()),
                scope.getProcessResourcePackageOutputDirectory(),
                MergeType.MERGE,
                scope.getGlobalScope().getProjectBaseName());
    }

Copy the code

Open the corresponding build and find a file that starts with symbo

createProcessResTask

And inside that will synthesize the corresponding file that we have above

 public AndroidTask<ProcessAndroidResources> createProcessResTask(...).{...// This is the r text file corresponding to the main app
 File symbolTableWithPackageName =
                FileUtils.join(
                        globalScope.getIntermediatesDir(),
                        FD_RES,
                        "symbol-table-with-package",
                        scope.getVariantConfiguration().getDirName(),
                        "package-aware-r.txt");
 
 // Then create the corresponding processing task
 AndroidTask<ProcessAndroidResources> processAndroidResources =
                androidTasks.create(
                        tasks,
                        createProcessAndroidResourcesConfigAction(
                                scope,
                                symbolLocation,
                                symbolTableWithPackageName,
                                resPackageOutputFolder,
                                useAaptToGenerateLegacyMultidexMainDexProguardRules,
                                mergeType,
                                baseName));
// Add the output of the task
scope.addTaskOutput(
                VariantScope.TaskOutputType.PROCESSED_RES, resPackageOutputFolder, taskName);
        scope.addTaskOutput(
                VariantScope.TaskOutputType.SYMBOL_LIST,
                new File(symbolLocation.get(), FN_RESOURCE_TEXT),
                taskName);

        // Synthetic output for AARs (see SymbolTableWithPackageNameTransform), and created in
        // process resources for local subprojects.
        // Here we see the name of the file above us
        
        scope.addTaskOutput(
                VariantScope.TaskOutputType.SYMBOL_LIST_WITH_PACKAGE_NAME,
                symbolTableWithPackageName,
                taskName);

        scope.setProcessResourcesTask(processAndroidResources);
        scope.getSourceGenTask().optionalDependsOn(tasks, processAndroidResources);
        return processAndroidResources;     
 } 
Copy the code

Read as the corresponding code, which basically creates the corresponding task, and then adds the corresponding task output to the corresponding varientScope. Next we see createProcessAndroidResourcesConfigAction

createProcessAndroidResourcesConfigAction

ProcessAndroidResources, similar to PackageApp, is also a subclass of IncrementTask. Let’s look directly at the implementation of TaskAction. Internally, invokeApaptForSplit is called to generate the R resource.

protected void doFullTaskAction(a){...// if aapt does not exist, create aapt
try (Aapt aapt = bypassAapt ? null : makeAapt()) {    
    
List<ApkData> apkDataList = new ArrayList<>(splitsToGenerate);
// handle split
for (ApkData apkData : splitsToGenerate) {
     if (apkData.requiresAapt()) {
  boolean codeGen =(apkData.getType() == OutputFile.OutputType.MAIN
                                    || apkData.getFilter(OutputFile.FilterType.DENSITY) == null);
   if (codeGen) {
      apkDataList.remove(apkData);
      invokeAaptForSplit(manifestsOutputs,
                         libraryInfoList,
                         packageIdFileSet,
                         splitList,
                         featureResourcePackages,
                         apkData,
                         odeGen,
                         aapt);
                        break; }}}// Handle all of them
    for (ApkData apkData : apkDataList) {
        if (apkData.requiresAapt()) {
              executor.execute(
                  () -> {
                         invokeAaptForSplit(
                              manifestsOutputs,
                              libraryInfoList,
                              packageIdFileSet,
                              splitList,
                              featureResourcePackages,
                              apkData,
                              false,
                              aapt);
                          return null; }); }}... }Copy the code

invokeAaptForSplit

The resource files that need to be processed after the main configuration are called AndroidBuild for processing. AndroidBuild uses AAPT2 for processing internally. Next, we divide the internal files into basic ones for analysis

Configure the corresponding resource file
// Create the corresponding BuildOut set according to featureResourcePackage to match the corresponding resources with the output
for (File featurePackage : featureResourcePackages) {
     Collection<BuildOutput> splitOutputs =
     BuildOutputs.load(VariantScope.TaskOutputType.PROCESSED_RES, featurePackage);
     if (!splitOutputs.isEmpty()) {
                featurePackagesBuilder.add(Iterables.getOnlyElement(splitOutputs).getOutputFile());
     }
}    

// Create a generic path to the res output file, which is a temporary file and will be deleted after the merge.
//intermediates/res-main.ap_
File resOutBaseNameFile =
        new File(
           //intermediates
           resPackageOutputFolder,
            FN_RES_BASE
            + RES_QUALIFIER_SEP
             + apkData.getFullName()
              + SdkConstants.DOT_RES);
// Create a BuildOutput corresponding to the manifest output
BuildOutput manifestOutput =
   OutputScope.getOutput(manifestsOutputs, taskInputType, apkData);
// Other files
String packageForR = null;
File srcOut = null;
File symbolOutputDir = null;
File proguardOutputFile = null;
File mainDexListProguardOutputFile = null;
Copy the code

The following steps have little to do with the whole process. By default, no code is generated. Besides, InstanceRun process is not the focus of this article and aAPT is used, so there is no need to check these logic

if (generateCode) {
 ...
}
if (buildContext.isInInstantRunMode(){
 ...   
}
if (bypassAapt) {
...
}
Copy the code

Create AaptPackageConfig

The next step is to create the configuration corresponding to AAPT and make it process according to our configuration.

AaptPackageConfig.Builder config =
     new AaptPackageConfig.Builder()
          .setManifestFile(manifestFile)
          .setOptions(DslAdaptersKt.convert(aaptOptions))
          .setResourceDir(getInputResourcesDir().getSingleFile())
          .setLibrarySymbolTableFiles(
                  generateCode
                          ? dependencySymbolTableFiles
                          : ImmutableSet.of())
          .setCustomPackageForR(packageForR)
          .setSymbolOutputDir(symbolOutputDir)
          .setSourceOutputDir(srcOut)
          .setResourceOutputApk(resOutBaseNameFile)
          .setProguardOutputFile(proguardOutputFile)
          .setMainDexListProguardOutputFile(mainDexListProguardOutputFile)
          .setVariantType(getType())
          .setDebuggable(getDebuggable())
          .setPseudoLocalize(getPseudoLocalesEnabled())
          .setResourceConfigs(
                  splitList.getFilters(SplitList.RESOURCE_CONFIGS))
          .setSplits(getSplits(splitList))
          .setPreferredDensity(preferredDensity)
          .setPackageId(packageId)
          .setDependentFeatures(featurePackagesBuilder.build())
          .setListResourceFiles(aaptGeneration == AaptGeneration.AAPT_V2);

getBuilder().processResources(aapt, config);
Copy the code

Aapt is created in doFullTaskAction when this method is called, and if it is empty, it will be created

try (Aapt aapt = bypassAapt ? null: makeAapt()) { ... }... invokdeAaptForSplit(...)Copy the code

It is created by AaptGradleFactory


private Aapt makeAapt(a){ AndroidBuilder builder = getBuilder(); .return AaptGradleFactory.make(
    aaptGeneration,
    builder,
    processOutputHandler,
    fileCache,
    true,
    FileUtils.mkdirs(new File(getIncrementalFolder(), "aapt-temp")),
    aaptOptions.getCruncherProcesses());    
}

Copy the code

After analyzing the aAPT creation we continue to go back to the above, look at the configuration of those, the above code Settings are more, I briefly list some of the main ones

  • Set the Manifest file: setManifestFile
  • Set the ResourceDir: setResourceDir
  • R file table Settings library: setLibrarySymbolTableFiles
  • Set R file string content: setCustomPackageForR
  • Set the version of the resource generation tool: setListResourceFiles(aaptGeneration == aaptGeneration.AAPT_V2)

Call AndroidBuild to process the resource

Package_aware_r.txt (symbole) {package_aware_r.txt (symbole);

com.example.test
anim abc_fade_in
anim abc_fade_out
anim abc_grow_fade_in_from_bottom
anim abc_popup_enter
anim abc_popup_exit
anim abc_shrink_fade_out_from_bottom
anim abc_slide_in_bottom
anim abc_slide_in_top
anim abc_slide_out_bottom
anim abc_slide_out_top
attr counterTextColor
id tv_default_contact_wxchat
Copy the code

And then we’re looking at Lib’s

int dimen mtrl_snackbar_margin 0x0
int[] styleable ViewBackgroundHelper { 0x10100d4.0x0.0x0}...Copy the code

After reading the corresponding text to get some idea of it, we look at processResource in AndroidBuild

/** * generates R files, or other package resources ** /
public void processResources(a){

try {
     aapt.link(aaptConfig).get();
} catch (Exception e) {
            throw new ProcessException("Failed to execute aapt", e);
}
// Load the r text file in the app project
File mainRTxt = new File(aaptConfig.getSymbolOutputDir(), "R.txt");
SymbolTable mainSymbols =
        mainRTxt.isFile()
                ? SymbolIo.readFromAapt(mainRTxt, mainPackageName)
                : SymbolTable.builder().tablePackage(mainPackageNam
// Load the r.tuck file in the library we depend on
Set<SymbolTable> depSymbolTables =
        SymbolUtils.loadDependenciesSymbolTables(
                aaptConfig.getLibrarySymbolTableFiles(), mainPa
boolean finalIds = true;
// Check if it is Library, if so, the generated ID is not final
if (aaptConfig.getVariantType() == VariantType.LIBRARY) {
    finalIds = false;
// Call RGeneration to generate the corresponding R.java file
RGeneration.generateRForLibraries(mainSymbols, depSymbolTables, sourceOut, finalIds);
}
Copy the code
  1. For the link method with Aapt, we look at the official documentation

During the linking phase, AAPT2 merge all intermediate files (such as resource tables, binary XML files, and processed PNG files) generated during the compilation phase

When to compress PNG files with AAPT, we will analyze later.

  1. The generated id is not final for the library, which explains at the code level why r.id.x cannot be used in the switch ina separate component

RGeneration generated R.j ava

Let’s look directly at the code and summarize the main steps

public static void generateRForLibraryies(a) {

// Create a Map to store the SymboleTable (r.txt) to be written to
 Map<String, SymbolTable> toWrite = new HashMap<>();    
// Iterate over the collection of resources passed in by the calling method and place it in toWraiteMap, which is the default
// mian and finish, so filter out
for (SymbolTable st : libraries) {
   if (st.getTablePackage().equals(main.getTablePackage())) {
       continue;
   SymbolTable existing = toWrite.get(st.getTablePackage());
   if(existing ! =null) {
       toWrite.put(st.getTablePackage(), existing.merge(st));
   } else{ toWrite.put(st.getTablePackage(), st); }}// Iterate over each of the above collection keys and place them all under mainPackage
 // Because the dependency relationship duplicate resource id, if the resource does not exist will be automatically removed
 /** * for example, Library A has two versions, 1,2; Library B relies on version 1 of A, so its Symbole X will contain resource X inherited from A. Because Library A 2 does not contain resource X, its symboleTable will not have resource X. If the application or Library relies on both B and Library A 2, and therefore on the solution, * It automatically depends on the higher version of 2, which results in resource X being symboleTabe in B, but not in the resource. * Finally, because the resource does not exist, it does not exist in the main symbol table for (String PKG: new HashSet<>(toWrite.keySet())) { SymbolTable st = toWrite.get(pkg); st = main.filter(st).rename(st.getTablePackage()); toWrite.put(pkg, st); Towrite.values ().foreach (st -> symbolio.exportTojava (st, out, finalIds)); }Copy the code

By analyzing the code, there are three main steps

  • Create and populate toWriteMap
  • Merge to generate the main symboleTable, removing symbols for resources that do not exist
  • Generate the R.java file

Here we have analyzed how the resource file R is generated and how the resource is linked and merged after processing.

PNG compression

Finally, how does Aapt compress files

AaptOptions

We look at the corresponding code to see if the PNG compression configuration is turned on and out of date, with the buildType.isCrunchPngs control


    //This is replaced by {@link BuildType#isCrunchPngs()}.
    @Deprecated
    public boolean getCruncherEnabled(a) {
    // Simulate true if unset. This is not really correct, but changing it to be a tri-state
    // nullable Boolean is potentially a breaking change if the getter was being used by build
    // scripts or third party plugins.
    return cruncherEnabled == null ? true : cruncherEnabled;
    }
Copy the code

The latest buildType.isCrunchpng will eventually be used in MergeRource, which is responsible for merging resources

public void execute(@NonNull MergeResources mergeResourcesTask){... mergeResourcesTask.crunchPng = scope.isCrunchPngs(); . }Copy the code

Where scope is VariantScopeImpl

 public boolean isCrunchPngs(a) {
 
     Boolean buildTypeOverride = getVariantConfiguration().getBuildType().isCrunchPngs();
 
 }
Copy the code

So compression is initiated in the MergeResource Task. And the internal use is AAPT

protected void doFullTaskAction(a){
  processResources ? makeAapt(
          aaptGeneration,
          getBuilder(),
          fileCache,
          crunchPng,/ / compression
          variantScope,
          getAaptTempDir(),
          mergingLog)

}
Copy the code

Similar to the above analysis of R resource file generation, this is also created by AaptGradleFactory

public static Aapt makeAapt(a)
{
    return AaptGradleFactory.make(
        aaptGeneration,
        ...
        crunchPng,
        intermediateDir,
        scope.getGlobalScope().getExtension().getAaptOptions().getCruncherProcesses());
}
Copy the code

We found that this was only used in AAptV1

public static Aapt make(...).{

switch (aaptGeneration) {
    case AAPT_V1:
       return new AaptV1(
               builder.getProcessExecutor(),
               teeOutputHandler,
               buildTools,
               new FilteringLogger(builder.getLogger()),
               crunchPng ? AaptV1.PngProcessMode.ALL : AaptV1.PngProcessMode.NO_CRUNCH,
               cruncherProcesses);
   case AAPT_V2:
       return new OutOfProcessAaptV2(
              builder.getProcessExecutor(),
              teeOutputHandler,
              buildTools,
              intermediateDir,
              new FilteringLogger(builder.getLogger()));
   case AAPT_V2_JNI:
       return new AaptV2Jni(
               intermediateDir,
               WaitableExecutor.useGlobalSharedThreadPool(),
               teeOutputHandler,
               fileCache);
   case AAPT_V2_DAEMON_MODE:
       return new QueueableAapt2(
               teeOutputHandler,
               buildTools,
               intermediateDir,
               new FilteringLogger(builder.getLogger()),
               0 /* use default */); . }Copy the code

We know Aapt1 has been deprecated, so does it not compress PNG? Let’s start by looking at which class Aapt1 handles its compression

QueudCruncher

public AaptV1(a) {
this.cruncher =
     QueuedCruncher.builder()
             .executablePath(getAaptExecutablePath())
             .logger(logger)
             .numberOfProcesses(cruncherProcesses)
             .build();

  if(cruncher ! =null) {
      cruncherKey = cruncher.start();
  } else {
      cruncherKey = null; }}Copy the code

QueudCruncher is a QueuedResourceProcessor subclass, and start is a parent class, which is created by the parent class. Let’s look at the parent class constructor

QueueResourceProcessor

proteeted QueuedResourceProcessor(a) {

f (processesNumber > 0) {
            processToUse = processesNumber;
        } else {
            processToUse = DEFAULT_NUMBER_DAEMON_PROCESSES;
        }

        processingRequests =
                new WorkQueue<>(
                        logger, queueThreadContext, "queued-resource-processor", processToUse, 0);

}
Copy the code

� It creates the corresponding processingRequests based on processNumbler. Under Aapt1, the real processing is QueudCruncher’s compile method. The uppermost resource processing ResourceProcessor method, when called start, will call the key needed to generate its processing. This call is compile method in Aapt1

 public ListenableFuture<File> compile(@NonNull CompileResourceRequest request){... futureResult = cruncher.compile(cruncherKey, request,null); . }Copy the code

In cruncher.compile, the job created uses the processingRequests created by our parent class construct. The option to compress or not is passed down here. We look at the Compile method for cruncher

public ListenableFuture<File> compile(...).{
final Job<AaptProcess> aaptProcessJob =
   new AaptQueueThreadContext.QueuedJob({
   
   new Task<AaptProcess>() {
        @Override
        public void run(
                @NonNull Job<AaptProcess> job,
                @NonNull JobContext<AaptProcess> context)
                throws IOException {
            AaptProcess aapt = context.getPayload();
            if (aapt == null) {
                logger.error(
                        null."Thread(%1$s) has a null payload",
                        Thread.currentThread().getName());
                return; } aapt.crunch(request.getInput(), outputFile, job); }... processingRequests.push(aaptProcessJob); })}Copy the code

Look at the code and see that it adds tasks to its queue. Then, for each task, determine whether compression is needed. The real compression is still done by aAPT’s Curnch. When the start method is called, the task is started. The task performs the WorkQueue operation, and the run method processes the jobs in the task. Aapt2 Aapt1 we looking at the above analysis, we found Aapt2QueuedResourceProcessor, its like QueuedCruncher are QueuedResourceProcessor subclasses, therefore concluded that the should have the function of compression. But it does not accept the corresponding parameters. In Aapt1, this parameter is used to determine that if compression is not required, a copyFile will be returned in compile instead of creating the corresponding job.

if(! processMode.shouldProcess(request.getInput())) {return copyFile(request);
  }
// Turn on the compression method analyzed above
try {
 futureResult = cruncher.compile(cruncherKey, request, null); }...Copy the code

Aapt2 compression �

Aapp2 creates the class as QueueableAapt2 by default. Let’s see if the method is filtered

try {
      futureResult = aapt.compile(requestKey, request, processOutputHandler);
    } catch (ResourceCompilationException e) {
            throw new Aapt2Exception(
                    String.format("Failed to compile file %s", request.getInput()), e);
}

Copy the code

It ends up calling the method in AaptProcess, which, unlike Aapt1, still ends up calling compile instead of Crunch. There is no way to see how it is compressed because it is done by concatenating the corresponding command line. Let’s take Aapt, and look at how it builds commands. Okay

Aapt command build

Above AaptGradleFactory creates aAPT by passing TargetInfo, which holds buildInfo inside, which is passed to the corresponding AAPT instance

TargetInfo target = builder.getTargetInfo(); BuildToolInfo buildTools = target.getBuildTools(); .return new QueueableAapt2(
    teeOutputHandler,
    buildTools,
    intermediateDir,
    new FilteringLogger(builder.getLogger()),
    0 /* use default */); .Copy the code

BuildTools

Object creation adds the docking AAPT2 command


BuildToolInfo buildToolInfo = BuildToolInfo.modifiedLayout(
                buildToolRevision,
                mTreeLocation,
                new File(hostTools, FN_AAPT),
                new File(hostTools, FN_AIDL),
                new File(mTreeLocation, "prebuilts/sdk/tools/dx"),
                new File(mTreeLocation, "prebuilts/sdk/tools/lib/dx.jar"),
                new File(hostTools, FN_RENDERSCRIPT),
                new File(mTreeLocation, "prebuilts/sdk/renderscript/include"),
                new File(mTreeLocation, "prebuilts/sdk/renderscript/clang-include"),
                new File(hostTools, FN_BCC_COMPAT),
                new File(hostTools, "arm-linux-androideabi-ld"),
                new File(hostTools, "aarch64-linux-android-ld"),
                new File(hostTools, "i686-linux-android-ld"),
                new File(hostTools, "x86_64-linux-android-ld"),
                new File(hostTools, "mipsel-linux-android-ld"),
                new File(hostTools, FN_ZIPALIGN),
                new File(hostTools, FN_AAPT2));
        return new TargetInfo(androidTarget, buildToolInfo);


public static final String FN_AAPT2 =
            "aapt2" + ext(".exe"."");
Copy the code

When the corresponding Aapt2 object is created, it is used to obtain the execution path of Aapt2

public QueueableAapt2(
   @Nullable ProcessOutputHandler processOutputHandler,
   @NonNull BuildToolInfo buildToolInfo,
   @NonNull File intermediateDir,
   @NonNull ILogger logger,
   int numberOfProcesses) {
   (
      processOutputHandler,
      getAapt2ExecutablePath(buildToolInfo),
      intermediateDir,
      logger,
      numberOfProcesses);
    }
Copy the code

The AaptProcess that is finally executed above the creation will set this path in

//in AaptueuThreadContext.java
@Override
    public boolean creation(@NonNull Thread t) throws IOException, InterruptedException {
        try {
            AaptProcess aaptProcess = new AaptProcess.Builder(aaptLocation, logger).start();
            boolean ready = aaptProcess.waitForReadyOrFail();
            if (ready) {
                aaptProcesses.put(t.getName(), aaptProcess);
            }
            return ready;
        } catch (InterruptedException e) {
            logger.error(e, "Cannot start slave process");
            throwe; }}Copy the code

In this way aaptProcess will add the corresponding command after it, and can call the AAPT tool in the Sdk directory that we downloaded. We can also see the error annotation in the corresponding method, which is not analyzed.

Aapt2 tool compressed Png

We directly check the source code of AAPT tool under SDK and check the corresponding method in its png. CPP. The method is analyze_image, and the specific internal logic will not be analyzed.

analyze_image(logger, *info, grayScaleTolerance, rgbPalette, alphaPalette,
                  &paletteEntries, &hasTransparency, &colorType, outRows){



}
Copy the code

Here, the whole process of PNG compression is analyzed, and the whole process of AAPT call is also analyzed. The libpng library is used for compression. Pngcrunch. CPP can be found in AAPT2 /compile. Libpng can also be found in android.bp

static_libs: [
        "libandroidfw"."libutils"."liblog"."libcutils"."libexpat"."libziparchive"."libpng"."libbase"."libprotobuf-cpp-full"."libz"."libbuildversion"."libidmap2_policies",
    ],
    stl: "libc++_static".Copy the code

conclusion

This paper sorted out the overall internal process of AGP, focusing on the analysis of task creation, that is, what tasks are mainly created, that is, what these tasks do, combined with the official Apk packaging flow chart, a clear understanding of its process. At the same time, it also analyzes the generation of R. Java files, and explains why the componentization cannot be used in swtich. Finally, it simply analyzes how Aapt compresses PNG process, that is, how Aapt is used simple process.