最近研究了一些 Android 编译流程相关的东西。这里记录成文章分享给大家。今天先分享一下代码编译相关的细节。Android 的代码编译包括 Java 和 kotlin 代码编译。本篇分析一下 Java 代码的编译流程。
编译流程
Android 应用的构建依赖于 Gradle 和 Android Gradle Plugin(AGP),而 Gradle 里面则包括了 Java Plugin:
在 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
}
JavaCompile
的 compile
方法负责具体的编译:
@TaskAction
protected void compile(InputChanges inputs) {
DefaultJavaCompileSpec spec = createSpec();
if (!compileOptions.isIncremental()) {
performFullCompilation(spec);
} else {
performIncrementalCompilation(inputs, spec);
}
}
CompileOptions
包括增量编译的判断,当 isIncremental
为 true 的时候,表示支持增量。执行 performIncrementalCompilation
。这里通过 CompileJavaBuildOperationReportingCompiler
的 execute
方法执行 SelectiveCompiler
的 execute
方法。在 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
里创建的负责编译的对象:
这里会跟到 DefaultToolchainJavaCompiler
方法:
这个 Compiler 在 execute 的时候会创建最后执行编译的对象:
代码语言:javascript复制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
创建需要的 Task 运行执行编译。实际上这里调用到了 javac
的编译。
增量编译
那么 Java 是怎么判断如何进行增量编译,哪些情况会触发全量编译呢?我们可以通过如下代码获取java编译task变化的文件:
代码语言:javascript复制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
对象来计算文件的具体指纹:
newHasher
的默认方式是 MD5:
public static Hasher newHasher() {
return DEFAULT.newHasher();
}
// default
private static final HashFunction DEFAULT = MD5;
得到文件变化后还有一个问题就是类依赖问题,当一个A类的方法签名变化后,A的被依赖类B也会进行编译,效果如下:
这里回到 SelectiveCompiler
的 execute
:
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变化:
文件变化: 文件变化逻辑类似 classpath
这里的 dependents
的相关方法就是获取依赖的类文件。也就是处理上面提到的增量编译的类依赖问题。这里逻辑比较复杂,不需要过于深入纠结,从名字我们可以分析出来管理的依赖内容有依赖的class文件和资源文件。值得注意的是,虽然 Gradle 有增量编译逻辑,但是在这里还是会有一些触发全量编译的流程,会触发 rebuildAllCompiler
的执行:
- 当这个依赖是被所有依赖的时候,例如三方库依赖变化,会触发全量编译
if (dependents.isDependencyToAll()) {
spec.setFullRebuildCause(dependents.getDescription());
return;
}
- 同理如果改动的源码符合这个条件也会触发全量,例如没有支持增量编译的 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
为例:
process 里会根据不同的策略把 apt 的输入记录下来,供增量编译的时候使用。关于这几种增量策略。可以在 Gradle 的文档里面找到:https://docs.gradle.org/5.0/userguide/java_plugin.html#sec:incremental_annotation_processing这个是 Gradle 5开始支持的功能。这里简单介绍下这几种增量apt:
- isolatiing 独立搜索每个注解标记的元素
- aggregating 多个源文件聚合到一个或者多个输出文件
- dynamic 动态决定是isolating还是aggregating
总结
到这里 Java 编译的大致流程就分析的差不多了。其中很多东西可以更加深入的研究,感兴趣的朋友可以自行研究。其中比较有用的一点就是在日常使用 apt 的时候,我们需要重视 apt 增量编译的重视,防止因为apt太多导致的工程编译速度劣化。