前言

编译运转是一个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)
    }
}

省略了一些代码,首要做了几件事:

  1. 检查KGPGradle的版本兼容,假如不兼容则抛出反常,间断构建
  2. 假如在project中现已添加了android插件,则开端装备kotlin-android插件
  3. 经过KotlinCompileConfig来装备KotlinCompile Task,设置destinationDirectory作为Kotlin编译成果存储目录,后续会作为java compilerclasspath输入

第三步:装备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()
        }
    }
}

能够看出,首要做了这么几件事

  1. 判别是否敞开了classpathSnapthot,这也是支撑跨模块增量编译的开关,假如敞开了就注册Transform
  2. 经过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_TYPEDIRECTORY_ARTIFACT_TYPE转换为CLASSPATH_ENTRY_SNAPSHOT_ARTIFACT_TYPE

也便是说依靠的jar和类目录都会转换为CLASSPATH_ENTRY_SNAPSHOT_ARTIFACT_TYPE类型,也就能够获取咱们依靠的所有classpathabi

接下来咱们看下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个部分,输入,输出,履行办法体

  1. ClasspathEntrySnapshotTransform的输入便是模块依靠的jar或许文件目录
  2. 输出则是以-snapshot.bin结尾的文件
  3. 办法体只做了一件事,经过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")
    }
}

Kotlin 增量编译是怎么实现的?

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的视点来剖析

  1. classpathSnapshotProperties是个包装类型的输入,内部包含@Classpath类型的输入,运用@Classpath输入时,假如输入文件名发生改动而内容没有发生改动时,不会触发Task从头运转,这对classpath来说非常重要
  2. incrementalProps是组件后的增量编译输入参数,包含kotlin输入,java输入,classpath输入等
  3. CompileKotlinTaskAction,它最后会履行到callCompilerAsync办法,在其间经过getChangedFilesgetClasspathChanges获取改动了的输入与classpath
  4. getClasspathChanges办法经过inputChanges获取一个现已改动与删除的文件的Pair
  5. getClasspathChanges则依据增量编译是否敞开,是否有文件发生更改,前史snapshotFile是否存在,返回不同的ClassPathChanges密封类

在增量编译参数组装完结后,接下来便是跟着逻辑走,最后会走到GradleKotlinCompilerWorkcompileWithDaemmonOrFailbackImpl

Kotlin 增量编译是怎么实现的?

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编译有三种战略,分别是

  1. 守护进程编译:Kotlin编译的默认形式,只有这种形式才支撑增量编译,能够在多个Gradle daemon进程间共享
  2. 进程内编译:Gradle daemon进程内编译
  3. 进程外编译:每次编译都是在不同的进程

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.CompileServiceImplcompile 办法,这样就终于调到了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编译器应该从头编译哪些代码,首要分为以下几个过程

  1. 初始化dirtyFiles,并将changedFiles参加dirtyFiles,由于changedFiles需求从头编译
  2. classpathSnapshot可用时,经过传入的snapshot.bin文件,与Project目录下的shrunk-classpath-snapshot.bin进行比较得出改动的classpath,以及受影响的类。在比较完毕时,也会更新当前目录的shrunk-classpath-snapshot.bin,供下次比较运用
  3. classpathSnapshot不行用时,经过getClasspathChanges办法来判别classpath改动,这儿边实际上是经过last-build.binbuild-history.bin来判别的,同时每次编译完结也会更新build-history.bin
  4. 将受classpath改动影响的类也参加dirtyFiles
  5. 返回dirtyFilesKotlin编译器真实开端编译

在这一步,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
}

这段代码首要做了这么几件事:

  1. 经过sourcesToCompile核算出发生改动的文件后,假如能够增量编译,则进入到compileIncrementally
  2. dirtySouces中找出需求从头编译的文件,交给runCompiler办法进行真实的编译
  3. 在编译完毕之后,写入last-build.binbuild-history.bin文件,供下次编译时对比运用

到这儿,增量编译的流程也就基本完结了。

总结

本文较为具体地介绍了Kotin是怎样一步步从编译进口到真实开端增量编译的,了解Kotlin增量编译原理能够协助你定位为什么Kotlin增量编译有时会失效,也能够了解怎么写出更容易命中增量编译的代码,期望对你有所协助。

关于Kotlin增量编译还有更多的细节,本文也只是介绍了首要的流程,感兴趣的同学可直接检查KGPKotlin编译器的源码

参考资料

深入研究Android编译流程-Kotlin是怎么编译的