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)
    

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

  1. 检查KGP与Gradle的版本兼容,如果不兼容则抛出异常,中止构建
  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")
    

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

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. 返回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

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

  1. 通过sourcesToCompile计算出发生改变的文件后,如果可以增量编译,则进入到compileIncrementally
  2. dirtySouces中找出需要重新编译的文件,交给runCompiler方法进行真正的编译
  3. 在编译结束之后,写入last-build.binbuild-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 增量编译是怎么实现的?的主要内容,如果未能解决你的问题,请参考以下文章

Android 编译优化探索2 Hack字节码

Android 编译优化探索3

检查 kapt 是不是使用增量注释处理

告别KAPT,使用KSP为Android编译提速

学到了!Webpack5 新特性之增量编译

CMake通过工具链升级进行增量编译