前言
编译运转是一个Android
开发者每天都要做的作业,增量编译关于开发者也极其重要,高命中率的增量编译能够极大的进步开发者的开发功率与体会
之前写了一些文章介绍Kotlin
增量编译的原理,以及Kotlin 1.7
支撑了跨模块增量编译
了解了这些基本原理之后,咱们今天一起来看下Kotlin
增量编译的源码,看看Kotlin
增量编译到底是怎样完结的
前置常识
Kotlin 快速编译背后的黑科技,了解一下~
Kotlin 1.7 新特性:支撑跨模块增量编译
Transform 被抛弃,TransformAction 了解一下~
首要是Kotlin
增量编译的原理介绍,以及由于在源码中运用了TransformAction
,也需求了解一下TransformAction
的基本运用
增量编译流程
第一步:编译进口
假如咱们要在项目中运用Kotlin
,都必须要添加org.jetbrains.kotlin.android
插件,这个插件是咱们编译Kotlin
的进口,它的代码在kotlin-gradle-plugin
插件中
这个插件的完结类便是KotlinAndroidPluginWrapper
,能够看出KotlinAndroidPluginWrapper
便是个包装,里边首要便是创立并装备KotlinAndroidPlugin
第二步:装备KotlinAndroidPlugin
KotlinAndroidPlugin
是插件真实的进口,在这儿完结compileKotlin Task
相关的装备作业
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)
}
}
省略了一些代码,首要做了几件事:
- 检查
KGP
与Gradle
的版本兼容,假如不兼容则抛出反常,间断构建 - 假如在
project
中现已添加了android
插件,则开端装备kotlin-android
插件 - 经过
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
,供下次比较运用 - 当
classpathSnapshot
不行用时,经过getClasspathChanges
办法来判别classpath
改动,这儿边实际上是经过last-build.bin
与build-history.bin
来判别的,同时每次编译完结也会更新build-history.bin
- 将受
classpath
改动影响的类也参加dirtyFiles
- 返回
dirtyFiles
供Kotlin
编译器真实开端编译
在这一步,Kotlin
编译器运用输入的各种参数进行剖析,将需求从头编译的文件参加dirtyFiles
,供下一步运用
第七步:Kotlin
编译器真实开端编译
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
}
这段代码首要做了这么几件事:
- 经过
sourcesToCompile
核算出发生改动的文件后,假如能够增量编译,则进入到compileIncrementally
- 从
dirtySouces
中找出需求从头编译的文件,交给runCompiler
办法进行真实的编译 - 在编译完毕之后,写入
last-build.bin
与build-history.bin
文件,供下次编译时对比运用
到这儿,增量编译的流程也就基本完结了。
总结
本文较为具体地介绍了Kotin
是怎样一步步从编译进口到真实开端增量编译的,了解Kotlin
增量编译原理能够协助你定位为什么Kotlin
增量编译有时会失效,也能够了解怎么写出更容易命中增量编译的代码,期望对你有所协助。
关于Kotlin
增量编译还有更多的细节,本文也只是介绍了首要的流程,感兴趣的同学可直接检查KGP
和Kotlin
编译器的源码
参考资料
深入研究Android编译流程-Kotlin是怎么编译的