前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android编译解析 - Java是如何编译的

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

作者头像
烧麦程
发布2022-05-10 20:51:32
7500
发布2022-05-10 20:51:32
举报
文章被收录于专栏:半行代码半行代码

最近研究了一些 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太多导致的工程编译速度劣化。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-04-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 半行代码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 编译流程
  • 增量编译
  • apt编译
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档