告别KAPT!使用 KSP 为 Kotlin 编译提速

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Kotlin Symbol Processing(KSP)是新一代的 Kotlin 注解处理工具,它基于 Kotlin Compiler 实现,相对于 KAPT 性能得到很大提升

今年初 Android 发布了 Kotlin Symbol Processing(KSP)的首个 Alpha 版,几个月过去,KSP 已经更新到 Beta3 了, 目前 API 已经基本稳定,相信距离稳定版发布也不会很远了。

为什么使用 KSP ?

不少人吐槽 Kotlin 的编译速度,KAPT 便是拖慢编译的元凶之一。

很多库都会使用注解简化模板代码,例如 Room、Dagger、Retrofit 等,Kotlin 代码使用 KAPT 处理注解。 KAPT 本质上是基于 APT 工作的,APT 只能处理 Java 注解,因此需要先生成 APT 可解析的 stub (Java代码),这拖慢了 Kotlin 的整体编译速度。

KSP 正是在这个背景下诞生的,它基于 Kotlin Compiler Plugin(简称KCP) 实现,不需要生成额外的 stub,编译速度是 KAPT 的 2 倍以上

KSP 与 KCP

Kotlin Compiler Plugin 在 kotlinc 过程中提供 hook 时机,可以再次期间解析 AST、修改字节码产物等,Kotlin 的不少语法糖都是 KCP 实现的,例如 data class@Parcelizekotlin-android-extension 等, 如今火爆的 Compose 其编译期工作也是借助 KCP 完成的。

理论上 KCP 的能力是 KAPT 的超集,可以替代 KAPT 以提升编译速度。但是 KCP 的开发成本太高,涉及 Gradle Plugin、Kotlin Plugin 等的使用,API 涉及一些编译器知识的了解,一般开发者很难掌握。

一个标准 KCP 的开发涉及以下诸多内容:

  • Plugin:Gradle 插件用来读取 Gradle 配置传递给 KCP(Kotlin Plugin)
  • Subplugin:为 KCP 提供自定义 KP 的 maven 库地址等配置信息
  • CommandLineProcessor:将参数转换为 KP 可识别参数
  • ComponentRegistrar:注册 Extension 到 KCP 不同流程中
  • Extension:实现自定义的 KP 功能

KSP 简化了上述流程,开发者无需了解编译器工作原理,处理注解等成本像 KAPT 一样低。

KSP 与 KAPT

KSP 顾名思义,在 Symbols 级别对 Kotlin 的 AST 进行处理,访问类、类成员、函数、相关参数等类型的元素。可以类比 PSI 中的 Kotlin AST

一个 Kotlin 源文件经 KSP 解析后的结果如下:

KSFile
  packageName: KSName
  fileName: String
  annotations: List<KSAnnotation>  (File annotations)
  declarations: List<KSDeclaration>
    KSClassDeclaration // class, interface, object
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      classKind: ClassKind
      primaryConstructor: KSFunctionDeclaration
      superTypes: List<KSTypeReference>
      // contains inner classes, member functions, properties, etc.
      declarations: List<KSDeclaration>
    KSFunctionDeclaration // top level function
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      functionKind: FunctionKind
      extensionReceiver: KSTypeReference?
      returnType: KSTypeReference
      parameters: List<KSVariableParameter>
      // contains local classes, local functions, local variables, etc.
      declarations: List<KSDeclaration>
    KSPropertyDeclaration // global variable
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      extensionReceiver: KSTypeReference?
      type: KSTypeReference
      getter: KSPropertyGetter
        returnType: KSTypeReference
      setter: KSPropertySetter
        parameter: KSVariableParameter
    KSEnumEntryDeclaration
      // same as KSClassDeclaration

这是 KSP 中的 Kotlin AST 抽象。 类似的, APT/KAPT 中有对 Java 的 AST 抽象,其中能找到一些对应关系,比如 Java 使用 Element 描述包、类、方法或者变量等, KSP 中使用 Declaration

Java/APT Kotlin/KSP Description
PackageElement KSFile 表示一个包程序元素。提供对有关包及其成员的信息的访问
ExecuteableElement KSFunctionDeclaration 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素
TypeElement KSClassDeclaration 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口
VariableElement KSVariableParameter / KSPropertyDeclaration 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数

Declaration 之下还有 Type 信息 ,比如函数的参数、返回值类型等,在 APT 中使用 TypeMirror 承载类型信息 ,KSP 中详细的能力由 KSType 实现。

KSP 的开发流程和 KAPT 类似:

  1. 解析源码AST
  2. 生成代码
  3. 生成的代码与源码一起参与 Kotlin 编译

需要注意 KSP 不能用来修改原代码,只能用来生成新代码

KSP 入口:SymbolProcessorProvider

KSP 通过 SymbolProcessor 来具体执行。SymbolProcessor 需要通过一个 SymbolProcessorProvider 来创建。因此 SymbolProcessorProvider 就是 KSP 执行的入口

interface SymbolProcessorProvider {
    fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}

SymbolProcessorEnvironment 获取一些 KSP 运行时的依赖,注入到 Processor

interface SymbolProcessor {
    fun process(resolver: Resolver): List<KSAnnotated> // Let's focus on this
    fun finish() {}
    fun onError() {}
}

process() 提供一个 Resolver , 解析 AST 上的 symbols。 Resolver 使用访问者模式去遍历 AST。

如下,Resolver 使用 FindFunctionsVisitor 找出当前 KSFile 中 top-level 的 function 以及 Class 成员方法:

class HelloFunctionFinderProcessor : SymbolProcessor() {
    ...
    val functions = mutableListOf<String>()
    val visitor = FindFunctionsVisitor()

    override fun process(resolver: Resolver) {
        //使用 FindFunctionsVisitor 遍历访问 AST
        resolver.getAllFiles().map { it.accept(visitor, Unit) }
    }

    inner class FindFunctionsVisitor : KSVisitorVoid() {
        override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
            //访问 Class 节点
            classDeclaration.getDeclaredFunctions().map { it.accept(this, Unit) }
        }

        override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
            // 访问 function 节点
            functions.add(function)
        }

        override fun visitFile(file: KSFile, data: Unit) {
            //访问 file
            file.declarations.map { it.accept(this, Unit) }
        }
    }
    ...
}

KSP API 示例

举几个例子看一下 KSP 的 API 是如何工作的

访问类中的所有成员方法

fun KSClassDeclaration.getDeclaredFunctions(): List<KSFunctionDeclaration> {
    return this.declarations.filterIsInstance<KSFunctionDeclaration>()
}

判断一个类或者方法是否是局部类或局部方法

fun KSDeclaration.isLocal(): Boolean {
    return this.parentDeclaration != null && this.parentDeclaration !is KSClassDeclaration
}

判断一个类成员是否对其他Declaration可见

fun KSDeclaration.isVisibleFrom(other: KSDeclaration): Boolean {
    return when {
        // locals are limited to lexical scope
        this.isLocal() -> this.parentDeclaration == other
        // file visibility or member
        this.isPrivate() -> {
            this.parentDeclaration == other.parentDeclaration
                    || this.parentDeclaration == other
                    || (
                        this.parentDeclaration == null
                            && other.parentDeclaration == null
                            && this.containingFile == other.containingFile
                    )
        }
        this.isPublic() -> true
        this.isInternal() && other.containingFile != null && this.containingFile != null -> true
        else -> false
    }
}

获取注解信息

// Find out suppressed names in a file annotation:
// @file:kotlin.Suppress("Example1", "Example2")
fun KSFile.suppressedNames(): List<String> {
    val ignoredNames = mutableListOf<String>()
    annotations.forEach {
        if (it.shortName.asString() == "Suppress" && it.annotationType.resolve()?.declaration?.qualifiedName?.asString() == "kotlin.Suppress") {
            it.arguments.forEach {
                (it.value as List<String>).forEach { ignoredNames.add(it) }
            }
        }
    }
    return ignoredNames
}

代码生成的示例

最后看一个相对完整的例子,用来替代APT的代码生成

@IntSummable
data class Foo(
  val bar: Int = 234,
  val baz: Int = 123
)

我们希望通过KSP处理@IntSummable,生成以下代码

public fun Foo.sumInts(): Int {
  val sum = bar + baz
  return sum
}

Dependencies

开发 KSP 需要添加依赖:

plugins {
    kotlin("jvm") version "1.4.32"
}

repositories {
    mavenCentral()
    google()
}

dependencies {
    implementation(kotlin("stdlib"))
    implementation("com.google.devtools.ksp:symbol-processing-api:1.5.10-1.0.0-beta01")
}

IntSummableProcessorProvider

我们需要一个入口的 Provider 来构建 Processor

import com.google.devtools.ksp.symbol.*

class IntSummableProcessorProvider : SymbolProcessorProvider {

    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return IntSummableProcessor(
            options = environment.options,
            codeGenerator = environment.codeGenerator,
            logger = environment.logger
        )
    }
}

通过 SymbolProcessorEnvironment 可以为 Processor 注入了 optionsCodeGeneratorlogger 等所需依赖

IntSummableProcessor

class IntSummableProcessor() : SymbolProcessor {
    
    private lateinit var intType: KSType

    override fun process(resolver: Resolver): List<KSAnnotated> {
        intType = resolver.builtIns.intType
        val symbols = resolver.getSymbolsWithAnnotation(IntSummable::class.qualifiedName!!).filterNot{ it.validate() }

        symbols.filter { it is KSClassDeclaration && it.validate() }
            .forEach { it.accept(IntSummableVisitor(), Unit) }

        return symbols.toList()
    }
}    
  • builtIns.intType 获取到 kotlin.IntKSType, 在后面需要使用。
  • getSymbolsWithAnnotation 获取注解为 IntSummable 的 symbols 列表
  • 当 symbol 是 Class 时,使用 Visitor 对其进行处理

IntSummableVisitor

Visitor 的接口一般如下,DR 代表 Visitor 的输入和输出,

interface KSVisitor<D, R> {
    fun visitNode(node: KSNode, data: D): R

    fun visitAnnotated(annotated: KSAnnotated, data: D): R
    
    // etc.
}

我们的需求没有输入输出,所以实现KSVisitorVoid即可,本质上是一个 KSVisitor<Unit, Unit>

inner class Visitor : KSVisitorVoid() {
    
    override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
        val qualifiedName = classDeclaration.qualifiedName?.asString()

        //1. 合法性检查
        if (!classDeclaration.isDataClass()) {
            logger.error(
                "@IntSummable cannot target non-data class $qualifiedName",
                classDeclaration
            )
            return
        }

        if (qualifiedName == null) {
            logger.error(
                "@IntSummable must target classes with qualified names",
                classDeclaration
            )
            return
        }
        
        //2. 解析Class信息
        //...
        
        //3. 代码生成
        //...
        
    }
    
    private fun KSClassDeclaration.isDataClass() = modifiers.contains(Modifier.DATA)
}

如上,我们判断这个Class是不是data class、其类名是否合法

解析Class信息

接下来需要获取 Class 中的相关信息,用于我们的代码生成:

inner class IntSummableVisitor : KSVisitorVoid() {

    private lateinit var className: String
    private lateinit var packageName: String
    private val summables: MutableList<String> = mutableListOf()

    override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
        //1. 合法性检查
        //...
        
        //2. 解析Class信息
        val qualifiedName = classDeclaration.qualifiedName?.asString()
        className = qualifiedName
        packageName = classDeclaration.packageName.asString()

        classDeclaration.getAllProperties()
            .forEach {
                it.accept(this, Unit)
            }

        if (summables.isEmpty()) {
            return
        }
        
        //3. 代码生成
        //...
    }
    
    override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: Unit) {
        if (property.type.resolve().isAssignableFrom(intType)) {
            val name = property.simpleName.asString()
            summables.add(name)
        }
    }
}
  • 通过 KSClassDeclaration 获取了classNamepackageName,以及 Properties 并将其存入 summables
  • visitPropertyDeclaration 中确保 Property 必须是 Int 类型,这里用到了前面提到的 intType

代码生成

收集完 Class 信息后,着手代码生成。
我们引入 KotlinPoet 帮助我们生成 Kotlin 代码

dependencies {
    implementation("com.squareup:kotlinpoet:1.8.0")
}
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {

    //1. 合法性检查
    //...
      

    //2. 解析Class信息
    //...
        
        
    //3. 代码生成
    if (summables.isEmpty()) {
        return
    }

    val fileSpec = FileSpec.builder(
        packageName = packageName,
        fileName = classDeclaration.simpleName.asString()
    ).apply {
        addFunction(
            FunSpec.builder("sumInts")
                .receiver(ClassName.bestGuess(className))
                .returns(Int::class)
                .addStatement("val sum = ${summables.joinToString(" + ")}")
                .addStatement("return sum")
                .build()
        )
    }.build()

    codeGenerator.createNewFile(
        dependencies = Dependencies(aggregating = false),
        packageName = packageName,
        fileName = classDeclaration.simpleName.asString()
    ).use { outputStream ->
        outputStream.writer()
            .use {
                fileSpec.writeTo(it)
            }
    }
}
  • 使用 KotlinPoet 的 FunSpec 生成 function 代码
  • 前面SymbolProcessorEnvironment 提供的CodeGenerator用来创建文件,并写入生成的FileSpec代码

总结

通过 IntSummable 的例子可以看到 KSP 完全可以替代 APT/KAPT 进行注解处理,且性能更出色。

目前,已有不少使用 APT 的三方库增加了对 KSP 的支持

Library Status Tracking issue for KSP
Room Experimentally supported
Moshi Experimentally supported
Kotshi Experimentally supported
Lyricist Experimentally supported
Auto Factory Not yet supported Link
Dagger Not yet supported Link
Hilt Not yet supported Link
Glide Not yet supported Link
DeeplinkDispatch Not yet supported Link

将 KAPT 替换为 KSP 也非常简单,以 Moshi 为例

当然,也可以在项目中同时使用 KAPT 和 KSP ,他们互不影响。KSP 取代 KAPT 的趋势越来越明显,果你的项目也处理注解的需求,不妨试试 KSP ?

https://github.com/google/ksp

目录
相关文章
|
8月前
|
移动开发 监控 Android开发
构建高效安卓应用:Kotlin 协程的实践与优化
【5月更文挑战第16天】 在移动开发领域,性能优化一直是开发者们追求的重要目标。特别是对于安卓平台来说,由于设备多样性和系统资源的限制,如何提升应用的响应性和流畅度成为了一个关键议题。近年来,Kotlin 语言因其简洁、安全和高效的特点,在安卓开发中得到了广泛的应用。其中,Kotlin 协程作为一种轻量级的并发解决方案,为异步编程提供了强大支持,成为提升安卓应用性能的有效手段。本文将深入探讨 Kotlin 协程在安卓开发中的应用实践,以及通过合理设计和使用协程来优化应用性能的策略。
75 8
|
8月前
|
调度 数据库 Android开发
构建高效安卓应用:探究Kotlin协程的优势
【5月更文挑战第27天】在移动开发领域,性能优化和流畅的用户体验始终是开发者追求的核心目标。随着Kotlin语言在Android平台的广泛采用,其提供的协程功能已经成为实现异步编程和提升应用响应性的重要工具。本文将深入探讨Kotlin协程在Android开发中的应用优势,通过与传统线程和回调机制的对比,揭示协程如何简化代码结构、提高执行效率,并最终增强应用性能。我们将从协程的基本概念出发,逐步解析其在网络请求、数据库操作和UI线程中的具体实践,以期为Android开发者提供性能优化的新思路。
|
8月前
|
移动开发 Android开发 开发者
构建高效安卓应用:Kotlin 协程的实践指南
【5月更文挑战第18天】 随着移动开发技术的不断进步,安卓平台亟需一种高效的异步编程解决方案来应对日益复杂的应用需求。Kotlin 协程作为一种新兴的轻量级线程管理机制,以其简洁的语法和强大的功能,成为解决这一问题的关键。本文将深入探讨Kotlin协程在安卓开发中的实际应用,从基本概念到高级技巧,为开发者提供一份全面的实践指南,旨在帮助读者构建更加高效、稳定的安卓应用。
|
8月前
|
移动开发 监控 Android开发
构建高效Android应用:Kotlin协程的实践与优化
【5月更文挑战第12天】 在移动开发领域,性能与响应性是衡量一个应用程序优劣的关键指标。特别是在Android平台上,由于设备的多样性和系统资源的限制,开发者需要精心编写代码以确保应用流畅运行。近年来,Kotlin语言因其简洁性和功能性而广受欢迎,尤其是其协程特性,为异步编程提供了强大而轻量级的解决方案。本文将深入探讨如何在Android应用中使用Kotlin协程来提升性能,以及如何针对实际问题进行优化,确保应用的高效稳定执行。
|
8月前
|
移动开发 Java Android开发
Android应用开发:Kotlin语言的优势与实践
【5月更文挑战第7天】 在移动开发的世界中,Android平台的Kotlin语言以其高效、简洁的语法和强大的功能吸引了众多开发者。本文将深入探讨Kotlin语言的核心优势,并通过实际案例展示如何在Android应用开发中有效地运用这一现代编程语言。我们将从语言特性出发,逐步分析其在提升开发效率、改善代码质量以及增强应用性能方面的具体表现,为读者提供一个全面而细致的Kotlin应用开发指南。
|
8月前
|
移动开发 安全 Android开发
构建高效Android应用:采用Kotlin进行内存优化
【5月更文挑战第29天】 在移动开发领域,性能优化一直是开发者关注的焦点。特别是对于Android应用而言,内存管理是影响应用性能和用户体验的关键因素之一。近年来,Kotlin作为官方推荐的开发语言,以其简洁的语法和强大的功能受到广大开发者的青睐。本文将深入探讨如何通过Kotlin语言的特性来优化Android应用的内存使用,从而提升应用的性能表现。我们将从内存泄露检测、对象创建与销毁策略,以及数据结构的合理选择等方面入手,为读者提供一系列实用的优化建议。
|
8月前
|
Java Android开发 开发者
构建高效Android应用:Kotlin协程的实践指南
【5月更文挑战第31天】在现代Android开发中,异步编程和性能优化成为关键要素。Kotlin协程作为一种在JVM上实现轻量级线程的方式,为开发者提供了简洁而强大的并发处理工具。本文深入探讨了如何在Android项目中利用Kotlin协程提升应用的响应性和效率,包括协程的基本概念、结构以及实际运用场景,旨在帮助开发者通过具体实例理解并掌握协程技术,从而构建更加流畅和高效的Android应用。
|
8月前
|
移动开发 Android开发 开发者
构建高效Android应用:Kotlin协程的实践之路
【5月更文挑战第25天】 在移动开发领域,性能优化与流畅的用户体验始终是开发者追逐的目标。对于Android平台而言,Kotlin协程作为一种新型的异步编程解决方案,提供了更加简洁和高效的代码实现方式。本文将深入探讨如何在Android应用中利用Kotlin协程来提升后台任务处理的效率,同时确保UI线程的流畅性,从而打造高性能的应用程序。我们将分析协程的核心概念,并通过实际案例演示如何集成协程到现有项目中,以及如何调试和优化协程代码,以期帮助开发者更好地掌握这一强大工具。
|
8月前
|
数据库 Android开发 开发者
构建高效的Android应用:Kotlin协程的优势与实践
【5月更文挑战第23天】 在移动开发领域,性能优化和流畅的用户体验始终是开发者追求的目标。随着Kotlin语言在Android平台的广泛采用,其提供的协程功能已成为提升应用性能的强大工具。本文将深入探讨Kotlin协程的核心优势,并通过具体实例展示如何在Android应用中高效地运用协程进行异步编程,以及如何通过协程简化后台任务处理,实现流畅的用户界面。
|
8月前
|
移动开发 Android开发 开发者
构建高效Android应用:探究Kotlin协程的优化实践
【5月更文挑战第13天】 在移动开发领域,Android平台的流畅体验至关重要。随着Kotlin语言的普及,协程作为其核心特性之一,为异步编程提供了简洁且高效的解决方案。本文将深入探讨Kotlin协程在Android应用中的优化使用,从基本概念到实际案例分析,旨在帮助开发者构建更加响应迅速、性能卓越的应用。我们将通过对比传统线程与协程的差异,展示如何利用协程简化代码结构,并通过优化实践减少资源消耗,提升用户体验。