KSP系列一:认识KSP注解处理器及其编译优化

演示工程: :point_right: ArouterKspCompiler

文档仓库: :point_right: Ksp document repo

简介

Kotlin Symbol Processing (KSP) 可用于开发kotlin轻量级编译器插件,也就是针对Kotlin语言的注解处理器插件;可以说,Kotlin KSP是完全对标Java APT(AbstractProcessor)的框架;

当然,为啥我们要从APT(KAPT)迁移到KSP呢?原因是性能:官网宣称 与kapt相比,使用KSP的注释处理器的运行速度可以快两倍 ,从描述看相当的厉害;

流程

注解处理器原理是JDK1.6中引入的JSR-269(Pluggable Annotations Processing API)规范,如图:

上述流程图落实到Java语言中,输入源文件自然是.java文件, 注解处理器框架自然是Java APT(AbstractProcessor);

需要注意的是,Java APT只能解析Java代码中的注解! 在纯Java时代,上述流程是没有任何问题的;但是Kotlin时代,就存在问题:

  • Kotlin文件能够使用APT编译时注解?

  • 如果能,谁去解析注解?

官方肯定是要兼容支持Kotlin使用APT注解的,具体的方案是使用 Kapt

  • 基于Kotlin文件生成Java Stub,简单理解为Kotlin转Java
  • 基于Java Stub去做APT分析,从而使APT插件兼容Kotlin

使用公式总结kapt的本质: kapt = generateStubs + apt ,处理流程如下图:

Android Studio自带编译分析柱状图中,搜索kaptGenerateStubs任务你会发现他们的耗时非常之高;如果Kotlin占比很多的情况下,这个耗时应该能占到编译总耗时的10%~20%; 而这个任务,仅仅是为兼容Kotlin文件中APT注解;这个Kotlin转Java操作属实是重量级!

官方的ksp方案则不同,具体有以下几点:

  • 直接兼容kotlin代码,从根源消除kapt耗时操作;

  • 能够完全解析kotlin中各种语法,如val/var、data class等kotlin灵活特性;

  • 直接兼容Java代码的注解处理,意味着编写一次ksp插件,kotlin和java都可以直接使用;

注意:彻底消除generateStub操作,需要移除所有的kapt插件,确保模块配置不存在 id kotlin-kapt

如下图:

另外官方宣称处理注解处理速度相较于APT也有很大提升,特别是KSP使用了全新增量编译机制;

总结起来,KSP的提升来自两方面:

  • 彻底移除Kapt中generateStub耗时操作;PS:实打实肉眼可见,约可减少10~25%编译耗时;
  • 相较于APT,自身更好的注解处理性能; PS:这个其实不是很好观测;

至此,我们理清了apt、kapt、ksp三者的关系,以及编译优化的着手点;

模型

不管是Java文件、Kotlin文件或者Class字节码,其实都是有对应的数据结构去描述他的文件结构的;KSP需要解析Kotlin文件,那么首先就需要使用数据结构去为Kotlin文件(Kotlin语法)建模型,然后才能实现解析;

从KSP角度看,Kotlin文件结构如下:

yaml 复制代码
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<KSValueParameter>
      // 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: KSValueParameter

看起来是不是很像反射这一套,看名字后大家应该都能有一个基本的理解👀 其实元编程这种对源文件处理的框架,应该都是大同小异的;

我们需要做的就是在自定义的ksp注解处理器插件中解析其中的关键Node,根据既定的规则来生成自定义代码(后续会有详细的实战);

开发

使用

当我们使用别人的ksp插件,如何做呢; 详细可参考 Use your own processor in a project

导入仓库:

scss 复制代码
pluginManagement {
    repositories {
        gradlePluginPortal()
    }
}

在需要使用自定义注解的每个模块,导入ksp插件本身,然后使用ksp导入自定义三方插件;

注意,ksp依赖于具体项目kotlin版本,请保证他们是最匹配的;1.6.10-1.0.4 前面的1.6.10代表kotlin版本号,1.0.4是小版本号,尽可能更到最新; :point_right:ksp官网版本查询

bash 复制代码
plugins {
    id("com.google.devtools.ksp") version "1.6.10-1.0.4"
}

dependencies {
    ksp 'com.github.JailedBird:ArouterKspCompiler:xxx'
}

然后ArouterKspCompiler插件就生效了

开发

自行开发ksp插件时,需要注意的细节就很多了,可参考:

导入kotlin插件

arduino 复制代码
plugins {
    kotlin("jvm") version "1.9.21" apply false
}

buildscript {
    dependencies {
        classpath(kotlin("gradle-plugin", version = "1.9.21"))
    }
}

添加ksp依赖

scss 复制代码
plugins {
    kotlin("jvm")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15")
}

编写插件,process中实现解析和文件生成(后续会详细介绍,本文直接介绍个大概!)

kotlin 复制代码
class AutowiredSymbolProcessorProvider : SymbolProcessorProvider {

    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return AutowiredSymbolProcessor(
            KSPLoggerWrapper(environment.logger), environment.codeGenerator
        )
    }

    class AutowiredSymbolProcessor(
        private val logger: KSPLoggerWrapper,
        private val codeGenerator: CodeGenerator,
    ) : SymbolProcessor {
      
        override fun process(resolver: Resolver): List<KSAnnotated> {
            val symbol = resolver.getSymbolsWithAnnotation(AUTOWIRED_CLASS_NAME)

            val elements = symbol
                .filterIsInstance<KSPropertyDeclaration>()
                .toList()

            if (elements.isNotEmpty()) {
                logger.info(">>> AutowiredSymbolProcessor init. <<<")
                try {
                    parseAutowired(elements)
                } catch (e: Exception) {
                    logger.exception(e)
                }
            }
        }
    }
}

使用SPI配置插件地址

META-INF/services /com.google.devtools.ksp.processing.SymbolProcessorProvider文件中,配置插件的全限定路径;

复制代码
cn.jailedbird.arouter.ksp.compiler.AutowiredSymbolProcessorProvider

至此,基础架子搭建起来了;

后续

本文只是对ksp进行了简单的介绍,后续会持续更新:

  • ksp系列-ksp增量编译机制详解和示例
  • ksp实战-Arouter ksp compiler实战分享

如果本项目对您的学习或工作有帮助,请点亮star支持作者😘

参考文献:

相关推荐
天花板之恋18 小时前
Compose状态管理
android jetpack
alexhilton1 天前
面向开发者的系统设计:像建筑师一样思考
android·kotlin·android jetpack
Lei活在当下2 天前
【业务场景架构实战】4. 支付状态分层流转的设计和实现
架构·android jetpack·响应式设计
天花板之恋3 天前
Compose之图片加载显示
android jetpack
消失的旧时光-19434 天前
Kotlinx.serialization 使用讲解
android·数据结构·android jetpack
Tans54 天前
Androidx Fragment 源码阅读笔记(下)
android jetpack·源码阅读
Lei活在当下5 天前
【业务场景架构实战】2. 对聚合支付 SDK 的封装
架构·android jetpack
Tans57 天前
Androidx Fragment 源码阅读笔记(上)
android jetpack·源码阅读
alexhilton8 天前
runBlocking实践:哪里该使用,哪里不该用
android·kotlin·android jetpack
Tans511 天前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读