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

2022-05-10 20:51:32 浏览数 (1)

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

编译流程

Android 应用的构建依赖于 Gradle 和 Android Gradle Plugin(AGP),而 Gradle 里面则包括了 Java Plugin:

在 AGP 里面相关的 task 都会维护在 TaskManager 里面。编译相关的会在 createCompileTask 里面执行:

代码语言:javascript复制
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 中:

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

JavaCompilecompile 方法负责具体的编译:

代码语言:javascript复制
@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 的编译,

代码语言:javascript复制
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:

代码语言:javascript复制
public static Hasher newHasher() {
 return DEFAULT.newHasher();
}

// default
private static final HashFunction DEFAULT = MD5;

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

这里回到 SelectiveCompilerexecute :

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

这里的 provider 指的是 JavaRecompilationSpecProvider:

代码语言:javascript复制
@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 的执行:

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

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

apt编译

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

代码语言:javascript复制
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:

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

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

代码语言:javascript复制
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太多导致的工程编译速度劣化。

0 人点赞