本文首发于知乎专栏:详解Android Gradle生成字节码流程html
当前绝大部分的Android工程都是使用Gradle框架搭配Android Gradle Plugin(如下简称AGP)和Kotlin Gradle Plugin(如下简称KGP)进行编译构建的。虽然市面上有不少入门介绍,可是分析其中实现细节的文章并很少。这篇文章主要介绍了AGP和KGP生成字节码的核心流程,经过这些介绍,读者将了解到Java类和Kotlin类是如何被编译为字节码的,并学习到一些加快编译速度的最佳实践。java
为了加深理解字节码的生成过程,读者须要以下一些背景知识:android
学习Gradle基础,能够参考深刻理解Android之Gradle和【Android 修炼手册】Gradle 篇 -- Gradle 的基本使用这两篇文章,重点掌握如下几点:git
AGP是谷歌团队为了支持Android工程构建所开发的插件。AGP在Gradle的基础上,新增了一些与Android工程构建相关的Task。AGP的基本构建流程能够参考【Android 修炼手册】Android Gradle Plugin 插件主要流程。若是咱们在工程中也使用了Kotlin语言来开发,则须要依赖KGP插件来编译Kotlin文件。github
只有找到插件的入口类,才能分析插件的源码。Android项目中每一个子工程的build.gradle脚本文件经过apply引用的插件id,实际上也是该插件入口类声明的文件名。好比:“com.android.application”和“kotlin-android”插件的入口分别为AppPlugin和KotlinAndroidPluginWrapper,入口声明以下: 缓存
StudyGradleDemo是一个Demo工程,能够用于调试AGP和KGP编译过程,也能够用于阅读和分析AGP、KGP源码,读者可按需自行下载。session
Gradle调试方法能够参照官方教程Debugging build logic。总结来讲:app
compile(Flavor)JavaWithJavac框架
AndroidJavaCompileide
如上图所示:当编译Java类文件时,AndroidJavaCompile和JavaCompile首先作一些预处理操做,如校验注解类型,判断编译配置是否容许增量编译等。若是配置为增量编译,则使用SelectiveCompiler对输入作全量/增量的判断(注意并非全部的修改都会进行增量编译,有些修改可能会触发全量编译),这些判断是在JavaRecompilationSpecProvider的processClasspathChanges和processOtherChanges方法中完成。若是判断结果为全量编译,则直接走接下来的编译流程;若是判断结果为增量编译,还会进一步肯定修改的影响范围,并把全部受到影响的类都做为编译的输入,再走接下来的编译流程。最后的编译流程是使用JdkJavaCompiler执行编译任务,用javac将类文件编译为字节码。
这里给出了Java类文件生成字节码的核心调用链路(实现类和具体方法),读者可参考该调用链路自行翻阅源码。
/* ------ 编译java文件准备阶段 ------ */
-> AndroidJavaCompile.compile
-> JavaCompile.compile
/* ------ 两种编译方式可选,本例选择跟踪:增量编译 ------ */
-> JavaCompile.performCompilation
-> CompileJavaBuildOperationReportingCompiler.execute
-> IncrementalResultStoringCompiler.execute
-> SelectiveCompiler.execute
/* ------ 搜索增量编译范围 ------ */
-> JavaRecompilationSpecProvider.provideRecompilationSpec
-> JavaRecompilationSpecProvider.processOtherChanges
-> InputChangeAction.execute
-> SourceFileChangeProcessor.processChange
-> PreviousCompilation.getDependents
-> ClassSetAnalysis.getRelevantDependents
/* ------ 编译任务执行 ------ */
-> CleaningJavaCompilerSupport.execute
-> AnnotationProcessorDiscoveringCompiler.execute
-> NormalizingJavaCompiler.execute
-> JdkJavaCompiler.execute
-> JavacTaskImpl.call
-> JavacTaskImpl.doCall
/* ------ javac执行阶段 ------ */
-> Main.compile
-> JavaCompiler.compile
-> JavaCompiler.compile2
复制代码
compile(Flavor)JavaWithJavac任务的入口类是AndroidJavaCompile。运行时该类首先作了注解的校验工做,而后再将类文件编译字节码。本节将从注解处理,编译方式,字节码生成,JdkJavaCompiler的拓展设计四个方面进行介绍,其余环节请读者自行查阅源码。
为了高效开发,咱们每每会自定义一些注解来生成模板代码。在编译过程当中,处理注解有两种方式:一种是直接在compile(Flavor)JavaWithJavac的Task中处理,一种是建立独立的Task处理。独立的Task又分为ProcessAnnotationsTask和KaptTask两种。
BooleanOption.ENABLE_SEPARATE_ANNOTATION_PROCESSING
标志位;AndroidJavaCompile中处理注解的源码以下,当var3不为空时,在编译字节码前会先处理注解。
// com.sun.tool.javac.main.JavaCompiler.java
public void compile(List<JavaFileObject> var1, List<String> var2, Iterable<? extends Processor> var3) {
...
this.initProcessAnnotations(var3);
this.delegateCompiler = this.processAnnotations(this.enterTrees(this.stopIfError(CompileState.PARSE, this.parseFiles(var1))), var2);
...
}
复制代码
通常而言,咱们首次打开工程或者执行了clean project操做以后,编译器会把工程中的所有文件编译一次,把编译过程当中的一些中间产物进行缓存,即为全量编译。若是后面又触发了一次编译,编译器首先会把变化内容和以前缓存的内容作对比,找出全部须要从新编译的文件,而后只对这些文件进行从新编译,其余的仍然复用以前的缓存,即为增量编译。一般来说,增量编译的速度确定快于全量编译,平时开发过程当中,咱们用到更多的应该也是增量编译。
将Java类文件编译为字节码支持全量编译和增量编译两种方式。当编译配置支持增量编译时,AGP会在JavaRecompilationSpecProvider类的processClasspathChanges方法和processOtherChanges方法中拿当前输入的修改内容和以前缓存的编译内容作对比。下面给出了processOtherChanges方法的源码,能够看出AGP主要从源文件、注解处理器,资源等方面进行了对比。
// JavaRecompilationSpecProvider.java
private void processOtherChanges(CurrentCompilation current, PreviousCompilation previous, RecompilationSpec spec) {
SourceFileChangeProcessor javaChangeProcessor = new SourceFileChangeProcessor(previous);
AnnotationProcessorChangeProcessor annotationProcessorChangeProcessor = new AnnotationProcessorChangeProcessor(current, previous);
ResourceChangeProcessor resourceChangeProcessor = new ResourceChangeProcessor(current.getAnnotationProcessorPath());
InputChangeAction action = new InputChangeAction(spec, javaChangeProcessor, annotationProcessorChangeProcessor, resourceChangeProcessor, this.sourceFileClassNameConverter);
this.inputs.outOfDate(action);
this.inputs.removed(action);
}
复制代码
若是输入的修改内容知足了全量编译的条件,则会触发全量编译;不然会执行增量编译。全量/增量判断的示意图以下:
除了上述状况外,编译过程还有一个很是重要的概念:类的依赖链。举个例子:定义了一个类A,而后类B引用了类A,而后类C有使用类B的一个方法,而后类D又引用了类C,这样A-B-C-D就构成一条类的依赖链。假如类A被修改了,AGP会用递归的方式找出全部这个类A相关的类依赖链,本例中即为A-B-C-D。在获得整个类依赖链以后,AGP会把这个依赖链做为输入进行编译,如此一来,看似只是修改了一个类,实际被编译的多是多个类文件。若是依赖链复杂,只修改一个类却编译上千的类也不是不可能,这样就出现了compile(Flavor)JavaWithJavac很是耗时的状况。AGP中递归搜寻类的依赖链源码以下:
// ClassSetAnalysis.java
private void recurseDependentClasses(Set<String> visitedClasses, Set<String> resultClasses, Set<GeneratedResource> resultResources, Iterable<String> dependentClasses) {
Iterator var5 = dependentClasses.iterator();
while(var5.hasNext()) {
String d = (String)var5.next();
if (visitedClasses.add(d)) {
if (!this.isNestedClass(d)) {
resultClasses.add(d);
}
DependentsSet currentDependents = this.getDependents(d);
if (!currentDependents.isDependencyToAll()) {
resultResources.addAll(currentDependents.getDependentResources());
this.recurseDependentClasses(visitedClasses, resultClasses, resultResources, currentDependents.getDependentClasses());
}
}
}
}
复制代码
AGP为何不仅编译当前修改的类,而是要编译整个类依赖链呢?笔者认为这其实涉及到自动化编译中一个很是重要的问题:在通用场景下,自动化编译的自动化边界如何肯定?好比本例中:AGP如何知道被修改的文件是否会影响其下游?这个问题很难回答,一般须要结合具体的场景来分析。AGP做为一个通用的编译工具,首要考虑的应该是准确性,在保证准确性的基础上再考虑速度问题。因此AGP增量编译的方案编译了整个类的依赖链。在开发过程当中,咱们能够从实际场景出发,在速度和准确性方面作出必定的取舍,如:release包要发到线上必需要正确性,而debug阶段为了加快编译速度,尽快看到效果,不追求绝对正确性,这样就能够针对性的作出优化了。
在增量编译肯定了最终的输入类文件后,接下来的任务就是将类文件编译为字节码,即javac执行过程。AGP的javac过程最终是经过调用JDK 的Java Compiler API来实现的。javac将Java类编译成字节码文件须要通过语法解析、词法解析、语义解析、字节码生成四个步骤。以下图:
javac最终执行前须要提早作一些准备工做,如编译参数的校验,收集注解处理器;执行后也须要作一些处理工做,如对返回结果的封装,日志记录等;AGP使用了装饰模式来实现这一流程,下面是其中一层装饰的源码:
// DefaultJavaCompilerFactory.java
public Compiler<JavaCompileSpec> create(Class<? extends CompileSpec> type) {
Compiler<JavaCompileSpec> result = this.createTargetCompiler(type, false);
return new AnnotationProcessorDiscoveringCompiler(new NormalizingJavaCompiler(result), this.processorDetector);
}
// AnnotationProcessorDiscoveringCompiler.java
public class AnnotationProcessorDiscoveringCompiler<T extends JavaCompileSpec> implements Compiler<T> {
private final Compiler<T> delegate;
private final AnnotationProcessorDetector annotationProcessorDetector;
public AnnotationProcessorDiscoveringCompiler(Compiler<T> delegate, AnnotationProcessorDetector annotationProcessorDetector) {
this.delegate = delegate;
this.annotationProcessorDetector = annotationProcessorDetector;
}
public WorkResult execute(T spec) {
Set<AnnotationProcessorDeclaration> annotationProcessors = this.getEffectiveAnnotationProcessors(spec);
spec.setEffectiveAnnotationProcessors(annotationProcessors);
return this.delegate.execute(spec);
}
...
}
复制代码
咱们先分析DefaultJavaCompilerFactory类中的create方法,这个方法首先经过createTargetCompiler()方法建立了一个目标Compiler(debug能够发现是JdkJavaCompiler),而后将该目标Compiler做为构造参数建立了NormalizingJavaCompiler,最后将NormalizingJavaCompiler实例做为构造参数建立了AnnotationProcessorDiscoveringCompiler,并将该实例返回。这些Compiler类都继承了Compiler接口,最终负责执行的是接口中的execute方法。从AnnotationProcessorDiscoveringCompiler的execute方法中,咱们能够看到先执行了getEffectiveAnnotationProcessors方法去搜寻有效的注解处理器,最后调用了delegate的execute方法,也就是继续执行NormalizingJavaCompiler的execute方法,以此类推,最后再执行JdkJavaCompiler的execute方法。
因而可知,AGP在生成字节码的过程当中,建立了多层装饰来将核心的字节码生成功能和其余一些装饰功能区分开,这样设计能够简化核心Compiler类,也有了更好的拓展性,这种设计思路是咱们须要学习的一点。整个字节码生成过程当中Compiler装饰关系以下图所示:
compile(Flavor)Kotlin
KotlinCompile, CompileServiceImpl
如上图所示:编译Kotlin类文件时,先由KotlinCompile作一些准备工做,如建立临时输出文件等。而后启动编译服务CompileService,并在该服务的实现类CompileServiceImpl中完成全量编译和增量编译的判断工做,最后由K2JVMCompiler执行编译,用kotlinc将Kotlin类文件编译为字节码。
这里给出了Kotlin类文件生成字节码的核心调用链路(实现类和具体方法),读者可参考该调用链路自行翻阅源码。
/* ------ 编译kotlin文件准备阶段,配置环境及参数 ------ */
-> KotlinCompile.callCompilerAsync
-> GradleCompilerRunner.runJvmCompilerAsync
-> GradleCompilerRunner.runCompilerAsync
-> GradleKotlinCompilerWork.run
-> GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl
/* ------ 三种编译策略可选,本例选择跟踪:daemon策略 ------ */
-> GradleKotlinCompilerWork.compileWithDaemon
/* ------ 两种编译方式可选,本例选择跟踪:增量编译 ------ */
-> GradleKotlinCompilerWork.incrementalCompilationWithDaemon
/* ------ 启动编译服务 ------ */
-> CompileServiceImpl.compile
-> CompileServiceImplBase.compileImpl
-> CompileServiceImplBase.doCompile
/* ------ 执行增量编译 ------ */
-> CompileServiceImplBase.execIncrementalCompiler
-> IncrementalCompilerRunner.compile
-> IncrementalCompilerRunner.compileIncrementally
-> IncrementalJvmCompilerRunner.runCompiler
/* ------ kotlinc执行阶段 ------ */
-> CLITool.exec
-> CLICompiler.execImpl
-> K2JVMCompiler.doExecute
-> KotlinToJVMBytecodeCompiler.compileModules
复制代码
在AbstractAndroidProjectHandler类中有这样一段代码:
// AbstractAndroidProjectHandler.kt
internal fun configureJavaTask(kotlinTask: KotlinCompile, javaTask: AbstractCompile, logger: Logger) {
...
javaTask.dependsOn(kotlinTask)
...
}
复制代码
咱们能够看到Kotlin文件字节码编译是在Java文件字节码编译以前完成的。为何要把Kotlin编译放到Java编译以前呢?官方并无给出解释,因此这里的理解就仁者见仁智者见智了,一种比较合理的解释是:通常来说,语言的发展都是向前兼容的,即后来的语言会兼容以前语言的特性。咱们开发过程当中不少状况下都是Kotlin和Java代码相互之间混合调用的,因此理论上来说,若是Kotlin工程依赖了Java的Library工程应该是能够兼容并编译成功的,反过来若是Java工程依赖了Kotlin的Library工程可能就会出现不兼容的状况,因此应该先编译Kotlin的文件。
compile(Flavor)Kotlin任务的入口类是KotlinCompile,运行时该类首先作一些编译准备工做,如参数校验工做,而后再将类文件编译字节码。本节将重点介绍编译策略,编译方式,字节码生成三个部分的实现,其余部分请读者自行查阅源码。
从GradleKotlinCompilerWork类的compileWithDaemonOrFallbackImpl方法中,咱们能够看到在Kotlin文件编译过程当中,根据编译参数设置的不一样,有三种可选的编译策略:daemon, in-process, out-of-process。三种编译策略的差别主要体如今编译任务的运行方式上:
按笔者理解:daemon策略应该是编译最快的策略,out-of-process策略应该是编译最慢的策略,in-process策略应该介于这两个策略之间。由于一般来说,在Gradle开启编译流程前就已经启动了daemon进程,daemon策略下能够直接启动编译服务并执行编译过程,这样原进程也能够去并行执行其余任务,而且还支持增量编译;而out-of-process策略须要启动一个全新的进程,而且不支持增量编译,因此编译耗时应该最久;有时为了方便调试,能够考虑使用in-process策略。
那应该怎么配置编译策略呢?有两种配置方式:
在全局的gradle.property(注意:全局的gradle目录通常是/User/.gradle/gradle.property,gradle.property不存在时需新建,而非当前工程的gradle.property)下使用以下配置:
kotlin.compiler.execution.strategy=???(可选项:daemon/in-process/out-of-process)
org.gradle.daemon=???(可选项:true/false)
复制代码
在调试命令后增长调试参数,指定编译策略。示例以下:
> ./gradlew <task> -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy=in-process -Dorg.gradle.daemon=false
复制代码
和AGP同样,KGP一样支持增量编译和全量编译两种方式。编译过程是否采用增量编译主要取决于KotlinCompile类的incremental属性,该属性初始化时被设置为true,而且后续的编译过程并无修改该属性,因此KGP默认支持增量编译。增量编译的核心判断源码以下:
// KotlinCompile.kt
init {
incremental = true
}
// GradleKotlinCompilerWork.kt
private fun compileWithDaemon(messageCollector: MessageCollector): ExitCode? {
...
val res = if (isIncremental) {
incrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
} else {
nonIncrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
}
...
}
复制代码
同AGP同样,KGP会在IncrementalJvmCompilerRunner类的calculateSourcesToCompile方法中进行全量/增量编译的判断,知足全量编译的条件则会触发全量编译,不然会执行增量编译。全量/增量判断的示意图以下:
// IncrementalCompilerRunner.kt
private fun compileIncrementally(args: Args, caches: CacheManager, allKotlinSources: List<File>, compilationMode: CompilationMode, messageCollector: MessageCollector): ExitCode {
...
val complementaryFiles = caches.platformCache.getComplementaryFilesRecursive(dirtySources)
...
exitCode = runCompiler(sourcesToCompile.toSet(), args, caches, services, messageCollectorAdapter)
...
caches.platformCache.updateComplementaryFiles(dirtySources, expectActualTracker)
...
}
复制代码
肯定了最终输入后,接下来即是生成字节码,即kotlinc执行过程。执行kotlinc的入口是K2JVMCompiler的doExecute方法。这个方法首先会配置编译的参数,并作一些编译准备工做(好比建立临时文件夹和临时输出文件),准备工做结束后调用KotlinToJVMBytecodeCompiler的repeatAnalysisIfNeeded作词法分析、语法分析和语义分析,最后调用DefaultCodegenFactory的generateMultifileClass方法来生成字节码。Kotlin类文件生成字节码的流程图以下:
// PropertyCodegen.java
private void gen(@NotNull KtProperty declaration, @NotNull PropertyDescriptor descriptor, @Nullable KtPropertyAccessor getter, @Nullable KtPropertyAccessor setter) {
...
if (isAccessorNeeded(declaration, descriptor, getter, isDefaultGetterAndSetter)) {
generateGetter(descriptor, getter);
}
if (isAccessorNeeded(declaration, descriptor, setter, isDefaultGetterAndSetter)) {
generateSetter(descriptor, setter);
}
}
复制代码
经过上述分析,相信读者已经对Android工程中Java类文件和Kotlin类文件生成字节码的过程了然于胸了。下面咱们来总结一些最佳实践来避免本应增量编译却触发全量编译的状况发生,从而加快编译的速度。
增量编译失效,意味着本次修改将会进行全量编译,那么编译时间必然会增长,因此咱们应该从如下几个方面来改善咱们的代码:
BuildConfig中的itemValue若是存在动态变化的值,建议区分场景,如release包变,开发调试包不变;
将注解处理器修改成支持增量的注解处理器,修改方法请参考官网Incremental annotation processing;
若是类中有定义一些公有静态常量须要被外部引用,尝试改成静态方法去获取,而不是直接引用,例如:
public class Constants {
private static String TAG = "Constans";
// 暴露静态方法给外部引用
public static String getTag() {
return TAG;
}
}
复制代码
至此,Java类和Kotlin类生成字节码的流程就介绍完了,最后咱们来总结一下:编译Java类时,AGP经过AndroidJavaCompile先作一些预处理操做,而后进行全量/增量编译的判断,最终经过javac生成字节码。编译Kotlin类时,KGP经过KotlinCompile先作一些准备工做,而后进行全量/增量编译的判断,最终经过kotlinc生成字节码。最后,为了加快编译速度,本文给出了最佳实践。