avant-propos
La compilation et l'exécution sont une Android
tâche qu'un développeur doit effectuer tous les jours. La compilation incrémentielle est également extrêmement importante pour les développeurs. La compilation incrémentielle avec un taux de succès élevé peut grandement améliorer l'efficacité du développement et l'expérience des développeurs.
J'ai écrit quelques articles auparavant pour introduire Kotlin
le principe de la compilation incrémentale et Kotlin 1.7
le support de la compilation incrémentale inter-modules
Après avoir compris ces principes de base, Kotlin
examinons aujourd'hui le code source de la compilation incrémentale pour voir comment Kotlin
la compilation incrémentale est implémentée
Pré-connaissance
Jetons un coup d'œil à la technologie noire derrière la compilation rapide de Kotlin ~
Nouvelles fonctionnalités de Kotlin 1.7 : prise en charge de la compilation incrémentielle inter-modules
Transform est abandonné, jetons un coup d'œil à TransformAction ~
Il s'agit principalement Kotlin
d'une introduction au principe de la compilation incrémentale, et du fait qu'il est utilisé dans le code source TransformAction
, il est également nécessaire d'en comprendre TransformAction
l'utilisation de base
Processus de compilation incrémental
Étape 1 : Compiler l'entrée
Si on veut l'utiliser dans le projet Kotlin
, il faut ajouter un org.jetbrains.kotlin.android
plugin, ce plugin est Kotlin
le point d'entrée de notre compilation, et son code est dans le kotlin-gradle-plugin
plugin.
La classe d'implémentation de ce plugin est KotlinAndroidPluginWrapper
, on peut voir KotlinAndroidPluginWrapper
qu'il s'agit d'un package, qui consiste principalement à créer et configurerKotlinAndroidPlugin
Étape 2 : ConfigurationKotlinAndroidPlugin
KotlinAndroidPlugin
C'est la véritable entrée du plug-in, où le compileKotlin Task
travail de configuration associé est effectué
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)
}
}
复制代码
Certains codes sont omis, principalement pour faire quelques choses :
- Vérifiez la compatibilité
KGP
avecGradle
la version, sinon, lancez une exception et abandonnez la construction - Si le plugin
project
a déjà été ajoutéandroid
, commencez à configurer lekotlin-android
plugin - 通过
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
,供下次比较使用- Lorsqu'il
classpathSnapshot
n'est pas disponible, le changementgetClasspathChanges
est jugé par la méthodeclasspath
, qui est en fait jugée parlast-build.bin
etbuild-history.bin
, et il sera mis à jour à chaque fois que la compilation sera terminée.build-history.bin
- Ajoutez également les classes concernées par le
classpath
changementdirtyFiles
- Retour
dirtyFiles
pour que leKotlin
compilateur commence réellement à compiler
Dans cette étape, le Kotlin
compilateur analyse les différents paramètres d'entrée et ajoute les fichiers qui doivent être recompilés dirtyFiles
pour l'étape suivante.
Étape 7 : Kotlin
Le compilateur démarre réellement la compilation
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
}
复制代码
Ce code fait principalement les choses suivantes :
- Après avoir
sourcesToCompile
calculé les fichiers modifiés, si la compilation incrémentale est possible, entrez lecompileIncrementally
dirtySouces
Découvrez les fichiers qui doiventrunCompiler
méthode pour une véritable compilation- Une fois la compilation terminée, écrivez
last-build.bin
etbuild-history.bin
archivez pour comparaison dans la prochaine compilation
À ce stade, le processus de compilation incrémentielle est essentiellement terminé.
Résumer
Cet article présente plus en détail Kotin
comment démarrer une compilation incrémentielle étape par étape à partir du point d'entrée de la compilation. Comprendre le Kotlin
principe de la compilation incrémentielle peut vous aider à localiser pourquoi Kotlin
la compilation incrémentielle échoue parfois, et vous pouvez également apprendre à écrire une compilation incrémentielle plus facile à hit.code, j'espère que cela vous aidera.
Il y a plus de détails sur Kotlin
la compilation incrémentale. Cet article ne présente que le processus principal. Les étudiants intéressés peuvent voir directement le code source KGP
du Kotlin
compilateur.
Références
Une plongée profonde dans le processus de compilation Android - comment Kotlin est compilé