prefacio
Compilar y ejecutar es una Android
tarea que un desarrollador tiene que hacer todos los días. La compilación incremental también es extremadamente importante para los desarrolladores. La compilación incremental con una alta tasa de aciertos puede mejorar en gran medida la eficiencia de desarrollo y la experiencia de los desarrolladores.
Escribí algunos artículos antes para presentar Kotlin
el principio de la compilación incremental y el Kotlin 1.7
soporte para la compilación incremental entre módulos.
Después de comprender estos principios básicos, echemos Kotlin
un vistazo al código fuente de la compilación incremental hoy para ver cómo Kotlin
se implementa la compilación incremental.
conocimiento previo
Echemos un vistazo a la tecnología negra detrás de la compilación rápida de Kotlin ~
Nuevas funciones en Kotlin 1.7: se abandona la compatibilidad con la compilación incremental entre módulos
Transform, echemos un vistazo a TransformAction ~
Es principalmente Kotlin
una introducción al principio de compilación incremental, y debido a que se usa en el código fuente TransformAction
, también es necesario comprender TransformAction
el uso básico .
Proceso de compilación incremental
Paso 1: Compile la entrada
Si queremos usarlo en el proyecto Kotlin
, debemos agregar un org.jetbrains.kotlin.android
complemento.Este complemento es Kotlin
el punto de entrada de nuestra compilación, y su código está en el kotlin-gradle-plugin
complemento.
La clase de implementación de este complemento es KotlinAndroidPluginWrapper
, se puede ver que KotlinAndroidPluginWrapper
es un paquete, que es principalmente para crear y configurarKotlinAndroidPlugin
Paso 2: ConfiguraciónKotlinAndroidPlugin
KotlinAndroidPlugin
Es la entrada real del complemento, donde compileKotlin Task
se realiza el trabajo de configuración relacionado.
internal open class KotlinAndroidPlugin(
private val registry: ToolingModelBuilderRegistry
) : Plugin<Project> {
override fun apply(project: Project) {
checkGradleCompatibility()
project.dynamicallyApplyWhenAndroidPluginIsApplied()
}
private fun preprocessVariant(
variantData: BaseVariant,
compilation: KotlinJvmAndroidCompilation,
project: Project,
rootKotlinOptions: KotlinJvmOptionsImpl,
tasksProvider: KotlinTasksProvider
) {
val configAction = KotlinCompileConfig(compilation)
configAction.configureTask { task ->
task.useModuleDetection.value(true).disallowChanges()
// 将kotlin 编译结果存储在tmp/kotlin-classes/$variantDataName目录下,会作为java compiler的class-path输入
task.destinationDirectory.set(project.layout.buildDirectory.dir("tmp/kotlin-classes/$variantDataName"))
}
tasksProvider.registerKotlinJVMTask(project, compilation.compileKotlinTaskName, compilation.kotlinOptions, configAction)
}
}
复制代码
Se omite parte del código, principalmente para hacer algunas cosas:
- Verifique la compatibilidad
KGP
conGradle
la versión, si no, lance una excepción y cancele la compilación - Si el complemento
project
ya se ha agregadoandroid
, comience a configurar elkotlin-android
complemento - 通过
KotlinCompileConfig
来配置KotlinCompile Task
,设置destinationDirectory
作为Kotlin
编译结果存储目录,后续会作为java compiler
的classpath
输入
第三步:配置KotlinCompile
的输入输出
要实现增量编译,最重要的一点就是配置输入输出,当输入输出没有发生变化时,Task
就可以被跳过,而KotlinCompile
输入输出的配置,主要是在KotlinCompileConfig
中完成的
configureTaskProvider { taskProvider ->
// 是否开启classpathSnapthot
val useClasspathSnapshot = propertiesProvider.useClasspathSnapshot
val classpathConfiguration = if (useClasspathSnapshot) {
// 注册 Transform
registerTransformsOnce(project)
project.configurations.detachedConfiguration(
project.dependencies.create(objectFactory.fileCollection().from(project.provider { taskProvider.get().libraries }))
)
} else null
taskProvider.configure { task ->
// 配置输入属性
task.classpathSnapshotProperties.useClasspathSnapshot.value(useClasspathSnapshot).disallowChanges()
if (useClasspathSnapshot) {
// 通过TransformAction读取输入
val classpathEntrySnapshotFiles = classpathConfiguration!!.incoming.artifactView {
it.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, CLASSPATH_ENTRY_SNAPSHOT_ARTIFACT_TYPE)
}.files
task.classpathSnapshotProperties.classpathSnapshot.from(classpathEntrySnapshotFiles).disallowChanges()
task.classpathSnapshotProperties.classpathSnapshotDir.value(getClasspathSnapshotDir(task)).disallowChanges()
} else {
task.classpathSnapshotProperties.classpath.from(task.project.provider { task.libraries }).disallowChanges()
}
}
}
复制代码
可以看出,主要做了这么几件事
- 判断是否开启了
classpathSnapthot
,这也是支持跨模块增量编译的开关,如果开启了就注册Transform
- 通过
TransformAction
获取输入,并配置给Task
相应的属性
下面我们着重来看下TransformAction
在这里做了什么工作?
第四步:跨模块增量编译支持
private fun registerTransformsOnce(project: Project) {
val buildMetricsReporterService = BuildMetricsReporterService.registerIfAbsent(project)
project.dependencies.registerTransform(ClasspathEntrySnapshotTransform::class.java) {
it.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, JAR_ARTIFACT_TYPE)
it.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, CLASSPATH_ENTRY_SNAPSHOT_ARTIFACT_TYPE)
}
project.dependencies.registerTransform(ClasspathEntrySnapshotTransform::class.java) {
it.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, DIRECTORY_ARTIFACT_TYPE)
it.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, CLASSPATH_ENTRY_SNAPSHOT_ARTIFACT_TYPE)
}
}
复制代码
了解了前置知识中的TransformAction
,可以看出这就是注册了只变换ArtifactType
的变换,主要涉及JAR_ARTIFACT_TYPE
和DIRECTORY_ARTIFACT_TYPE
转换为CLASSPATH_ENTRY_SNAPSHOT_ARTIFACT_TYPE
也就是说依赖的jar
和类目录都会转换为CLASSPATH_ENTRY_SNAPSHOT_ARTIFACT_TYPE
类型,也就可以获取我们依赖的所有classpath
的abi
了
接下来我们看下ClasspathEntrySnapshotTransform
的实现
ClasspathEntrySnapshotTransform
实现
abstract class ClasspathEntrySnapshotTransform : TransformAction<ClasspathEntrySnapshotTransform.Parameters> {
@get:Classpath
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>
override fun transform(outputs: TransformOutputs) {
val classpathEntryInputDirOrJar = inputArtifact.get().asFile
val snapshotOutputFile = outputs.file(classpathEntryInputDirOrJar.name.replace('.', '_') + "-snapshot.bin")
val granularity = getClassSnapshotGranularity(classpathEntryInputDirOrJar, parameters.gradleUserHomeDir.get().asFile)
val snapshot = ClasspathEntrySnapshotter.snapshot(classpathEntryInputDirOrJar, granularity, metrics)
ClasspathEntrySnapshotExternalizer.saveToFile(snapshotOutputFile, snapshot)
}
/**
* 如果是anroid.jar或者aar依赖,粒度为class, 否则为class_member_level
/
private fun getClassSnapshotGranularity(classpathEntryDirOrJar: File, gradleUserHomeDir: File): ClassSnapshotGranularity {
return if (
classpathEntryDirOrJar.startsWith(gradleUserHomeDir) ||
classpathEntryDirOrJar.name == "android.jar"
) CLASS_LEVEL
else CLASS_MEMBER_LEVEL
}
}
复制代码
关于自定义TransformAction
,其实跟Task
一样,也主要看3个部分,输入,输出,执行方法体
ClasspathEntrySnapshotTransform
的输入就是模块依赖的jar
或者文件目录- 输出则是以
-snapshot.bin
结尾的文件 - 方法体只做了一件事,通过
ClasspathEntrySnapshotter
计算出claspath
的快照并保存,如果是aar
依赖,计算的粒度为class
,如果是项目内的类,计算的粒度是class_member_level
ClasspathEntrySnapshotter
内部是如何计算classpath
快照的我们这就不看了,我们简单看下下面这样一个类计算的快照是怎样的
class MyTest {
fun startTest(text: String) {
println(text)
test1(1)
}
private fun test1(index: Int) {
println("here test126$index")
}
}
复制代码
MyTest
类计算出来的快照如图所示,主要classId
,classAbiHash
,classHeaderStrings
等内容
可以看出private
函数的声明也是abi
的一部分,当public
或者private
的函数声明发生变化时,classAbiHash
都会发生变化,而只修改函数体时,snapshot
不会发生任何变化。
第五步:KotlinCompile Task
执行编译
在配置完成之后,接下来我们就来看下KotlinCompile
是怎么执行编译的
abstract class KotlinCompile @Inject constructor(
override val kotlinOptions: KotlinJvmOptions,
workerExecutor: WorkerExecutor,
private val objectFactory: ObjectFactory
) : AbstractKotlinCompile<K2JVMCompilerArguments>(objectFactory {
// classpathSnapshot入参
@get:Nested
abstract val classpathSnapshotProperties: ClasspathSnapshotProperties
abstract class ClasspathSnapshotProperties {
@get:Classpath
@get:Incremental
@get:Optional // Set if useClasspathSnapshot == true
abstract val classpathSnapshot: ConfigurableFileCollection
}
// 增量编译参数
override val incrementalProps: List<FileCollection>
get() = listOf(
sources,
javaSources,
classpathSnapshotProperties.classpathSnapshot
)
override fun callCompilerAsync(inputChanges: InputChanges) {
// 获取增量编译环境变量
val icEnv = if (isIncrementalCompilationEnabled()) {
IncrementalCompilationEnvironment(
changedFiles = getChangedFiles(inputChanges, incrementalProps),
classpathChanges = getClasspathChanges(inputChanges),
)
} else null
val environment = GradleCompilerEnvironment(incrementalCompilationEnvironment = icEnv)
compilerRunner.runJvmCompilerAsync(
(kotlinSources + scriptSources).toList(),
commonSourceSet.toList(),
javaSources.files,
environment,
)
}
// 查找改动了的input
protected fun getChangedFiles(
inputChanges: InputChanges,
incrementalProps: List<FileCollection>
) = if (!inputChanges.isIncremental) {
ChangedFiles.Unknown()
} else {
incrementalProps
.fold(mutableListOf<File>() to mutableListOf<File>()) { (modified, removed), prop ->
inputChanges.getFileChanges(prop).forEach {
when (it.changeType) {
ChangeType.ADDED, ChangeType.MODIFIED -> modified.add(it.file)
ChangeType.REMOVED -> removed.add(it.file)
else -> Unit
}
}
modified to removed
}
.run {
ChangedFiles.Known(first, second)
}
}
// 查找改变了的classpath
private fun getClasspathChanges(inputChanges: InputChanges): ClasspathChanges = when {
!classpathSnapshotProperties.useClasspathSnapshot.get() -> ClasspathSnapshotDisabled
else -> {
when {
!inputChanges.isIncremental -> NotAvailableForNonIncrementalRun(classpathSnapshotFiles)
inputChanges.getFileChanges(classpathSnapshotProperties.classpathSnapshot).none() -> NoChanges(classpathSnapshotFiles)
!classpathSnapshotFiles.shrunkPreviousClasspathSnapshotFile.exists() -> {
NotAvailableDueToMissingClasspathSnapshot(classpathSnapshotFiles)
}
else -> ToBeComputedByIncrementalCompiler(classpathSnapshotFiles)
}
}
}
}
复制代码
对于KotlinCompile
,我们也可以从入参,出参,TaskAction
的角度来分析
classpathSnapshotProperties
是个包装类型的输入,内部包括@Classpath
类型的输入,使用@Classpath
输入时,如果输入文件名发生变化而内容没有发生变化时,不会触发Task
重新运行,这对classpath
来说非常重要incrementalProps
是组件后的增量编译输入参数,包括kotlin
输入,java
输入,classpath
输入等CompileKotlin
的TaskAction
,它最后会执行到callCompilerAsync
方法,在其中通过getChangedFiles
与getClasspathChanges
获取改变了的输入与classpath
getClasspathChanges
方法通过inputChanges
获取一个已经改变与删除的文件的Pair
getClasspathChanges
则根据增量编译是否开启,是否有文件发生更改,历史snapshotFile
是否存在,返回不同的ClassPathChanges
密封类
在增量编译参数拼装完成后,接下来就是跟着逻辑走,最后会走到GradleKotlinCompilerWork
的 compileWithDaemmonOrFailbackImpl
private fun compileWithDaemonOrFallbackImpl(messageCollector: MessageCollector): ExitCode {
val executionStrategy = kotlinCompilerExecutionStrategy()
if (executionStrategy == DAEMON_EXECUTION_STRATEGY) {
val daemonExitCode = compileWithDaemon(messageCollector)
if (daemonExitCode != null) {
return daemonExitCode
}
}
val isGradleDaemonUsed = System.getProperty("org.gradle.daemon")?.let(String::toBoolean)
return if (executionStrategy == IN_PROCESS_EXECUTION_STRATEGY || isGradleDaemonUsed == false) {
compileInProcess(messageCollector)
} else {
compileOutOfProcess()
}
}
复制代码
可以看出,kotlin
编译有三种策略,分别是
- 守护进程编译:
Kotlin
编译的默认模式,只有这种模式才支持增量编译,可以在多个Gradle daemon
进程间共享 - 进程内编译:
Gradle daemon
进程内编译 - 进程外编译:每次编译都是在不同的进程
compileWithDaemon
会调用到 Kotlin Compile
里执行真正的编译逻辑:
val exitCode = try {
val res = if (isIncremental) {
incrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
} else {
nonIncrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
}
} catch (e: Throwable) {
null
}
复制代码
到这里会执行 org.jetbrains.kotlin.daemon.CompileServiceImpl
的 compile
方法,这样就终于调到了Kotlin
编译器内部
第六步:Kotlin
编译器计算出需重编译的文件
经过这么多步骤,终于走到Kotlin
编译器内部了,下面我们来看下Kotlin
编译器的增量编译逻辑
protected inline fun <ServicesFacadeT, JpsServicesFacadeT, CompilationResultsT> compileImpl(){
//...
CompilerMode.INCREMENTAL_COMPILER -> {
when (targetPlatform) {
CompileService.TargetPlatform.JVM -> withIC(k2PlatformArgs) {
doCompile(sessionId, daemonReporter, tracer = null) { _, _ ->
execIncrementalCompiler(
k2PlatformArgs as K2JVMCompilerArguments,
gradleIncrementalArgs,
//...
)
}
}
}
复制代码
如上代码,会判断输入的编译参数,如果是增量编译并且是JVM
平台的话,就会执行execIncrementalCompiler
方法,最后会调用到sourcesToCompile
方法
private fun sourcesToCompile(
caches: CacheManager,
changedFiles: ChangedFiles,
args: Args,
messageCollector: MessageCollector,
dependenciesAbiSnapshots: Map<String, AbiSnapshot>
): CompilationMode =
when (changedFiles) {
is ChangedFiles.Known -> calculateSourcesToCompile(caches, changedFiles, args, messageCollector, dependenciesAbiSnapshots)
is ChangedFiles.Unknown -> CompilationMode.Rebuild(BuildAttribute.UNKNOWN_CHANGES_IN_GRADLE_INPUTS)
is ChangedFiles.Dependencies -> error("Unexpected ChangedFiles type (ChangedFiles.Dependencies)")
}
private fun calculateSourcesToCompileImpl(
caches: IncrementalJvmCachesManager,
changedFiles: ChangedFiles.Known,
args: K2JVMCompilerArguments,
abiSnapshots: Map<String, AbiSnapshot> = HashMap(),
withAbiSnapshot: Boolean
): CompilationMode {
val dirtyFiles = DirtyFilesContainer(caches, reporter, kotlinSourceFilesExtensions)
// 初始化dirtyFiles
initDirtyFiles(dirtyFiles, changedFiles)
// 计算变化的classpath
val classpathChanges = when (classpathChanges) {
is NoChanges -> ChangesEither.Known(emptySet(), emptySet())
// classpathSnapshot可用时
is ToBeComputedByIncrementalCompiler -> reporter.measure(BuildTime.COMPUTE_CLASSPATH_CHANGES) {
computeClasspathChanges(
classpathChanges.classpathSnapshotFiles,
caches.lookupCache,
storeCurrentClasspathSnapshotForReuse,
ClasspathSnapshotBuildReporter(reporter)
).toChangesEither()
}
is NotAvailableDueToMissingClasspathSnapshot -> ChangesEither.Unknown(BuildAttribute.CLASSPATH_SNAPSHOT_NOT_FOUND)
is NotAvailableForNonIncrementalRun -> ChangesEither.Unknown(BuildAttribute.UNKNOWN_CHANGES_IN_GRADLE_INPUTS)
// classpathSnapshot不可用时
is ClasspathSnapshotDisabled -> reporter.measure(BuildTime.IC_ANALYZE_CHANGES_IN_DEPENDENCIES) {
val lastBuildInfo = BuildInfo.read(lastBuildInfoFile)
getClasspathChanges(
args.classpathAsList, changedFiles, lastBuildInfo, modulesApiHistory, reporter, abiSnapshots, withAbiSnapshot,
caches.platformCache, scopes
)
}
is NotAvailableForJSCompiler -> error("Unexpected type for this code path: ${classpathChanges.javaClass.name}.")
}
// 将结果添加到dirtyFiles
val unused = when (classpathChanges) {
is ChangesEither.Unknown -> {
return CompilationMode.Rebuild(classpathChanges.reason)
}
is ChangesEither.Known -> {
dirtyFiles.addByDirtySymbols(classpathChanges.lookupSymbols)
dirtyClasspathChanges = classpathChanges.fqNames
dirtyFiles.addByDirtyClasses(classpathChanges.fqNames)
}
}
// ...
return CompilationMode.Incremental(dirtyFiles)
}
复制代码
calculateSourcesToCompileImpl
的目的就是计算Kotlin
编译器应该重新编译哪些代码,主要分为以下几个步骤
- 初始化
dirtyFiles
,并将changedFiles
加入dirtyFiles
,因为changedFiles
需要重新编译 classpathSnapshot
可用时,通过传入的snapshot.bin
文件,与Project
目录下的shrunk-classpath-snapshot.bin
进行比较得出变化的classpath
,以及受影响的类。在比较结束时,也会更新当前目录的shrunk-classpath-snapshot.bin
,供下次比较使用- Cuando no
classpathSnapshot
está disponible, el cambiogetClasspathChanges
se juzga por el métodoclasspath
, que en realidad se juzga porlast-build.bin
ybuild-history.bin
, y se actualizará cada vez que se complete la compilación.build-history.bin
- Agregue las clases afectadas por el
classpath
cambio tambiéndirtyFiles
- Regrese
dirtyFiles
para que elKotlin
compilador realmente comience a compilar
En este paso, el Kotlin
compilador analiza los diversos parámetros de entrada y agrega los archivos que deben volver a compilarse dirtyFiles
para el siguiente paso.
Paso 7: Kotlin
el compilador realmente comienza a compilar
private fun compileImpl(): ExitCode {
// ...
var compilationMode = sourcesToCompile(caches, changedFiles, args, messageCollector, classpathAbiSnapshot)
when (compilationMode) {
is CompilationMode.Incremental -> {
// ...
compileIncrementally(args, caches, allSourceFiles, compilationMode, messageCollector, withAbiSnapshot)
}
is CompilationMode.Rebuild -> rebuildReason = compilationMode.reason
}
// ...
}
protected open fun compileIncrementally(): ExitCode {
while (dirtySources.any() || runWithNoDirtyKotlinSources(caches)) {
// ...
val (sourcesToCompile, removedKotlinSources) = dirtySources.partition(File::exists)
// 真正进行编译
val compiledSources = runCompiler(
sourcesToCompile, args, caches, services, messageCollectorAdapter,
allKotlinSources, compilationMode is CompilationMode.Incremental
)
// ...
}
if (exitCode == ExitCode.OK) {
// 写入`last-build.bin`
BuildInfo.write(currentBuildInfo, lastBuildInfoFile)
}
val dirtyData = DirtyData(buildDirtyLookupSymbols, buildDirtyFqNames)
// 写入`build-history.bin`
processChangesAfterBuild(compilationMode, currentBuildInfo, dirtyData)
return exitCode
}
复制代码
Este código hace principalmente las siguientes cosas:
- Después
sourcesToCompile
de calcular los archivos modificados, si es posible la compilación incremental, ingrese elcompileIncrementally
dirtySouces
Descubra los archivos que deben volver arunCompiler
método para una compilación real- Una vez completada la compilación, escriba
last-build.bin
ybuild-history.bin
archive para comparar en la próxima compilación
En este punto, el proceso de compilación incremental básicamente se completa.
Resumir
Este artículo presenta con más detalle Kotin
cómo iniciar la compilación incremental paso a paso desde el punto de entrada de la compilación. Comprender el Kotlin
principio de la compilación incremental puede ayudarlo a localizar por qué Kotlin
a veces falla la compilación incremental, y también puede aprender a escribir una compilación incremental que sea más fácil de hit.code, espero que te ayude.
Hay más detalles sobre Kotlin
la compilación incremental. Este artículo solo presenta el proceso principal. Los estudiantes interesados pueden ver directamente el código fuente KGP
del Kotlin
compilador.
Referencias
Una inmersión profunda en el proceso de compilación de Android: cómo se compila Kotlin