preface

To be a good Android developer, you need a complete set ofThe knowledge systemHere, let us grow together into what we think ~.

At present, Gradle automation technology is becoming more and more important. Many students have been able to make their own Gradle plug-ins, but there are always some “memes” left in our minds, reminding us all the time, do you really master it? For example, “Meme 1” : The overall implementation architecture of Gradle plug-ins? I:… What are the most important optimizations or improvements in Android Gradle plugin update history? I:… “Meme 3” : What is the core process of Gradle builds? I:… “, “Meme 4” : How do dependencies work in Gradle? I:… , “Meme 5” : The AppPlugin build process? I:… , “Meme 6” : assembleDebug packaging process? I:… “, “Meme 7” : Can you explain the implementation principles of some important tasks? I:… . Are there many points that are not really understood?

Mind mapping

directory

  • Gradle plugin implementation architecture Overview
  • Second,Understand the update history of the Android Gradle Plugin
    • Android Gradle Plugin V3.5.0 (August 2019)
    • 2. Android Gradle Plugin V3.4.0 (April 2019)
    • Android Gradle Plugin V3.3.0 (2019 年 1 月)
    • 4. Android Gradle Plugin V3.2.0 (September 2018)
    • 5. Android Gradle Plugin V3.1.0 (March 2018)
    • 6. Android Gradle Plugin V3.0.0 (October 2017)
    • 7. Android Gradle Plugin V2.3.0 (February 2017)
  • Three,Gradle build core process parsing
    • 1, LoadSettings
    • 2, the Configure
    • 3, TaskGraph
    • 4, RunTasks
    • 5, Finished
  • Four,Gradle dependency implementation principles
    • 1. Indirectly call the Add method of DefaultDependencyHandler via MethodMissing mechanism to add dependencies.
    • 2. Different dependency declarations are actually converted by different converters.
    • Artifacts are Artifacts in nature.
    • 4. What is Configuration in Gradle?
    • 5. How does the Task publish its Artifacts?
  • Five,AppPlugin builds the process
    • 1. Preparation
    • 2. ConfigureProject configureProject
    • 3. ConfigureExtension configureExtension
    • 4, TaskManager# createTasksBeforeEvaluate create don’t rely on the flavor of the task
    • BasePlugin#createAndroidTasks create a build task
  • Vi.AssembleDebug packaging process analysis
    • 1. Review of Android packaging process
    • 2, assmableDebug packaging process analysis
  • Seven,Important tasks implement source code analysis
    • 1. Resource processing related tasks
    • 2. The process of packaging a Class file into a Dex file
  • Eight,conclusion
    • The last

Gradle plugin implementation architecture Overview

Android Gradle Plugin team before Android Gradle V3.2.0 has always been written in Java Gradle plugin, in V3.2.0 has used Kotlin for a large area of rewrite. Despite Groovy’s concise syntax and the flexibility with which its closures are written, Android Studio support for Groovy is very unfriendly, so for now we try to use Kotlin when writing custom Gradle plug-ins. This minimizes the pitfalls of not being prompted enough to write a plug-in.

Let’s take a look at the Gradle plugin’s overall implementation architecture, as shown in the following figure:

At the bottom level is the Gradle framework, which provides basic services such as task dependencies, directed acyclic graph construction, and so on.

The Android Gradle Plugin is built on the basis of the Gradle framework. Many Tasks and artifacts related to Android project packaging are created (artifacts are typically output after each task is executed).

At the top is a developer-defined Plugin. There are two common ways to use a custom Plugin, as follows:

  • 1) Insert some custom tasks based on the tasks provided by Android Gradle Plugin.
  • 2) Add Transform for compile-time code injection

Update history of Android Gradle Plugin

The following table lists the Gradle versions required for each Android Gradle plugin version. We should use the latest versions of Android Gradle Plugin and Gradle to get the best performance.

The plug-in version The required Gradle version
1.0.0-1.1.3 2.2.1-2.3 –
1.2.0-1.3.1 2.2.1-2.9 –
1.5.0 2.2.1-2.13 –
2.0.0-2.1.2 2.10-2.13
2.1.3-2.2.3 2.14.1 +
2.3.0 + 3.3 +
3.0.0 + 4.1 +
3.1.0 + 4.4 +
3.2.0-3.2.1 4.6 +
3.3.0-3.3.2 rainfall distribution on 10-12 4.10.1 +
3.4.0 3.4.1 track – 5.1.1 +
3.5.0 + 5.4.1-5.6.4

The latest Android Gradle Plugin version is V3.6.2 and Gradle version is V5.6.4. Let’s take a look at some of the most important changes in the Android Gradle Plugin update history.

Android Gradle Plugin V3.5.0 (August 2019)

The main focus of this update is to speed up the construction of the project.

2. Android Gradle Plugin V3.4.0 (April 2019)

If you are using Gradle 5.0 or higher, the default Gradle daemon memory heap size drops from 1 GB to 512 MB. This can cause build performance to degrade. If you want to replace this default setting, specify the Gradle daemon heap size in your project’s gradle.properties file.

1) The new Lint checks dependency configurations

A new dependency configuration, lintPublish, has been added and the old lintChecks behavior has been changed as follows:

  • lintChecks:Lint checks run only when a project is built locally.
  • lintPublish:Enable Lint checking in a published AAR so that projects that use the AAR will also apply those Lint checks.

The sample code is shown below:

dependencies {
      // Executes lint checks from the ':lint' project at build time.
      lintChecks project(':lint')
      // Packages lint checks from the ':lintpublish' in the published AAR.
      lintPublish project(':lintpublish')}Copy the code

2) Android does not need to install application function plug-in abandonment warning

In previous versions it was possible to build installation-free applications using the com.android.feature plugin. Now it is recommended to use dynamic feature plugin, which allows you to distribute installation-free and installation-free applications through a single Android App Bundle.

3) R8 is enabled by default

R8 significantly improves build performance by combining desugar (desugaring: the process of converting.class bytecode to.dex bytecode), compression, obfuscation, optimization, and dex processing in one step. R8 was introduced in Android Gradle Plugin V3.2.0 and is already enabled by default for applications and Android library projects using Plugin V3.4.0 and higher.

The compilation process before R8 was introduced

The compilation process after R8 was introduced

As you can see, R8 combines the functions of Proguard and D8. If you encounter compile failures due to R8, you can configure the following code to disable R8:

# Disables R8 for Android Library modules only.
android.enableR8.libraries = false
# Disables R8 for all modules.
android.enableR8 = false
Copy the code

3) discard ndkCompile

Compiling the native library with ndkBuild at this point will receive a build error. We should use CMake or NDK-build to add C and C++ code to the project.

Android Gradle Plugin V3.3.0 (2019 年 1 月)

1) Generate R classes faster for library projects

In older versions, the Android Gradle plugin generated an R.java file for each dependency of the project, and then compiled those R classes along with the rest of the application. Now, the plug-in directly generates the JAR containing the application’s compiled R classes, without first compiling the intermediate R.Java classes. This not only significantly improves the compilation performance of projects with multiple library subprojects and dependencies, but also speeds up the indexing of files in Android Studio.

4. Android Gradle Plugin V3.2.0 (September 2018)

New code compressor R8

R8 is a new tool that performs code compression and obfuscation instead of ProGuard. We simply add the following code to our project’s gradle.properties file to start using the preview version of R8:

android.enableR8 = true
Copy the code

Desugar using D8 is now enabled by default

5. Android Gradle Plugin V3.1.0 (March 2018)

New DEX compiler (D8)

By default, Android Studio uses a new DEX compiler called D8 at this point. DEX compilation is the process by which Pointers to ART (or Dalvik for older versions of Android) convert.class bytecode to.dex bytecode. Compared to previous compilers (DX), D8 compiles faster and outputs smaller DEX files while maintaining the same or better application runtime performance.

If problems occur while using D8, you can configure the following code in gradle.properties to temporarily disable D8 and use DX:

android.enableD8=false
Copy the code

6. Android Gradle Plugin V3.0.0 (October 2017)

1) The parallelism of multi-module projects is improved through finely controlled task diagrams.

When changing dependencies, Gradle speeds up compilation by not recompiling API modules for dependencies that cannot be accessed. Gradle’s new dependency configurations (implementation, API, compileOnly and runtimeOnly) can be used to limit which dependencies leak their apis to other modules.

2) With the help of dex processing of each class, incremental compilation speed can be accelerated.

Each class is now compiled into a separate DEX file, and only the modified classes are re-dex processed.

3) Enable Gradle compile cache to optimize certain tasks to use the cached output

Enabling Gradle compilation caching can speed up compilation by optimizing certain tasks to use the cached output.

4) AAPT2 have enabled and improved incremental resource handling by default

If we have problems with AAPT2, we can deactivate AAPT2 by setting the following code in the gradle.properties file:

android.enableAapt2=false
Copy the code

Then restart the Gradle daemon by running./gradlew –stop from the command line.

7. Android Gradle Plugin V2.3.0 (February 2017)

1) Increase the compile cache

Stores the specific output generated by the Android plug-in when the project is compiled. When caching is used, compiling plug-ins is significantly faster because the compilation system can directly reuse these cache files for subsequent compilations without having to recreate them. Alternatively, we can use the cleanBuildCache Task to clear the compile cache.

See gradle-Plugin for details of updates to older versions.

Gradle build core process analysis

When we type./gradlew/gradle… After the command, a Gradle build begins. It consists of the following three steps:

  • First, initialize Gradle to build the framework itself.
  • 2), then wrap the command-line arguments and send them to DefaultGradleLauncher.
  • Finally, trigger the Gradle build lifecycle in DefaultGradleLauncher and start the standard build process.

Before diving into the source code, let’s look at the core flow chart of Gradle builds from the top down to get a sense of the overall Gradle build process.

When we perform a gralde command will invoke the gradle/wrapper/gradle – wrapper. Org. Inside the jar gradle. Wrapper. GradleWrapperMain the main method of a class, It is an entry method for Gradle. The core source code for this method is as follows:

public static void main(String[] args) throws Exception {...Index to the gradle zip address configured in the gradle-wrapper.properties file and wrap it as a WrapperExecutor instance.
        WrapperExecutor wrapperExecutor = WrapperExecutor.forWrapperPropertiesFile(propertiesFile);
        Execute wrapperExecutor to execute gradle.
        wrapperExecutor.execute(args, new Install(logger, new Download(logger, "gradlew"."0"), new PathAssembler(gradleUserHome)), new BootstrapMainStarter());
}
Copy the code

WrapperExecutor (); wrapperExecutor ();

public void execute(String[] args, Install install, BootstrapMainStarter bootstrapMainStarter) throws Exception {
        // Download the gradle wrapper dependencies and source code.
        File gradleHome = install.createDist(this.config);
        // 2. Start the Gradle build process here.
        bootstrapMainStarter.start(args, gradleHome);
    }
Copy the code

First, in comment 1, you download the dependencies and source code for gradle Wrapper. Then, in comment 2, the start method of bootstrapMainStarter is called to start the Gradle build process from there. The getLoadedSettings, getConfiguredBuild, executeTasks, and finishBuild methods of DefaultGradleLauncher are eventually called internally. Their corresponding states are defined in the Stage enumeration class in DefaultGradleLauncher, as follows:

private static enum Stage {
        LoadSettings,
        Configure,
        TaskGraph,
        RunTasks {
            String getDisplayName(a) {
                return "Build";
            }
        },
        Finished;

        private Stage(a) {}String getDisplayName(a) {
            return this.name(); }}Copy the code

Below, we will analyze these five processes in detail.

1, LoadSettings

The process of loading setting.gradle begins when the getLoadedSettings method is called. The source code is as follows:

public SettingsInternal getLoadedSettings(a) {
        this.doBuildStages(DefaultGradleLauncher.Stage.LoadSettings);
        return this.gradle.getSettings();
}
Copy the code

The doBuildStages method is again called, and the internal implementation is as follows:

private void doBuildStages(DefaultGradleLauncher.Stage upTo) { Preconditions.checkArgument(upTo ! = DefaultGradleLauncher.Stage.Finished,"Stage.Finished is not supported by doBuildStages.");

        try {
            // Execute when Stage is RunTask.
            if (upTo == DefaultGradleLauncher.Stage.RunTasks && this.instantExecution.canExecuteInstantaneously()) {
                this.doInstantExecution();
            } else {
               // Execute when Stage is not RunTask. this.doClassicBuildStages(upTo);}}catch (Throwable var3) {
            this.finishBuild(upTo.getDisplayName(), var3); }}Copy the code

Continue to call doClassicBuildStages. The source code is as follows:

private void doClassicBuildStages(DefaultGradleLauncher.Stage upTo) {
        // If Stage is LoadSettings, execute prepareSettings to configure and generate Setting instance.
        this.prepareSettings();
        if(upTo ! = DefaultGradleLauncher.Stage.LoadSettings) {// Execute prepareProjects when Stage is set to Configure.
            this.prepareProjects();
            if(upTo ! = DefaultGradleLauncher.Stage.Configure) {// Execute prepareTaskExecution when Stage is a TaskGraph.
                this.prepareTaskExecution();
                if(upTo ! = DefaultGradleLauncher.Stage.TaskGraph) {// Execute saveTaskGraph and runWork to save the TaskGraph and execute the corresponding Tasks. this.instantExecution.saveTaskGraph();
                   this.runWork(); }}}}Copy the code

As you can see, the doClassicBuildStages method is an important one that distributes all Stage tasks. Here’s a summary:

  • 1) Execute prepareSettings method to configure and generate Setting instance when Stage is LoadSettings.
  • 2) Execute prepareProjects method to Configure the project when Stage is Configure.
  • Execute prepareTaskExecution when Stage is a TaskGraph.
  • 4) execute saveTaskGraph and runWork methods to saveTaskGraph and execute corresponding Tasks when Stage is RunTasks.

Then, we move on to the prepareSettings method, whose source code looks like this:

private void prepareSettings(a) {
        if (this.stage == null) {
            / / 1, the callback BuildListener. BuildStarted () callback interface.
            this.buildListener.buildStarted(this.gradle);
            Call the prepareSettings method of DefaultSettingsPreparer, the implementation class of settingPreparer.
            this.settingsPreparer.prepareSettings(this.gradle);
            this.stage = DefaultGradleLauncher.Stage.LoadSettings; }}Copy the code

The prepareSettings method does two things:

  • 1), the callback BuildListener buildStarted interface.
  • 2) Call the prepareSettings method of DefaultSettingsPreparer, the implementation class of the settingPreparer interface.

We continue to see the prepareSettings method for DefaultSettingsPreparer as follows:

public void prepareSettings(GradleInternal gradle) {
        // run init.gradle, which is called before each project build to do some initialization.
        this.initScriptHandler.executeScripts(gradle); SettingsLoader settingsLoader = gradle.getParent() ! =null ? this.settingsLoaderFactory.forNestedBuild() : this.settingsLoaderFactory.forTopLevelBuild();
        // 2. Call findAndLoadSettings of DefaultSettingsLoader to locate settings. gradle.
        settingsLoader.findAndLoadSettings(gradle);
}
Copy the code

Two things are done in the prepareSettings method:

  • Execute init.gradle, which is called before each project build to do some initialization.
  • 2) Call findAndLoadSettings of SettingLoader interface DefaultSettingsLoader to locate settings. gradle.

DefaultSettingLoader findAndLoadSettings method associated with the implementation of the code is very much, limited by space, I directly point out the main processing flow in the findAndLoadSettings method:

  • 1) First, find settings.gradle location.
  • 2), then compile the contents in the buildSrc (Android’s default Plugin directory) folder.
  • 3) Next, parse gradle.properites: the configuration information in gradle.properties and the configuration properties passed in from the command line are read and stored.
  • 4), then, the analytic Settings. Gradle file: here the last invoked BuildOperationScriptPlugin. Apply to perform Settings. Gradle file.
  • 5) Finally, create project and subProject instances based on the information in settings. gradle.

2, the Configure

After the LoadSetting stage, the Configure stage is executed, and all the configuration stage does is compile gradle scripts into class files and execute them. As can be seen from the previous, the prepareProjects method will be executed as follows:

private void prepareProjects(a) {
        if (this.stage == DefaultGradleLauncher.Stage.LoadSettings) {
            // call the prepareProjects method of the DefaultProjectsPreparer interface implementation class.
            this.projectsPreparer.prepareProjects(this.gradle);
            this.stage = DefaultGradleLauncher.Stage.Configure; }}Copy the code

The prepareProjects method of DefaultProjectsPreparer, the implementation class of the ProjectsPreparer interface, is continued here. The source code is as follows:

public void prepareProjects(GradleInternal gradle) {...// If configure-on-demand is specified in the gradle.properties file, only the main project and the project required to execute the task will be configured.
        if (gradle.getStartParameter().isConfigureOnDemand()) {
            this.projectConfigurer.configure(gradle.getRootProject());
        } else {
            Configure-on-demand = configure-on-demand = configure-on-demand = configure-on-demand = configure-on-demand = configure-on-demand The configureHierarchy method of the TaskPathProjectEvaluator implementation class of the ProjectConfigurer interface is called to configure all projects.
            this.projectConfigurer.configureHierarchy(gradle.getRootProject());
            (new ProjectsEvaluatedNotifier(this.buildOperationExecutor)).notify(gradle);
        }

        this.modelConfigurationListener.onConfigure(gradle);
}
Copy the code

In comment 1, if the parameter configure-on-demand is specified in the gradle.properties file, only the main project and the projects needed to execute the task are configured. We are only looking at cases where the default is not specified. Otherwise, in comment 2, the configureHierarchy method of the TaskPathProjectEvaluator implementation class of the ProjectConfigurer interface is called to configure all projects.

We continue to see the configureHierarchy method as follows:

public void configureHierarchy(ProjectInternal project) {
        this.configure(project);
        Iterator var2 = project.getSubprojects().iterator();

        while(var2.hasNext()) {
            Project sub = (Project)var2.next();
            this.configure((ProjectInternal)sub); }}Copy the code

You can see that Iterator is used to traverse and configure all projects in the configureHierarchy method. The configure method ends up calling the Run method of the EvaluateProject class, as shown below:

public void run(final BuildOperationContext context) {
            this.project.getMutationState().withMutableState(new Runnable() {
                public void run(a) {
                    try {
                        
                        EvaluateProject.this.state.toBeforeEvaluate();
                       
                        / / 1, the callback ProjectEvaluationListener beforeEvaluate interface.
                        LifecycleProjectEvaluator.this.buildOperationExecutor.run(new LifecycleProjectEvaluator.NotifyBeforeEvaluate(EvaluateProject.this.project, EvaluateProject.this.state));
                       
                        if(! EvaluateProject.this.state.hasFailure()) {
                            EvaluateProject.this.state.toEvaluate();

                            try {
                               // The evaluate method sets the default init, Wrapper task, and default plugin, and then compels and executes the build.gradle script
                               LifecycleProjectEvaluator.this.delegate.evaluate(EvaluateProject.this.project, EvaluateProject.this.state);
                            } catch (Exception var10) {
                                LifecycleProjectEvaluator.addConfigurationFailure(EvaluateProject.this.project, EvaluateProject.this.state, var10, context);
                            } finally {
                                EvaluateProject.this.state.toAfterEvaluate();
                                / / 3, the callback ProjectEvaluationListener. AfterEvaluate interface.
                                LifecycleProjectEvaluator.this.buildOperationExecutor.run(new LifecycleProjectEvaluator.NotifyAfterEvaluate(EvaluateProject.this.project, EvaluateProject.this.state)); }}if (EvaluateProject.this.state.hasFailure()) {
                            EvaluateProject.this.state.rethrowFailure();
                        } else{ context.setResult(ConfigureProjectBuildOperationType.RESULT); }}finally {
                        EvaluateProject.this.state.configured(); }}}); }Copy the code

There are three important processes in EvaluateProject’s Run method:

  • 1), the callback ProjectEvaluationListener beforeEvaluate interface.
  • The evaluate method sets the default init, Wrapper task, and default plug-in, and then compilers and executes the build.gradle script.
  • 3), the callback ProjectEvaluationListener afterEvaluate interface.

3, TaskGraph

After the initialization and configuration phases, the prepareTaskExecution method of DefaultGradleLauncher is called to create a directed acyclic graph composed of Tasks. The method is as follows:

private void prepareTaskExecution(a) {
        if (this.stage == DefaultGradleLauncher.Stage.Configure) {
            / / 1, call TaskExecutionPreparer interface implementation class BuildOperatingFiringTaskExecutionPreparer prepareForTaskExecution method.
            this.taskExecutionPreparer.prepareForTaskExecution(this.gradle);
            this.stage = DefaultGradleLauncher.Stage.TaskGraph; }}Copy the code

Continue to call here TaskExecutionPreparer interface implementation class BuildOperatingFiringTaskExecutionPreparer prepareForTaskExecution method, as shown below:

 public void prepareForTaskExecution(GradleInternal gradle) {
        this.buildOperationExecutor.run(new BuildOperatingFiringTaskExecutionPreparer.CalculateTaskGraph(gradle));
}
Copy the code

As you can see, the CalculateTaskGraph build operation is performed using the buildOperationExecutor instance, and we can see its run method as follows:

public void run(BuildOperationContext buildOperationContext) {
            // 1. Fill in the task diagram
            final TaskExecutionGraphInternal taskGraph = this.populateTaskGraph();
            buildOperationContext.setResult(new Result() {
                 getRequestedTaskPaths() {
                    return this.toTaskPaths(taskGraph.getRequestedTasks());
                }

                
                public List<String> getExcludedTaskPaths(a) {
                    return this.toTaskPaths(taskGraph.getFilteredTasks());
                }

                private List<String> toTaskPaths(Set<Task> tasks) {
                    return ImmutableSortedSet.copyOf(Collections2.transform(tasks, new Function<Task, String>() {
                        public String apply(Task task) {
                            returntask.getPath(); } })).asList(); }}); }Copy the code

In note 1, populateTaskGraph is called directly to populate the Tasks directed acyclic graph. The source code is as follows:

TaskExecutionGraphInternal populateTaskGraph(a) {           
            / / 1, here again call TaskExecutionPreparer interface of another class DefaultTaskExecutionPreparer prepareForTaskExecution method.
            BuildOperatingFiringTaskExecutionPreparer.this.delegate.prepareForTaskExecution(this.gradle);
            return this.gradle.getTaskGraph();
}
Copy the code

As you can see, in note 1 place, again call TaskExecutionPreparer interface of another class DefaultTaskExecutionPreparer prepareForTaskExecution method. The method is as follows:

public void prepareForTaskExecution(GradleInternal gradle) {
        / / 1
        this.buildConfigurationActionExecuter.select(gradle);
        TaskExecutionGraphInternal taskGraph = gradle.getTaskGraph();
        // 2. Populate the taskGraph instance with dependencies between Tasks.
        taskGraph.populate();
        this.includedBuildControllers.populateTaskGraphs();
        if (gradle.getStartParameter().isConfigureOnDemand()) {
            (new ProjectsEvaluatedNotifier(this.buildOperationExecutor)).notify(gradle); }}Copy the code

In note 1, call the buildConfigurationActionExecuter the select method of the interface, + interface segregation adopted the strategy design pattern here, Subsequent will call ExcludedTaskFilteringBuildConfigurationAction configure method, in turn DefaultTasksBuildExecutionAction configure Methods, TaskNameResolvingBuildConfigurationAction configure method.

Now, let’s analyze the processing in turn.

1), ExcludedTaskFilteringBuildConfigurationAction# configure: the task of dealing with the need to rule out

public void configure(BuildExecutionContext context) {
        // 1. Get the names of the excluded Tasks and place them in turn in the filters.
        GradleInternal gradle = context.getGradle();
        Set<String> excludedTaskNames = gradle.getStartParameter().getExcludedTaskNames();
        if(! excludedTaskNames.isEmpty()) { Set<Spec<Task>> filters =new HashSet();
            Iterator var5 = excludedTaskNames.iterator();

            while(var5.hasNext()) {
                String taskName = (String)var5.next();
                filters.add(this.taskSelector.getFilter(taskName));
            }

            // add Tasks to the TaskGraph instance
            gradle.getTaskGraph().useFilter(Specs.intersect(filters));
        }

        context.proceed();
}
Copy the code

In comment 1, we get the names of the excluded Tasks and place them in the filters, and then in comment 2 we add the Tasks we want to filter to the TaskGraph instance.

As you can see, the TaskGraph is given a filter to handle tasks that need to be excluded, so that tasks can be excluded later when dependencies are evaluated.

2), DefaultTasksBuildExecutionAction# configure: add the default task

public void configure(BuildExecutionContext context) {
        StartParameter startParameter = context.getGradle().getStartParameter();
        Iterator var3 = startParameter.getTaskRequests().iterator();

        TaskExecutionRequest request;
        // Do nothing if you specify a Task to execute.
        do {
            if(! var3.hasNext()) { ProjectInternal project = context.getGradle().getDefaultProject();this.projectConfigurer.configure(project);
                List<String> defaultTasks = project.getDefaultTasks();
                // 2. If DefaultTasks is not specified, run gradle help.
                if (defaultTasks.size() == 0) {
                    defaultTasks = Collections.singletonList("help");
                    LOGGER.info("No tasks specified. Using default task {}", GUtil.toString(defaultTasks));
                } else {
                    LOGGER.info("No tasks specified. Using project default tasks {}", GUtil.toString(defaultTasks));
                }

                // 3. Otherwise, add the default Task.
                startParameter.setTaskNames(defaultTasks);
                context.proceed();
                return;
            }

            request = (TaskExecutionRequest)var3.next();
        } while(request.getArgs().isEmpty());

        context.proceed();
}
Copy the code

As you can see, DefaultTasksBuildExecutionAction class the configure method of processing is divided into the following three steps:

  • 1. If you specify a Task to execute, do nothing.
  • 2. If defaultTasks is not specified, gradle help information is displayed.
  • 3. Otherwise, add the default defaultTasks.

3), TaskNameResolvingBuildConfigurationAction# configure: computing task dependency graph

public void configure(BuildExecutionContext context) {
        GradleInternal gradle = context.getGradle();
        TaskExecutionGraphInternal taskGraph = gradle.getTaskGraph();
        List<TaskExecutionRequest> taskParameters = gradle.getStartParameter().getTaskRequests();
        Iterator var5 = taskParameters.iterator();

        while(var5.hasNext()) {
            TaskExecutionRequest taskParameter = (TaskExecutionRequest)var5.next();
            // 1. Parse Tasks.
            List<TaskSelection> taskSelections = this.commandLineTaskParser.parseTasks(taskParameter);
            Iterator var8 = taskSelections.iterator();

            // 2. Iterate over and add all selected Tasks to the taskGraph instance.
            while(var8.hasNext()) {
                TaskSelection taskSelection = (TaskSelection)var8.next();
                LOGGER.info("Selected primary task '{}' from project {}", taskSelection.getTaskName(), taskSelection.getProjectPath());
                taskGraph.addEntryTasks(taskSelection.getTasks());
            }
        }

        context.proceed();
}
Copy the code

There are two main processes:

  • 1),Parsing the Tasks:Analyze whether Project is specified before the Task argument in the command line, for example ‘: Project :assembleRelease’. If not, select all tasks under Project that match the taskName.
  • 2),Iterate over and add all selected Tasks to the taskGraph instance:Internally handles dependencies between Tasks such as Dependson Finalizedby MustrunAfter shouldrunAfter and stores the information in TaskInfo.

4) Populate the Task dependency graph

Finally, we return to DefaultTaskExecutionPreparer prepareForTaskExecution method, in 2 place, You can then call the taskGraph method to populate the taskGraph instance with the dependencies between Tasks.

4, RunTasks

After filling the taskGraph, we are ready to execute these Tasks. We see the executeTasks method of DefaultGradleLauncher instance as follows:

public GradleInternal executeTasks(a) {
        this.doBuildStages(DefaultGradleLauncher.Stage.RunTasks);
        return this.gradle;
}
Copy the code

The doClassicBuildStages method is also called in doClassicBuildStages.

 private void doClassicBuildStages(DefaultGradleLauncher.Stage upTo) {
        this.prepareSettings();
        if(upTo ! = DefaultGradleLauncher.Stage.LoadSettings) {this.prepareProjects();
            if(upTo ! = DefaultGradleLauncher.Stage.Configure) {this.prepareTaskExecution();
                if(upTo ! = DefaultGradleLauncher.Stage.TaskGraph) {this.instantExecution.saveTaskGraph();
                    / / 1
                    this.runWork(); }}}}Copy the code

At comment 1, the runWork method is continued, as follows:

private void runWork(a) {
        if (this.stage ! = DefaultGradleLauncher.Stage.TaskGraph) {throw new IllegalStateException("Cannot execute tasks: current stage = " + this.stage);
        } else {
            List<Throwable> taskFailures = new ArrayList();
            / / 1
            this.buildExecuter.execute(this.gradle, taskFailures);
            if(! taskFailures.isEmpty()) {throw new MultipleBuildFailures(taskFailures);
            } else {
                this.stage = DefaultGradleLauncher.Stage.RunTasks; }}}Copy the code

The buildExecuter interface’s implementation class DefaultBuildWorkExecutor is called as follows:

public void execute(GradleInternal gradle, Collection<? super Throwable> failures) {
        this.execute(gradle, 0, failures);
}

private void execute(final GradleInternal gradle, final int index, final Collection<? super Throwable> taskFailures) {
        if (index < this.executionActions.size()) {
            / / 1
            ((BuildExecutionAction)this.executionActions.get(index)).execute(new BuildExecutionContext() {
                public GradleInternal getGradle(a) {
                    return gradle;
                }

                public void proceed(a) {
                  // 2. After executing the first item in the executionActions list, proceed to the next item.
                    DefaultBuildWorkExecutor.this.execute(gradle, index + 1, taskFailures); } }, taskFailures); }}Copy the code

1), perform DryRunBuildExecutionAction: DryRun processing

As you can see, here again call BuildExecutionAction interface implementation class DryRunBuildExecutionAction the execute method, as shown below:

 public void execute(BuildExecutionContext context, Collection<? super Throwable> taskFailures) {
        GradleInternal gradle = context.getGradle();
        // 1. If the command line contains the --dry-run parameter, the Task execution is skipped and the relationship between the Task name and execution sequence is displayed.
        if (gradle.getStartParameter().isDryRun()) {
            Iterator var4 = gradle.getTaskGraph().getAllTasks().iterator();

            while(var4.hasNext()) {
                Task task = (Task)var4.next();
                this.textOutputFactory.create(DryRunBuildExecutionAction.class).append(((TaskInternal)task).getIdentityPath().getPath()).app end("").style(Style.ProgressStatus).append("SKIPPED").println(); }}else{ context.proceed(); }}Copy the code

In comment 1, if the command line contains the –dry-run parameter, the execution of the Task is skipped and the Task name and execution sequence are printed.

2), executive: SelectedTaskExecutionAction: use threads to execute the Tasks selected

After perform DryRunBuildExecutionAction, back to the execute method of DefaultBuildWorkExecutor class, at 2, executes executionActions next item on the list, namely, the second: SelectedTaskExecutionAction, we take a look at it the execute method, as shown below:

public void execute(BuildExecutionContext context, Collection<? super Throwable> taskFailures) {... taskGraph.addTaskExecutionGraphListener(new SelectedTaskExecutionAction.BindAllReferencesOfProjectsToExecuteListener());
        
        / / 1.
        taskGraph.execute(taskFailures);
}
Copy the code

As you can see, here called directly TaskExecutionGraphInternal interface implementation class DefaultTaskExecutionGraph the execute method to perform the Tasks, it is a key source as shown below:

public void execute(Collection<? super Throwable> failures) {
        ProjectExecutionServiceRegistry projectExecutionServices = new ProjectExecutionServiceRegistry();

        try {
            // Use a thread pool to execute tasks
            this.executeWithServices(projectExecutionServices, failures);
        } finally{ projectExecutionServices.close(); }}Copy the code

Here and continue to call the DefaultTaskExecutionGraph executeWithServices method using thread pool in parallel execution Task, its core source as shown below:

private void executeWithServices(ProjectExecutionServiceRegistry projectExecutionServices, Collection<? super Throwable> failures) {...this.ensurePopulated(); .try {
            / / 1
            this.planExecutor.process(this.executionPlan, failures, new DefaultTaskExecutionGraph.BuildOperationAwareExecutionAction(this.buildOperationExecutor.getCurrentOperation(), new DefaultTaskExecutionGraph.InvokeNodeExecutorsAction(this.nodeExecutors, projectExecutionServices)));
            LOGGER.debug("Timing: Executing the DAG took " + clock.getElapsed());
        } finally{... }}Copy the code

Finally, here is the process method calling DefaultPlanExecutor, the implementation class of the PlanExecutor interface, as shown below:

public void process(ExecutionPlan executionPlan, Collection<? super Throwable> failures, Action<Node> nodeExecutor) {
        ManagedExecutor executor = this.executorFactory.create("Execution worker for '" + executionPlan.getDisplayName() + "'");

        try {
            WorkerLease parentWorkerLease = this.workerLeaseService.getCurrentWorkerLease();
            // start using the thread pool to execute tasks asynchronously
            this.startAdditionalWorkers(executionPlan, nodeExecutor, executor, parentWorkerLease);
            (new DefaultPlanExecutor.ExecutorWorker(executionPlan, nodeExecutor, parentWorkerLease, this.cancellationToken, this.coordinationService)).run();
            this.awaitCompletion(executionPlan, failures);
        } finally{ executor.stop(); }}Copy the code

In comment 1, a thread pool is used to execute tasks asynchronously, as shown below:

private void startAdditionalWorkers(ExecutionPlan executionPlan, Action<? super Node> nodeExecutor, Executor executor, WorkerLease parentWorkerLease) {
        LOGGER.debug("Using {} parallel executor threads".this.executorCount);

        for(int i = 1; i < this.executorCount; ++i) {
            executor.execute(new DefaultPlanExecutor.ExecutorWorker(executionPlan, nodeExecutor, parentWorkerLease, this.cancellationToken, this.coordinationService)); }}Copy the code

Note that the default thread used to execute tasks is 8 threads. Using thread pool performed DefaultPlanExecutor ExecutorWorker here, in its run method will call to DefaultBuildOperationExecutor run method to perform the Task. However, some pre-processing needs to be done before the Task is executed.

3) Some pre-processing before Task execution

In the run method of DefaultBuildOperationExecutor only did two things, here we can see, as shown below:

  • First, call TaskExecutionListener#beforeExecute.
  • 2. Then,A chain performs a series of common processing of tasks, the specific processing is as follows:
    • 1),CatchExceptionTaskExecuter#execute:Add try catch to avoid exceptions during execution.
    • 2),ExecuteAtMostOnceTaskExecuter#execute:Check whether the task has been executed.
    • 3),SkipOnlyIfTaskExecuter#execute:Check whether the onlyIf condition of the task can be executed.
    • 4),SkipTaskWithNoActionsExecuter#execute:Skip tasks that have no action. If there is no action, the task does not need to be executed.
    • 5),ResolveTaskArtifactStateTaskExecuter#execute:Set the state of the artifact.
    • 6),SkipEmptySourceFilesTaskExecuter#execut:Skip tasks whose source file is set to empty. If the source file is empty, the task has no resources to process.
    • 7),ValidatingTaskExecuter#execute:Verify that the task can be executed.
    • 8),ResolveTaskOutputCachingStateExecuter#execute:Process the task output cache.
    • 9),SkipUpToDateTaskExecuter#execute:Skip the update-to-date task.
    • 10),ExecuteActionsTaskExecuter#execute:An executer used to actually execute tasks.

As you can see, before the real execution Task need to experience some general Task of the preprocessing, the last to be called ExecuteActionsTaskExecuter the execute method to actually perform the Task.

4) Actually execute tasks

public TaskExecuterResult execute(final TaskInternal task, final TaskStateInternal state, final TaskExecutionContext context) {
        final ExecuteActionsTaskExecuter.TaskExecution work = new ExecuteActionsTaskExecuter.TaskExecution(task, context, this.executionHistoryStore, this.fingerprinterRegistry, this.classLoaderHierarchyHasher);
        // Use workExecutor objects to actually execute tasks.
        final CachingResult result = (CachingResult)this.workExecutor.execute(new AfterPreviousExecutionContext() {
            public UnitOfWork getWork(a) {
                return work;
            }

            public Optional<String> getRebuildReason(a) {
                return context.getTaskExecutionMode().getRebuildReason();
            }

            public Optional<AfterPreviousExecutionState> getAfterPreviousExecutionState(a) {
                returnOptional.ofNullable(context.getAfterPreviousExecution()); }}); . }Copy the code

Here you can see, using the workExecutor objects to actually perform the Task, when executed will callback ExecuteActionsTaskExecuter. TaskExecution inner class the execute method, its implementation source code is as follows:

 public WorkResult execute(@Nullable InputChangesInternal inputChanges) {
            this.task.getState().setExecuting(true);

            WorkResult var2;
            try {
                ExecuteActionsTaskExecuter.LOGGER.debug("Executing actions for {}.".this.task);
               
                Call back the beforeActions interface of TaskActionListener.
                ExecuteActionsTaskExecuter.this.actionListener.beforeActions(this.task);
               
                // all actions are executed internally.
                ExecuteActionsTaskExecuter.this.executeActions(this.task, inputChanges);
                var2 = this.task.getState().getDidWork() ? WorkResult.DID_WORK : WorkResult.DID_NO_WORK;
            } finally {
                this.task.getState().setExecuting(false);
               
               / / 3, the callback TaskActionListener afterActions.
               ExecuteActionsTaskExecuter.this.actionListener.afterActions(this.task);
            }

            return var2;
}
Copy the code

In ExecuteActionsTaskExecuter. TaskExecution inner class did three things in the execute method, as shown below:

  • 1) Callback the beforeActions of TaskActionListener.
  • 2) All actions that execute Task are iterated internally. => Executing a Task is executing all the actions that are involved, which is the essence of a Task.
  • 3), the callback TaskActionListener afterActions.

After executing all the actions of the Task, Will eventually in the run method of DefaultBuildOperationExecutor callback TaskExecutionListener. AfterExecute identified Task completes finally.

5, Finished

Only one important thing is done in the Finished phase, which is the buildFinished interface that calls back to buildListener, as shown below:

private void finishBuild(String action, @Nullable Throwable stageFailure) {
        if (this.stage ! = DefaultGradleLauncher.Stage.Finished) { ...try {
               
                 this.buildListener.buildFinished(buildResult);
            } catch (Throwable var7) {
                failures.add(var7);
            }

            this.stage = DefaultGradleLauncher.Stage.Finished; . }}Copy the code

6, summary

From the analysis of Gradle executing tasks, we can see that the essence of a Task is a series of Actions. Now that we know more about the Gradle build process, let’s go back to the beginning and look at the Gradle build flow chart.

In addition, Zhangyi54’s animation explanation of Gradle principle visualized the boring principle process, which is worth recommending. What needs to be noted is that Gradle version of animation explanation is older, but the main principle is still the same, you can rest assured to watch.

Note: BuildScript for build.gradle scripts is executed before the rest of the script.

Gradle depends on the implementation principle

1. Indirectly call the Add method of DefaultDependencyHandler via MethodMissing mechanism to add dependencies

DependencyHandler: implementation(), API (), compile(); DependencyHandler: implementation(); API ();

This is done by indirectly calling the Add () method of the DefaultDependencyHandler implementation via the MethodMissing mechanism.

So, what is methodMissing?

It is an important feature of the Groovy language that allows calls to undefined methods to be caught at run time.

Gradle encapsulates this feature. To use this feature, a class simply implements the MixIn interface. The code looks like this:

public interface MethodMixIn {
    MethodAccess getAdditionalMethods(a);
}
Copy the code

2. Different dependency declarations are actually converted by different converters

For example, the following two dependencies:

  • 1),DependencyStringNotationConverter:Will be similar to the ‘androidx. Appcompat: appcompat: 1.1.0’ regular depend on the statement is converted to rely on.
  • 2),DependencyProjectNotationConverter:Convert dependency declarations like ‘project(“:mylib”)’ to dependencies.

In addition, all dependencies except the Project dependency will eventually be converted to SelfResolvingDependency, which can resolve itself.

Artifacts are Artifacts in nature.

4. What is Configuration in Gradle?

Implementation and API. However, assuming moudle or AAR dependencies is just the surface of the Configuration, and the essence of the dependencies is to obtain Artifacts generated during the dependency build process.

For most tasks, there is output after execution. Artifacts generation and consumption between moudles constitute a complete producer-consumer model.

5. How does the Task publish its Artifacts?

In every task calls VariantScopeImpl publishIntermediateArtifact methods to release their product, the final will be called to DefaultVariant an artifact of the method of product release.

Reference links:


Android Gradle Plugin V3.6.2 source code

Gradle V5.6.4 source code

3, Android Plugin DSL Reference

Gradle DSL Reference

5, designing – gradle – plugins

Android-training => gradle

One of 7, serial | understanding Gradle framework: the Plugin, the Extension, buildSrc

8, serial | understanding gradle frame # 2: dependence analysis

9, serial | understanding gradle frame # 3: artifacts

Android Gradle Plugin Plugin

Android Gradle Plugin Plugin

12. Gradle Cook his computer (analysis of source code of construction)

13. Gradle Cook up his computer (analysis of the source code of core entrusted object of construction life cycle)

Thank you for reading this article and I hope you can share it with your friends or technical group, it means a lot to me.

I hope we can be friends inGithub,The Denver nuggetsTo share knowledge.