5月27日 23:19
How does Gradle's incremental build work? How to optimize build performance?
Gradle's incremental build is one of its core performance optimization features. It significantly improves build speed by only processing changed files. Here's a detailed explanation of Gradle incremental builds:
Incremental Build Principles
Incremental builds are based on task input and output declarations. Gradle tracks changes to these files and only re-executes tasks when input files have changed.
Workflow
- First build: Execute task and record hash values of inputs and outputs
- Subsequent builds: Compare current input hash values with recorded hash values
- Decision:
- If inputs unchanged and outputs exist, skip task execution (UP-TO-DATE)
- If inputs changed or outputs don't exist, re-execute task
Input and Output Declarations
Basic Inputs and Outputs
groovytasks.register('processFiles') { // Input files inputs.file('input.txt') inputs.dir('src/main/resources') // Output directory outputs.dir('build/processed') doLast { copy { from 'src/main/resources' into 'build/processed' } } }
Multiple Inputs and Outputs
groovytasks.register('combineFiles') { inputs.files('file1.txt', 'file2.txt') inputs.files(fileTree('src') { include '**/*.java' }) outputs.file('build/combined.txt') outputs.dirs('build/classes', 'build/resources') doLast { // Process files } }
Property Inputs
groovytasks.register('generateCode') { // Property inputs inputs.property('version', project.version) inputs.property('target', 'production') outputs.dir('build/generated') doLast { // Generate code based on properties } }
Incremental Builds for Built-in Tasks
Java Compilation Tasks
groovy// Compilation tasks provided by Java plugin automatically support incremental builds tasks.withType(JavaCompile).configureEach { options.incremental = true // Enable incremental compilation }
Test Tasks
groovytest { // Test tasks automatically support incremental builds useJUnitPlatform() // Configure test inputs testLogging { events 'passed', 'skipped', 'failed' } }
Custom Incremental Tasks
Using Incremental Task API
groovyabstract class IncrementalTask extends DefaultTask { @Incremental @InputDirectory abstract DirectoryProperty getInputDirectory() @OutputDirectory abstract DirectoryProperty getOutputDirectory() @TaskAction void execute(InputChanges inputChanges) { if (inputChanges.incremental) { inputChanges.getFileChanges(inputDirectory).each { change -> if (change.fileType == FileType.FILE) { switch (change.changeType) { case ChangeType.ADDED: println "Added file: ${change.file}" processFile(change.file) break case ChangeType.MODIFIED: println "Modified file: ${change.file}" processFile(change.file) break case ChangeType.REMOVED: println "Removed file: ${change.file}" removeOutput(change.file) break } } } } else { // Non-incremental build, process all files println "Executing full build" inputDirectory.get().asFile.eachFileRecurse { file -> if (file.isFile()) { processFile(file) } } } } void processFile(File file) { // Process single file } void removeOutput(File file) { // Remove corresponding output file } } // Register task tasks.register('incrementalProcess', IncrementalTask) { inputDirectory.set(file('src/main/resources')) outputDirectory.set(file('build/processed')) }
Incremental Build Configuration
Enable Incremental Build
groovy// gradle.properties org.gradle.caching=true org.gradle.parallel=true
Task-level Configuration
groovytasks.withType(JavaCompile).configureEach { options.incremental = true } tasks.withType(Test).configureEach { // Test tasks support incremental builds by default }
Build Cache
Local Build Cache
groovy// gradle.properties org.gradle.caching=true
Remote Build Cache
groovybuildCache { local { enabled = true } remote(HttpBuildCache) { url = 'https://cache.example.com/cache/' enabled = true push = true // Allow pushing to cache credentials { username = 'user' password = 'password' } } }
Cache Task Outputs
groovytasks.register('expensiveTask') { outputs.cacheIf { true } // Enable caching doLast { // Execute expensive operations } }
Incremental Build Best Practices
1. Clearly Declare Inputs and Outputs
groovytasks.register('customTask') { // Clearly declare all inputs inputs.files('config.xml', 'properties.json') inputs.property('env', System.getenv('ENV')) // Clearly declare all outputs outputs.dir('build/output') doLast { // Task logic } }
2. Use File Trees
groovytasks.register('processResources') { inputs.dir('src/main/resources').withPropertyName('resources') outputs.dir('build/resources').withPropertyName('output') doLast { copy { from inputs.dir into outputs.dir } } }
3. Avoid Unnecessary Inputs
groovytasks.register('compileJava') { // Only include necessary input files inputs.files(fileTree('src/main/java') { include '**/*.java' exclude '**/generated/**' }) outputs.dir('build/classes') }
4. Use Property Inputs
groovytasks.register('generateConfig') { inputs.property('database.url', project.findProperty('db.url')) inputs.property('database.username', project.findProperty('db.username')) outputs.file('build/config/application.properties') doLast { // Generate configuration file } }
Debugging Incremental Builds
View Task Status
bash# View if task is UP-TO-DATE ./gradlew build --info # View detailed incremental build information ./gradlew build --debug
Force Re-execution of Tasks
bash# Force re-execution of specific task ./gradlew clean build # Force re-execution of tasks (without cleaning output) ./gradlew build --rerun-tasks # Force re-execution of specific task ./gradlew :app:compileJava --rerun-tasks
Analyze Build Performance
bash# Generate build report ./gradlew build --scan # View task execution time ./gradlew build --profile
Common Issues and Solutions
1. Task Always Re-executes
Problem: Task doesn't correctly declare inputs and outputs
Solution:
groovytasks.register('problemTask') { // Ensure all inputs are declared inputs.files('input.txt') inputs.property('version', project.version) // Ensure all outputs are declared outputs.dir('build/output') }
2. Output Files Modified Externally
Problem: Output files modified by other processes, causing cache invalidation
Solution:
groovytasks.register('sensitiveTask') { outputs.upToDateWhen { // Custom UP-TO-DATE check logic true } }
3. Incremental Build Not Working
Problem: Task doesn't support incremental builds
Solution:
groovy// Use @Incremental annotation abstract class MyIncrementalTask extends DefaultTask { @Incremental @InputDirectory abstract DirectoryProperty getInputDir() @OutputDirectory abstract DirectoryProperty getOutputDir() }
Performance Optimization Recommendations
- Enable build cache: Significantly improve speed of repeated builds
- Use parallel builds: Leverage multi-core CPUs to accelerate builds
- Optimize task dependencies: Reduce unnecessary task executions
- Use incremental compilation: Java compilation tasks support by default
- Avoid time-consuming operations in configuration phase: Move logic to execution phase
- Use configuration cache: Reduce configuration time
- Reasonably use incremental task API: Particularly effective for file processing tasks