反射 (Reflection)
反射允许程序在运行时动态地自省(Introspect)和修改自身的行为。尽管 Kotlin 100% 兼容 Java 反射,但它设计了一套独立的 K-System 反射 API,以支持 Kotlin 特有的语言特性(如属性、空安全、默认参数等)。
依赖配置与体积成本
Kotlin 的完整反射能力并未内置在标准库(stdlib)中,而是剥离在单独的 kotlin-reflect 库中。
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23")Android 开发警示
kotlin-reflect 库体积较大(约 2.5MB,混淆后约 1MB+)。对于包体积敏感的 Android 应用,应尽量避免使用全量 Kotlin 反射。 替代方案:
- 仅使用 Java 反射 (
MyClass::class.java)。 - 使用 KSP (Kotlin Symbol Processing) 进行编译时代码生成。
类引用基础 (Class References)
获取 Kotlin 类的运行时引用非常简单:
val c = MyClass::class它是轻量的
MyClass::class 仅仅是一个轻量级的对象引用,获取它不会触发复杂的反射元数据加载。只有当你真正访问 .members 或 .supertypes 时,繁重的解析过程才会发生。
核心映射体系:KClass vs Class
理解 Kotlin 反射的关键在于理解它与 Java 反射的映射关系。
| 特性 | Kotlin (KClass) | Java (Class) | 描述 |
|---|---|---|---|
| 引用获取 | MyClass::class | MyClass::class.java | .java 属性是两者转换的桥梁 |
| 属性支持 | KProperty (支持 getter/setter) | Field (仅字段) | Java 反射无法直接理解 Kotlin 属性的概念 |
| 函数调用 | KFunction (支持默认参数) | Method | Java 反射无法处理 Kotlin 的默认参数 |
| 空安全 | 运行时类型感知空标记 | 无 | KClass 能区分 String 与 String? |
高级调用实战
1. 动态访问属性 (KProperty)
KProperty 不仅能读写值,还是委托属性(Delegated Properties)的底层基石。
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)
就像函数一样,构造函数也可以作为对象传递。这在实现工厂模式或回调时非常优雅。
class User(val name: String)
// 函数类型: (String) -> User
val factory: (String) -> User = ::User
val user = factory("Viro")3. 函数调用:callBy (支持默认参数)
Java 反射的 invoke 无法处理 Kotlin 的默认参数。Kotlin 提供了 callBy 来解决此问题。
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, User4. 访问私有成员
默认情况下,反射遵循可见性规则。要访问 private 成员,必须显式开启权限。
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 关键字,允许我们在运行时保留并访问泛型类型。
// 只有内联函数支持 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 中添加保留规则。
# 保留特定类的类名和成员名不被混淆
-keep class com.example.User { *; }
# 或者保留所有被特定注解标记的类
-keep @com.example.Reflectable class * { *; }
# 如果使用了 kotlin-reflect,通常还需要保留 Metadata
-keep class kotlin.Metadata { *; }核心开发准则
- 缓存是关键:反射操作(如
KClass.members)在首次调用时涉及极重的解析和缓存初始化过程。千万不要在循环中重复获取 KClass 或 KProperty,应将其缓存为静态常量。- 性能量级参考:直接调用 (1x) < 缓存后的反射 (~2x) < 未缓存的反射 (>100x)。
- 区分 Java 与 Kotlin 反射:如果只是简单的
newInstance或获取注解,直接用clazz.java性能更好且不需要引入kotlin-reflect库。只有在需要处理属性(Property)或默认参数时才使用 Kotlin 反射。 - KMP 项目慎用:在 Kotlin Multiplatform 中,除了 JVM 平台,其他平台(JS, Native)的反射能力极弱甚至不存在。如果需要跨平台元编程,请无脑选择 KSP。