Skip to content

反射 (Reflection)

源:Reflection

反射允许程序在运行时动态地自省(Introspect)和修改自身的行为。尽管 Kotlin 100% 兼容 Java 反射,但它设计了一套独立的 K-System 反射 API,以支持 Kotlin 特有的语言特性(如属性、空安全、默认参数等)。

依赖配置与体积成本

Kotlin 的完整反射能力并未内置在标准库(stdlib)中,而是剥离在单独的 kotlin-reflect 库中。

kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23")

Android 开发警示

kotlin-reflect 库体积较大(约 2.5MB,混淆后约 1MB+)。对于包体积敏感的 Android 应用,应尽量避免使用全量 Kotlin 反射。 替代方案:

  1. 仅使用 Java 反射 (MyClass::class.java)。
  2. 使用 KSP (Kotlin Symbol Processing) 进行编译时代码生成。

类引用基础 (Class References)

获取 Kotlin 类的运行时引用非常简单:

kotlin
val c = MyClass::class

它是轻量的

MyClass::class 仅仅是一个轻量级的对象引用,获取它不会触发复杂的反射元数据加载。只有当你真正访问 .members.supertypes 时,繁重的解析过程才会发生。

核心映射体系:KClass vs Class

理解 Kotlin 反射的关键在于理解它与 Java 反射的映射关系。

特性Kotlin (KClass)Java (Class)描述
引用获取MyClass::classMyClass::class.java.java 属性是两者转换的桥梁
属性支持KProperty (支持 getter/setter)Field (仅字段)Java 反射无法直接理解 Kotlin 属性的概念
函数调用KFunction (支持默认参数)MethodJava 反射无法处理 Kotlin 的默认参数
空安全运行时类型感知空标记KClass 能区分 StringString?

高级调用实战

1. 动态访问属性 (KProperty)

KProperty 不仅能读写值,还是委托属性(Delegated Properties)的底层基石。

kotlin
data class User(val name: String, var age: Int)

val user = User("Viro", 18)

// 动态读取
val nameProp = User::name
println(nameProp.get(user)) // "Viro"

// 动态写入 (仅限 var)
val ageProp = User::age as KMutableProperty1<User, Int>
ageProp.set(user, 20)

2. 构造函数引用 (Constructor References)

就像函数一样,构造函数也可以作为对象传递。这在实现工厂模式或回调时非常优雅。

kotlin
class User(val name: String)

// 函数类型: (String) -> User
val factory: (String) -> User = ::User

val user = factory("Viro")

3. 函数调用:callBy (支持默认参数)

Java 反射的 invoke 无法处理 Kotlin 的默认参数。Kotlin 提供了 callBy 来解决此问题。

kotlin
fun greet(msg: String, prefix: String = "Hi") {
    println("$prefix, $msg")
}

val func = ::greet

// ❌ 使用 call:必须提供所有参数,不支持默认值
// func.call("User") -> 抛出异常

// ✅ 使用 callBy:支持默认参数
val param = func.parameters.find { it.name == "msg" }!!
func.callBy(mapOf(param to "User")) 
// 输出:Hi, User

4. 访问私有成员

默认情况下,反射遵循可见性规则。要访问 private 成员,必须显式开启权限。

kotlin
class Secret {
    private val code = "123"
}

val prop = Secret::class.declaredMemberProperties
    .find { it.name == "code" }!!

prop.isAccessible = true // ⭐️ 暴力破解可见性
println(prop.get(Secret()))

实化类型参数 (Reified Type Parameters)

在 JVM 上,泛型在运行时会被擦除。但 Kotlin 的 inline 函数配合 reified 关键字,允许我们在运行时保留并访问泛型类型。

kotlin
// 只有内联函数支持 reified
inline fun <reified T> isType(value: Any): Boolean {
    // 这里的 T 在运行时是真实存在的 KClass
    return value is T
}

// 获取泛型的 KType (比 KClass 更丰富,包含泛型参数信息)
inline fun <reified T> getTypeInfo() {
    val type: KType = typeOf<T>()
    println(type)
}

Android 混淆 (R8/Proguard) 避坑

反射是基于字符串名称来查找类和成员的。如果 R8 开启了混淆,类名 User 可能会变成 a.b.c,属性 name 变成 n。这会导致反射调用抛出 NoSuchMethodException

解决方案:在 proguard-rules.pro 中添加保留规则。

proguard
# 保留特定类的类名和成员名不被混淆
-keep class com.example.User { *; }

# 或者保留所有被特定注解标记的类
-keep @com.example.Reflectable class * { *; }

# 如果使用了 kotlin-reflect,通常还需要保留 Metadata
-keep class kotlin.Metadata { *; }

核心开发准则

  1. 缓存是关键:反射操作(如 KClass.members)在首次调用时涉及极重的解析和缓存初始化过程。千万不要在循环中重复获取 KClass 或 KProperty,应将其缓存为静态常量。
    • 性能量级参考:直接调用 (1x) < 缓存后的反射 (~2x) < 未缓存的反射 (>100x)。
  2. 区分 Java 与 Kotlin 反射:如果只是简单的 newInstance 或获取注解,直接用 clazz.java 性能更好且不需要引入 kotlin-reflect 库。只有在需要处理属性(Property)或默认参数时才使用 Kotlin 反射。
  3. KMP 项目慎用:在 Kotlin Multiplatform 中,除了 JVM 平台,其他平台(JS, Native)的反射能力极弱甚至不存在。如果需要跨平台元编程,请无脑选择 KSP