I’ve been looking at a few things about the Android build process. Here is a record of the article to share with you. Today, I’ll share some details about how to compile the code. Android code compilation includes Java and Kotlin code compilation. This article looks at the compilation process of Java code.

The compilation process

Android applications are built using Gradle and the Android Gradle Plugin (AGP). Gradle contains the Java Plugin:Related tasks are maintained in AGPTaskManagerThe inside. Compile-related will be increateCompileTaskExecute inside:

private void createCompileTask(@NonNull VariantImpl variant) {
	ApkCreationConfig apkCreationConfig = (ApkCreationConfig) variant;

	TaskProvider<? extends JavaCompile> javacTask = createJavacTask(variant);
	addJavacClassesStream(variant);
	setJavaCompilerTask(javacTask, variant);
	createPostCompilationTasks(apkCreationConfig);
}

// createJavacTask
fun createJavacTask(creationConfig: ComponentCreationConfig): TaskProvider<out JavaCompile> {
	taskFactory.register(JavaPreCompileTask.CreationAction(creationConfig))
	val javacTask: TaskProvider<out JavaCompile> = taskFactory.register(
	JavaCompileCreationAction(creationConfig,project.pluginManager.hasPlugin(KOTLIN_KAPT_PLUGIN_ID)))
	postJavacCreation(creationConfig)
	return javacTask
}
Copy the code

Responsible for the Java compiler specific class definition in JavaCompileCreationAction:

class JavaCompileCreationAction(a){
	override val type: Class<JavaCompile>
    get() = JavaCompile::class.java
}
Copy the code

The JavaCompile compile method is responsible for the specific compilation:

@TaskAction
protected void compile(InputChanges inputs) {
	DefaultJavaCompileSpec spec = createSpec();
	if(! compileOptions.isIncremental()) { performFullCompilation(spec); }else{ performIncrementalCompilation(inputs, spec); }}Copy the code

CompileOptions includes incremental compilation. When isIncremental is true, incremental is supported. Perform performIncrementalCompilation. Here by the execute method the implementation CompileJavaBuildOperationReportingCompiler SelectiveCompiler the execute method. When SelectiveCompiler runs, it does Java compilation,

CurrentCompilation currentCompilation = new CurrentCompilation(spec, classpathSnapshotProvider);
RecompilationSpec recompilationSpec = recompilationSpecProvider.provideRecompilationSpec(currentCompilation, previousCompilation);
if (recompilationSpec.isFullRebuildNeeded()) {
  return rebuildAllCompiler.execute(spec);
}

try {
	WorkResult result = recompilationSpecProvider.decorateResult(recompilationSpec, cleaningCompiler.getCompiler().execute(spec));
  return result.or(WorkResults.didWork(cleanedOutput));
} finally {}
Copy the code

Here torecompilationSpecDetermine that if some condition breaks incremental compilation, then full compilation is triggered. This is going to be calledcompileObject created for compilation in:We’ll follow you hereDefaultToolchainJavaCompilerMethods:This Compiler creates the object to be compiled at execute:

public <T extends CompileSpec> WorkResult execute(T spec) {
  final Class<T> specType = (Class<T>) spec.getClass();
	return compilerFactory.create(specType).execute(spec);
}
Copy the code

The last object compilerFactory create gets isorg.gradle.api.internal.tasks.compile.JdkJavaCompiler Create the required Task to run to perform compilation. Actually it’s called herejavacThe compilation.

Incremental compilation

So how does Java determine how to do incremental compilation, and which situations trigger full compilation? We can get the Java compiled task change file with the following code:

val services = (project as? ProjectInternal)? .services services? .let { val store = it.get(ExecutionHistoryStore::class.java)
  val detector = it.get(ExecutionStateChangeDetector::class.java)
	val lastState = store.load(":${project.name}:${Task_Name_Java}").get(a)// compileDebugJavaWithJavac
}
Copy the code

State stores the previous file, and the default object isDefaultAfterPreviousExecutionState.inputFilePropertiesGenerics areorg.gradle.internal.fingerprint.FileCollectionFingerprintGradle uses file fingerprints to determine which files have changed. The default implementation class isDefaultCurrentFileCollectionFingerprintThere is one inside this classHashObject to calculate the specific fingerprint of a file: newHasherThe default is MD5:

public static Hasher newHasher() {
	return DEFAULT.newHasher();
}

// default
private static final HashFunction DEFAULT = MD5;
Copy the code

When A method signature of class A changes, A dependent class B will be compiled as well. The result is as follows:

So let’s go back to execute for SelectiveCompiler:

RecompilationSpec recompilationSpec = recompilationSpecProvider.provideRecompilationSpec(currentCompilation, previousCompilation);
Copy the code

In this case, the provider is JavaRecompilationSpecProvider:

@Override
public RecompilationSpec provideRecompilationSpec(CurrentCompilation current, PreviousCompilation previous) {
  RecompilationSpec spec = new RecompilationSpec(previous);
  SourceFileClassNameConverter sourceFileClassNameConverter = getSourceFileClassNameConverter(previous);
  
  processClasspathChanges(current, previous, spec);
  processOtherChanges(current, previous, spec, sourceFileClassNameConverter);
  
  Set<String> typesToReprocess = previous.getTypesToReprocess();
  spec.addClassesToProcess(typesToReprocess);
  return spec;
}
Copy the code

The most critical step here is to handle classpath changes and file changes:

Changes in the classpath:

File changes: File change logic is similar to classpathHere,dependentsThe related method is to get the dependent class file. That is, dealing with the class dependencies of incremental compilation mentioned above. Here the logic is more complicated, do not need to be too in-depth tangled, from the name we can analyze the dependency content of the dependency class file and resource file. It is important to note that Gradle has incremental compilation logic, but there are still some processes that trigger full compilationrebuildAllCompilerThe execution of the:

  1. Full compilation is triggered when the dependency is all dependent, such as a tripartite library dependency change
if (dependents.isDependencyToAll()) {
	spec.setFullRebuildCause(dependents.getDescription());
  return;
}
Copy the code
  1. Similarly, if the modified source code meets this condition, it will also trigger full, such as APT that does not support incremental compilation. You’re going to get a
must have exactly one originating element, but had 0
Copy the code

Of cause. If we use compile-time annotations in our code in a variety of ways, full compilation will be triggered every time we compile. So here we need to separate out the use of APT.

Apt to compile

For management of apt in JdkJavaCompiler can be reflected, in creating JavaCompiler.Com pilationTask, for real before compilation logic layers of packaging.

JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options, spec.getClasses(), compilationUnits);
if (compiler instanceof IncrementalCompilationAwareJavaCompiler) {
  task = ((IncrementalCompilationAwareJavaCompiler) compiler).makeIncremental(task, result.getSourceClassesMapping(), result.getConstantsAnalysisResult(), new CompilationSourceDirs(spec));
}
task = new AnnotationProcessingCompileTask(task, annotationProcessors, spec.getAnnotationProcessorPath(), result.getAnnotationProcessingResult());
task = new ResourceCleaningCompilationTask(task, fileManager);
Copy the code

These include AnnotationProcessing:

@Override
public Boolean call() {
  try {
    setupProcessors();
		return delegate.call();
	} finally{ cleanupProcessors(); }}Copy the code

SetupProcessors create our Processor by reflection and execute it. When our APT supports incremental compilation, we continue to use the corresponding wrapper class:

private Processor decorateForIncrementalProcessing(Processor processor, IncrementalAnnotationProcessorType type, AnnotationProcessorResult processorResult) {
	switch (type) {
		case ISOLATING:
			return new IsolatingProcessor(processor, processorResult);
		case AGGREGATING:
			return new AggregatingProcessor(processor, processorResult);
		case DYNAMIC:
			return new DynamicProcessor(processor, processorResult);
		default:
			return newNonIncrementalProcessor(processor, processorResult); }}Copy the code

In order toIsolationProcessorFor example:Apt inputs are recorded in process according to different strategies for incremental compilation. About these incremental strategies. It can be found in the Gradle documentation:Docs.gradle.org/5.0/usergui…This is what Gradle 5 is starting to support. Here is a brief introduction to these incremental APT:

  • Isolatiing searches the elements of each annotation tag independently
  • Aggregating Multiple source files to one or more output files
  • Can determine day or aggregating

conclusion

Here is the Java compilation of the general process on the analysis of the almost. Many of these things can be further studied, interested friends can study on their own. One of the more useful points is that in daily use of APT, we need to pay attention to apt incremental compilation to prevent the deterioration of project compilation speed caused by too many APT.