Kotlin 增量编译是怎么实现的?
Posted 嘴巴吃糖了
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin 增量编译是怎么实现的?相关的知识,希望对你有一定的参考价值。
前言
编译运行是一个android开发者每天都要做的工作,增量编译对于开发者也极其重要,高命中率的增量编译可以极大的提高开发者的开发效率与体验
之前写了一些文章介绍Kotlin增量编译的原理,以及Kotlin 1.7
支持了跨模块增量编译
了解了这些基本原理之后,我们今天一起来看下Kotlin增量编译的源码,看看Kotlin增量编译到底是怎么实现的
增量编译流程
第一步:编译入口
如果我们要在项目中使用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
获取一个已经改变与删除的文件的PairgetClasspathChanges
则根据增量编译是否开启,是否有文件发生更改,历史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是如何编译的
作者:程序员江同学
链接:https://juejin.cn/post/7137089121989689351
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
一、架构师筑基必备技能
1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……
二、Android百大框架源码解析
1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程
三、Android性能优化实战解析
- 腾讯Bugly:对字符串匹配算法的一点理解
- 爱奇艺:安卓APP崩溃捕获方案——xCrash
- 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
- 百度APP技术:Android H5首屏优化实践
- 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
- 携程:从智行 Android 项目看组件化架构实践
- 网易新闻构建优化:如何让你的构建速度“势如闪电”?
- …
四、高级kotlin强化实战
1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》
-
从一个膜拜大神的 Demo 开始
-
Kotlin 写 Gradle 脚本是一种什么体验?
-
Kotlin 编程的三重境界
-
Kotlin 高阶函数
-
Kotlin 泛型
-
Kotlin 扩展
-
Kotlin 委托
-
协程“不为人知”的调试技巧
-
图解协程:suspend
五、Android高级UI开源框架进阶解密
1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
六、NDK模块开发
1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习
七、Flutter技术进阶
1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)
…
八、微信小程序开发
1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证卡片免费领取↓↓↓
以上是关于Kotlin 增量编译是怎么实现的?的主要内容,如果未能解决你的问题,请参考以下文章