I always hear about AGP. What did it do?

Posted by fatfrank on Wed, 23 Feb 2022 16:32:56 +0100

preface

The beginning of the story is like this.

When reading "Android Development Master Course" before, it was mentioned in the column of startup optimization that systrace + function plug-in is a good way of Caton troubleshooting.

The main way is through Transform + Asm. I believe you are an old acquaintance.

When using the Demo for learning, I found that the Demo didn't work after upgrading AGP(Android Gradle Plugin) to 4.0.0.

After analyzing the Demo, it is found that the code does not use the method of directly registering Transform for stake insertion, but obtains the Task corresponding to transformClassesWithDexBuilderForxxx, and sets the Transform in the Task as the currently implemented Transform through reflection:

Then why can't this method work in AGP 4.0.0?

AS we all know, during the process of building code, AS will print the tasks executed into the Build window. By observing the construction process of different versions of AGP, it is found that transformClassesWithDexBuilderForxxx is not printed in the construction process of 4.0.0.

Ah, this

A good Task will be gone if it's gone. Well, see you for the AGP source code!

This article first briefly analyzes the role of AGP with you, and then analyzes the source code of Transform with you in subsequent articles.

1, Basic preparation

Before analyzing the source code, I think you should have a basic understanding of the Android packaging process, at least the packaging process shown in the figure below:

Otherwise, you may not understand the technical terms below.

2, How to open AGP source code

When looking at the AGP code, I was always struggling with whether to download the AGP source code. Later, I listened to the suggestions of my colleagues and leaders and directly used the code relied on by the project for analysis.

There are two main reasons:

  1. The source code of AGP is too large, 30g, and the version is very old
  2. Using the AGP code that the project depends on is simple

Just add it to the project

implementation "com.android.tools.build:gradle:4.1.1"

You can view it.

3, Code analysis

By the way, the version of AGP is 4.1.1.

The first step is to find AppPlugin

In AS, if a project is created, it is added under the main module by default:

apply plugin: 'com.android.application'

Small partners who have customized Plugin know that there must be a COM in the source code android. application. The properties file corresponds to it. This is our Plugin entry.

Global search com android. Application, open com android. application. Properties, which are:

implementation-class=com.android.build.gradle.AppPlugin

Press the "Command" button and click the source code. It is found that another Plugin is declared in AppPlugin. Finally, it jumps to:

implementation-class=com.android.build.gradle.internal.plugins.AppPlugin

The package name is different from the previous one. This is our final entry.

Do you have such doubts? I'll add apply plugin: com android. Application, when will this code be called?

I don't know if you have noticed that every time you change build When creating a Gradle file, the AS will ask us to click the "Sync Now" button. After clicking, the configuration process in Gradle will be triggered, and finally the Plugin#apply method will be run. You can verify it when customizing the Plugin.

Step 2 AppPlugin

The parent class of AppPlugin is AbstractAppPlugin, and the parent class of AbstractAppPlugin is BasePlugin. The plug-in starts in the BasePlugin#apply method:

@Override
public final void apply(@NonNull Project project) {
    CrashReporting.runAction(
            () -> {
                basePluginApply(project);
                pluginSpecificApply(project);
            });
}

Here, we only need to focus on the two methods basePluginApply and pluginSpecificApply in the method block.

Enter the key method basePluginApply method. In the early stage of this method, a lot of inspection work has been done, including path, version and AGP version. After that, a lot of monitoring work has been done. Take a look at the source code:

private void basePluginApply(@NonNull Project project) {
    // ...  Code omission
    // Dependency check
    DependencyResolutionChecks.registerDependencyCheck(project, projectOptions);
    // ...  Omit path check, module check, etc., and build parameter listener
    // AGP version check
    AgpVersionChecker.enforceTheSamePluginVersions(project);
    // Build listener for process Task execution
    RecordingBuildListener buildListener = ProfilerInitializer.init(project, projectOptions);
    ProfileAgent.INSTANCE.register(project.getName(), buildListener);
    threadRecorder = ThreadRecorder.get();
    //...  Code omission
    // a key
    // 1. Configuration items
    threadRecorder.record(
            ExecutionType.BASE_PLUGIN_PROJECT_CONFIGURE,
            project.getPath(),
            null,
            this::configureProject);
    // 2. Configuration extension
    threadRecorder.record(
            ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION,
            project.getPath(),
            null,
            this::configureExtension);
    // 3. Create a Task
    threadRecorder.record(
            ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION,
            project.getPath(),
            null,
            this::createTasks);
}

The key methods I have marked out are configuring items, configuring extensions and creating tasks.

Step 3 configure Project

It should be noted that this configuration does not correspond to the Gradle life cycle, but does some configuration work for the current Project.

private void configureProject() {
    // ...  Perform a large number of services
    // Dependent version dependent
    Provider<ConstraintHandler.CachedStringBuildService> cachedStringBuildServiceProvider =
            new ConstraintHandler.CachedStringBuildService.RegistrationAction(project)
                    .execute();
    // maven cache related
    Provider<MavenCoordinatesCacheBuildService> mavenCoordinatesCacheBuildService =
            new MavenCoordinatesCacheBuildService.RegistrationAction(
                    project, cachedStringBuildServiceProvider)
                    .execute();
    // Dependent library correlation
    new LibraryDependencyCacheBuildService.RegistrationAction(project).execute();
    // aapt preparation
    new Aapt2WorkersBuildService.RegistrationAction(project, projectOptions).execute();
    new Aapt2DaemonBuildService.RegistrationAction(project).execute();
    new SyncIssueReporterImpl.GlobalSyncIssueService.RegistrationAction(
            project, SyncOptions.getModelQueryMode(projectOptions))
            .execute();
    // SDK related
    Provider<SdkComponentsBuildService> sdkComponentsBuildService =
            new SdkComponentsBuildService.RegistrationAction(
                    project,
                    projectOptions,
                    project.getProviders()
                            .provider(() -> extension.getCompileSdkVersion()),
                    project.getProviders()
                            .provider(() -> extension.getBuildToolsRevision()),
                    project.getProviders().provider(() -> extension.getNdkVersion()),
                    project.getProviders().provider(() -> extension.getNdkPath()))
                    .execute();
    // Enforce minimum versions of certain plugins
    GradlePluginUtils.enforceMinimumVersionsOfPlugins(project, issueReporter);
    // Apply the Java plugin
    project.getPlugins().apply(JavaBasePlugin.class);
    dslServices =
            new DslServicesImpl(
                    projectServices,
                    new DslVariableFactory(syncIssueReporter),
                    sdkComponentsBuildService);
    // Message printing service registration
    MessageReceiverImpl messageReceiver =
            new MessageReceiverImpl(
                    SyncOptions.getErrorFormatMode(projectOptions),
                    projectServices.getLogger());
    // ...  ellipsis
    createLintClasspathConfiguration(project);
}

My understanding of the above code is the preparation before creating a Task. Moreover, the xxxAction described in the above code is also very confusing and does not correspond to the Action in the Task.

Step 4 confirm the extension

The corresponding method of confirming the extension is configureExtension.

Usually build. Com under the app module Gradle files often have such configurations:

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.qidian.test"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            minifyEnabled false
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

The purpose of configureExtension is to convert this kind of script information into information that can be recognized by the code:

private void configureExtension() {
    // Help class for Gradle DSL
    DslServices dslServices = globalScope.getDslServices();
    final NamedDomainObjectContainer<BaseVariantOutput> buildOutputs =
            project.container(BaseVariantOutput.class);
    // ...  Code omission
    // ...  variant's factory class and management, etc
    variantFactory = createVariantFactory(projectServices, globalScope);
    variantInputModel =
            new LegacyVariantInputManager(
                    dslServices,
                    variantFactory.getVariantType(),
                    new SourceSetManager(
                            project,
                            isPackagePublished(),
                            dslServices,
                            new DelayedActionsExecutor()));
    // Create extension
    extension =
            createExtension(
                    dslServices, globalScope, variantInputModel, buildOutputs, extraModelInfo);
    globalScope.setExtension(extension);
    variantManager =
            new VariantManager<>(
                    globalScope,
                    project,
                    projectServices.getProjectOptions(),
                    extension,
                    variantFactory,
                    variantInputModel,
                    projectServices,
                    threadRecorder);
    registerModels(
            registry,
            globalScope,
            variantInputModel,
            extension,
            extraModelInfo);
    // create default Objects, signingConfig first as its used by the BuildTypes.
    variantFactory.createDefaultComponents(variantInputModel);
    // ... 
}

A brief look at the code shows that most of the code is related to variant and extension.

Pay more attention to the generated extension. BasePlugin#createExtension is an abstract method, which is finally handed over to AppPlugin#createExtension method:

protected AppExtension createExtension(
        @NonNull DslServices dslServices,
        @NonNull GlobalScope globalScope,
        @NonNull
                DslContainerProvider<DefaultConfig, BuildType, ProductFlavor, SigningConfig>
                dslContainers,
        @NonNull NamedDomainObjectContainer<BaseVariantOutput> buildOutputs,
        @NonNull ExtraModelInfo extraModelInfo) {
    return project.getExtensions()
            .create(
                    "android",
                    getExtensionClass(),
                    dslServices,
                    globalScope,
                    buildOutputs,
                    dslContainers.getSourceSetManager(),
                    extraModelInfo,
                    new ApplicationExtensionImpl(dslServices, dslContainers));
}

At first glance, it seems unfamiliar, but if you have developed a plug-in, you must know AppExtension, which can get the build mentioned above Any information in android {} under gradle.

Step 5 create a Task

This should be the most important step. Creating a Task is in the BasePlugin#createTasks method:

private void createTasks() {
    // Register tasks that are not related to Variant
    threadRecorder.record(
            ExecutionType.TASK_MANAGER_CREATE_TASKS,
            project.getPath(),
            null,
            () ->
                    TaskManager.createTasksBeforeEvaluate(
                            globalScope,
                            variantFactory.getVariantType(),
                            extension.getSourceSets()));
    // After the Gradle configuration phase is completed, register the tasks related to Variant
    project.afterEvaluate(
            CrashReporting.afterEvaluate(
                    p -> {
                        variantInputModel.getSourceSetManager().runBuildableArtifactsActions();
                        threadRecorder.record(
                                ExecutionType.BASE_PLUGIN_CREATE_ANDROID_TASKS,
                                project.getPath(),
                                null,
                                this::createAndroidTasks);
                    }));
}

There are two main methods in this method:

  • Task manager #createtasksbeforeevaluate: the static method indicates that a batch of tasks will be created before Project configuration.
  • createAndroidTasks: register a callback after the configuration life cycle is completed. After the Project configuration is completed, the Variant has been determined, and a batch of tasks will be created.

Task manager #createtasksbeforeevaluate contains a large section of code for registering tasks. If you are interested, you can check the source code yourself.

Step 6 create a Task after configuration

Wait until Project enters the callback of the configuration life cycle, and enter the method createAndroidTasks:

final void createAndroidTasks() {
    if (extension.getCompileSdkVersion() == null) {
        // ...  compileSdkVersion related
    }
    // ...
    // get current plugins and look for the default Java plugin.
    if (project.getPlugins().hasPlugin(JavaPlugin.class)) {
        throw new BadPluginException(
                "The 'java' plugin has been applied, but it is not compatible with the Android plugins.");
    }
    // ...

    // Set some configuration
    ProcessProfileWriter.getProject(project.getPath())
            .setCompileSdk(extension.getCompileSdkVersion())
            .setBuildToolsVersion(extension.getBuildToolsRevision().toString())
            .setSplits(AnalyticsUtil.toProto(extension.getSplits()));

    String kotlinPluginVersion = getKotlinPluginVersion();
    if (kotlinPluginVersion != null) {
        ProcessProfileWriter.getProject(project.getPath())
                .setKotlinPluginVersion(kotlinPluginVersion);
    }
    AnalyticsUtil.recordFirebasePerformancePluginVersion(project);
    // Note 1 create Variant
    variantManager.createVariants();
    List<ComponentInfo<VariantT, VariantPropertiesT>> variants =
            variantManager.getMainComponents();
    TaskManager<VariantT, VariantPropertiesT> taskManager =
            createTaskManager(
                    variants,
                    variantManager.getTestComponents(),
                    !variantInputModel.getProductFlavors().isEmpty(),
                    globalScope,
                    extension,
                    threadRecorder);
    // Note 2 create Task
    taskManager.createTasks();

    // ...

    // Note 3: create Task configure compose related tasks
    taskManager.createPostApiTasks();

    // now publish all variant artifacts for non test variants since
    // tests don't publish anything.
    for (ComponentInfo<VariantT, VariantPropertiesT> component : variants) {
        component.getProperties().publishBuildArtifacts();
    }

    // ...
    variantManager.setHasCreatedTasks(true);
    // notify our properties that configuration is over for us.
    GradleProperty.Companion.endOfEvaluation();
}

First, it can be seen from note 1 that all variants have been created in this step.

Next, from notes 2 and 3, we can see that createAndroidTasks has used taskManager to create tasks twice.

Step 7 TaskManager creates multiple tasks for the first time

The TaskManager#createTasks method used to create a Task for the first time. Click this method:

public void createTasks() {
    // lint related tasks
    taskFactory.register(new PrepareLintJarForPublish.CreationAction(globalScope));
    // create a lifecycle task to build the lintChecks dependencies
    taskFactory.register(
            COMPILE_LINT_CHECKS_TASK,
            task -> task.dependsOn(globalScope.getLocalCustomLintChecks()));
    // Create top level test tasks.
    createTopLevelTestTasks();
    // Focus on traversing variant create tasks for all variants (main and tests)
    for (ComponentInfo<VariantT, VariantPropertiesT> variant : variants) {
        createTasksForVariant(variant, variants);
    }
    // Test related tasks
    for (ComponentInfo<
            TestComponentImpl<? extends TestComponentPropertiesImpl>,
            TestComponentPropertiesImpl>
            testComponent : testComponents) {
        createTasksForTest(testComponent);
    }
    // Information record related tasks
    createReportTasks();
}

There are still many registered tasks, Lint, tasks related to testing and information recording, etc.

The most important thing is to get the Variant created above, and traverse and execute the createTasksForVariant method. Let's see which methods it registers for each Variant:

private void createTasksForVariant(
        @NonNull ComponentInfo<VariantT, VariantPropertiesT> variant,
        @NonNull List<ComponentInfo<VariantT, VariantPropertiesT>> variants) {
    // ...  ellipsis
    createAssembleTask(variantProperties);
    if (variantType.isBaseModule()) {
        createBundleTask(variantProperties);
    }
    doCreateTasksForVariant(variant, variants);
}

You must be familiar with the method of createassemblyask, because we use the commands of assemblydebug or assemblyrelease every time we package. This method is to create the Task corresponding to the assembly.

The docreatestatsforvariant method is a method to create tasks related to Variant. However, in TaskManager, it is an abstract method, which is implemented by ApplicationTaskManager.

So what tasks are created in it? Turn back and the picture in the back will tell you!

Step 8 TaskManager creates multiple tasks for the second time

The second time multiple tasks are created, the TaskManager#createPostApiTasks method is called, which is mainly related to ViewBinding, DataBinding and Kotlin compilation. You can take a look at them if you are interested.

I won't analyze them with the students one by one here. Just look at the picture:

Briefly explain:

  • Blue: tasks registered by createTasksBeforeEvaluate before Gradle configuration phase
  • Orange: Task created after the Gradle configuration phase is completed
  • Red: important tasks
  • Arrow: dependencies (not all)

Of course, I didn't list all the tasks, and the dependencies only listed what I saw (there was too much code to read).

If we combine the above picture with the previous official packaging flow chart, we find that many can be corresponding:

  1. There are processing tasks such as AIDL, Source Code and Resource file in front
  2. There are tasks related to Class compilation and code confusion in the medium term
  3. Later, create and merge Dex and package Apk related tasks

Moreover, there are dependencies between tasks (not shown in the figure). For example, I command:

./gradlew assembleDebug

This command will call the Task corresponding to assemblydebug. Before that, it will complete the previously dependent tasks, such as resource processing, compiling related, packaging and generating the APK we want, etc.

Here, the source code is almost analyzed. Back to the second step, BasePlugin also executes the pluginSpecificApply method in the apply method, but this method is an empty method.

summary

The purpose of this article is to give you an outline of AGP. What has AGP done?

It can be found that most of the tasks registered in AGP are for packaging services, and each small Task is a screw for packaging the assembly line.

The next article will analyze the process of Transform with you. See you in the next issue!

If you think this article is good, "like" is the best affirmation!

Reference article:

< 🍵 Supplement Android skill tree -- from AGP construction process to APK packaging process
[Android cultivation manual] Gradle chapter - Gradle source code analysis

Topics: Android Gradle