4. Kotlin注解基础:注解的定义、元注解、注解的使用目标与保留策略

注解这东西,说白了就是给代码贴标签。你写个类、写个方法,往上面加个注解,就等于告诉编译器或者运行时框架:「嘿,这个家伙有点特殊,你注意一下」。我在做Android开发的头两年,其实对注解的理解也就停留在@Override@Nullable这个层面,直到后来开始做路由框架和编译期代码生成,才真正体会到注解的威力。

今天我们就来把Kotlin注解的基础彻底捋一遍。你想想看,如果连注解怎么定义、怎么控制它的生命周期都不清楚,后面学反射和注解处理器肯定会踩坑。

4.1 注解的定义:从Java到Kotlin的进化

在Kotlin里定义一个注解,用的是annotation class关键字。这跟Java的@interface有点像,但更简洁。

// Kotlin注解定义
annotation class BindView(val id: Int)

// 使用
class MainActivity {
    @BindView(id = R.id.tv_title)
    lateinit var titleView: TextView
}

我个人习惯把注解定义放在一个单独的包下,比如com.example.annotations。这样项目结构清晰,别人一看就知道这是自定义注解。

这里有个细节要注意:Kotlin注解的构造函数参数是有严格限制的。只能传这些类型:

  • 基本类型(Int、Long、Boolean等)
  • String
  • 枚举
  • 其他注解
  • Class引用(KClass
  • 以上类型的数组
注意:Kotlin注解的参数不能传可空类型,也不能传自定义的普通类对象。我在项目中见过有人试图传一个Object进去,编译直接报错。

4.2 元注解:注解的注解

元注解就是用来修饰注解的注解。听起来有点绕,但其实就是给注解本身打标签。Kotlin从Java继承了几种常用的元注解,也加了一些自己的东西。

元注解 作用 我的经验
@Target 指定注解可以用在什么地方 最常用,几乎每个自定义注解都要配
@Retention 指定注解保留到哪个阶段 编译期处理用SOURCE,运行时反射用RUNTIME
@Repeatable 允许同一个地方重复使用该注解 用得少,但做权限校验时很香
@MustBeDocumented 让注解出现在API文档中 做SDK开发时建议加上

举个例子,我要定义一个只在编译期生效的注解:

@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class DebugOnly

嗯,这里要注意AnnotationTargetAnnotationRetention是Kotlin的枚举,跟Java的ElementTypeRetentionPolicy对应。

4.3 使用目标:注解到底贴在哪?

Kotlin的@Target比Java丰富得多。Java只有类、方法、字段这些,Kotlin还细分到了属性、getter、setter、构造函数参数等等。

我曾经在做一个数据库ORM框架时,需要区分注解是加在Java字段上还是Kotlin属性上。如果不搞清楚这个,反射拿数据的时候就会拿不到。

// 常见的AnnotationTarget取值
@Target(
    AnnotationTarget.CLASS,        // 类、接口、对象
    AnnotationTarget.FUNCTION,     // 函数
    AnnotationTarget.PROPERTY,     // 属性
    AnnotationTarget.FIELD,        // 字段(Java中的field)
    AnnotationTarget.VALUE_PARAMETER, // 构造函数参数
    AnnotationTarget.EXPRESSION,   // 表达式
    AnnotationTarget.TYPE,         // 类型
    AnnotationTarget.TYPE_PARAMETER // 类型参数
)
小技巧:如果你不确定注解该用哪个目标,直接写@Target(AnnotationTarget.CLASS)最保险。等用到了再扩展。

4.4 保留策略:注解的生命周期

保留策略决定了注解能活多久。Kotlin的AnnotationRetention有三种:

  • SOURCE:只在源码中存在,编译后就被丢弃。适合做代码检查、代码生成。
  • BINARY:保留到字节码中,但运行时反射不可见。适合做字节码插桩。
  • RUNTIME:保留到运行时,可以通过反射获取。适合做运行时处理。

我刚开始学的时候,总觉得RUNTIME最强大,什么注解都用它。后来发现,如果只是做编译期代码生成,用SOURCE就够了。用RUNTIME反而会增加APK体积,因为注解信息会被打包进去。

// 编译期处理,不保留到运行时
@Retention(AnnotationRetention.SOURCE)
annotation class GenerateLayout

// 运行时反射需要
@Retention(AnnotationRetention.RUNTIME)
annotation class NeedPermission

4.5 知识体系总览

下面这张图把Kotlin注解的核心知识点串起来了。你可以把它当作一个快速参考。

Kotlin 注解 定义 annotation class 关键字 参数类型有限制 不可为空、不可传对象 元注解 @Target - 使用目标 @Retention - 保留策略 @Repeatable - 可重复 使用目标 CLASS / FUNCTION PROPERTY / FIELD VALUE_PARAMETER 保留策略 SOURCE - 源码级 BINARY - 字节码级 RUNTIME - 运行时 应用场景 编译期代码生成 运行时反射处理 权限校验 / 路由

4.6 实战:写一个完整的自定义注解

光说不练假把式。我们结合前面讲的所有知识点,写一个完整的注解示例。

// 1. 定义注解
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class LogExecutionTime(
    val tag: String = "Performance",
    val level: Int = 1
)

// 2. 使用注解
class UserRepository {
    @LogExecutionTime(tag = "DB_QUERY", level = 2)
    fun fetchUsers(): List<User> {
        // 模拟耗时操作
        Thread.sleep(100)
        return listOf()
    }
}

// 3. 运行时通过反射处理
fun processLogAnnotations(obj: Any) {
    val methods = obj::class.java.declaredMethods
    for (method in methods) {
        val annotation = method.getAnnotation(LogExecutionTime::class.java)
        if (annotation != null) {
            println("方法: ${method.name}, tag: ${annotation.tag}, level: ${annotation.level}")
        }
    }
}

核心要点回顾:

  • 定义注解用 annotation class,参数类型有限制
  • 元注解控制注解的行为:@Target@Retention@Repeatable
  • 使用目标决定注解能贴在哪,保留策略决定注解活多久
  • 编译期处理用 SOURCE,运行时反射用 RUNTIME

好了,Kotlin注解的基础就这些。说白了就是三件事:怎么定义、贴在哪、活多久。把这些搞清楚了,后面学反射和注解处理器就会顺畅很多。

公众号:蓝海资料掘金营,微信deep3321