Android编译解析-Java是如何编译的

最近研究了一些 Android 编译流程相关的东西。这里记录成文章分享给大家。今天先分享一下代码编译相关的细节。 Android 的代码编译包括 Java 和 kotlin 代码编译。本篇分析一下 Java 代码的编译流程。

编译流程

Android 应用的构建依赖于 Gradle 和 Android Gradle Plugin(AGP),而 Gradle 里面则包括了 Java Plugin: image.png 在 AGP 里面相关的 task 都会维护在 TaskManager 里面。编译相关的会在 createCompileTask 里面执行:

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
}
复制代码

负责 Java 编译的具体类定义在 JavaCompileCreationAction 中:

class JavaCompileCreationAction() {
	override val type: Class<JavaCompile>
    get() = JavaCompile::class.java
}
复制代码

JavaCompilecompile 方法负责具体的编译:

@TaskAction
protected void compile(InputChanges inputs) {
	DefaultJavaCompileSpec spec = createSpec();
	if (!compileOptions.isIncremental()) {
		performFullCompilation(spec);
	} else {
		performIncrementalCompilation(inputs, spec);
	}
}
复制代码

CompileOptions 包括增量编译的判断,当 isIncremental 为 true 的时候,表示支持增量。执行 performIncrementalCompilation。 这里通过 CompileJavaBuildOperationReportingCompilerexecute 方法执行 SelectiveCompilerexecute 方法。 在 SelectiveCompiler 运行的时候,会执行 Java 的编译,

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 {}
复制代码

这里会对 recompilationSpec 进行判断,如果某些条件破坏了增量编译,那么就会触发全量编译。 这里会调用到 compile 里创建的负责编译的对象: image.png 这里会跟到 DefaultToolchainJavaCompiler 方法: image.png 这个 Compiler 在 execute 的时候会创建最后执行编译的对象:

public <T extends CompileSpec> WorkResult execute(T spec) {
  final Class<T> specType = (Class<T>) spec.getClass();
	return compilerFactory.create(specType).execute(spec);
}
复制代码

compilerFactory 最后 create 得到的对象是 org.gradle.api.internal.tasks.compile.JdkJavaCompiler image.png 创建需要的 Task 运行执行编译。实际上这里调用到了 javac 的编译。

增量编译

那么 Java 是怎么判断如何进行增量编译,哪些情况会触发全量编译呢? 我们可以通过如下代码获取java编译task变化的文件:

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() // compileDebugJavaWithJavac
}
复制代码

state 里面存储了之前的文件,默认的对象是 DefaultAfterPreviousExecutionState, inputFileProperties 的泛型是 org.gradle.internal.fingerprint.FileCollectionFingerprint 这里能看出来Gradle是通过区分文件指纹来决定哪些文件变化了的,默认实现类是 DefaultCurrentFileCollectionFingerprint, 这个类内部存在一个 Hash 对象来计算文件的具体指纹: image.png newHasher 的默认方式是 MD5:

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

// default
private static final HashFunction DEFAULT = MD5;
复制代码

得到文件变化后还有一个问题就是类依赖问题,当一个A类的方法签名变化后,A的被依赖类B也会进行编译,效果如下: image.png

这里回到 SelectiveCompilerexecute :

RecompilationSpec recompilationSpec = recompilationSpecProvider.provideRecompilationSpec(currentCompilation, previousCompilation);
复制代码

这里的 provider 指的是 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;
}
复制代码

这里最关键的步骤就是处理 classpath 变化和文件变化:

classpath变化: image.png

文件变化: 文件变化逻辑类似 classpath image.pngimage.png 这里的 dependents 的相关方法就是获取依赖的类文件。也就是处理上面提到的增量编译的类依赖问题。 这里逻辑比较复杂,不需要过于深入纠结,从名字我们可以分析出来管理的依赖内容有依赖的class文件和资源文件。 值得注意的是,虽然 Gradle 有增量编译逻辑,但是在这里还是会有一些触发全量编译的流程,会触发 rebuildAllCompiler 的执行: image.png

  1. 当这个依赖是被所有依赖的时候,例如三方库依赖变化,会触发全量编译
if (dependents.isDependencyToAll()) {
	spec.setFullRebuildCause(dependents.getDescription());
  return;
}
复制代码
  1. 同理如果改动的源码符合这个条件也会触发全量,例如没有支持增量编译的 apt 就满足这个条件。会得到一个
must have exactly one originating element, but had 0
复制代码

的cause。如果我们在代码中各种使用编译时注解,则每次编译的时候都会触发全量编译。写到这里我们需要把 apt 使用这个情况单独拎出来看看。

apt编译

对于 apt 的处理,在 JdkJavaCompiler 里面可以得到体现,在创建的 JavaCompiler.CompilationTask 中,会针对真正编译前的逻辑进行一层层的包装。

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);
复制代码

其中就包括 AnnotationProcessing:

@Override
public Boolean call() {
  try {
    setupProcessors();
		return delegate.call();
	} finally {
		cleanupProcessors();
	}
}
复制代码

setupProcessors 则会通过反射创建我们的 Processor 并执行。当我们的 apt 支持增量编译的时候,我们会继续使用相应的包装类:

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 new NonIncrementalProcessor(processor, processorResult);
	}
}
复制代码

IsolationProcessor 为例: image.png process 里会根据不同的策略把 apt 的输入记录下来,供增量编译的时候使用。 关于这几种增量策略。可以在 Gradle 的文档里面找到:docs.gradle.org/5.0/usergui… 这个是 Gradle 5开始支持的功能。 这里简单介绍下这几种增量apt:

  • isolatiing 独立搜索每个注解标记的元素
  • aggregating 多个源文件聚合到一个或者多个输出文件
  • dynamic 动态决定是isolating还是aggregating

总结

到这里 Java 编译的大致流程就分析的差不多了。其中很多东西可以更加深入的研究,感兴趣的朋友可以自行研究。 其中比较有用的一点就是在日常使用 apt 的时候,我们需要重视 apt 增量编译的重视,防止因为apt太多导致的工程编译速度劣化。

猜你喜欢

转载自juejin.im/post/7086096930308096014
今日推荐