Reflection(反射)——一种运行时机制,允许代码检查自身结构:在编译阶段无需了解类型即可获取类、方法、字段和注解。该工具是许多移动框架的基础——JSON序列化(Gson、Moshi)、依赖注入(Dagger、Koin)和测试运行器(JUnit、XCTest)。根据Oracle Java Reflection Tutorial, 2024,reflection是Java平台的必备元素,所有大型库都在使用它。
要点
Reflection——程序在运行期间观察和修改自身结构及行为的能力。在面向对象语言中,这意味着获取Class、Method、Field和Constructor对象,它们将程序元素表示为可供读取和调用的数据。
“reflection”一词于1982年在人工智能领域被引入(Brian Cantwell Smith),并在Smalltalk语言中实现。在移动开发中,reflection首次出现在Java ME和Objective-C(1986年,NextStep)中。如今每个大型移动平台都有自己的reflection API:Android使用Java/Kotlin,iOS使用Objective-C Runtime,Swift使用Swift Mirror API。
Reflection机制基于编译器在字节码或二进制文件中保存的元数据。Android将关于类的完整信息保存在DEX文件中,iOS则保存在Mach-O的__objc_classlist段中。Runtime将这些元数据加载到内存中,并提供用于遍历它们的API。
Java Reflection API围绕java.lang.Class类构建。Java中的任何对象都可以通过.getClass()或Class.forName()转换为Class。从Class中可以提取所有方法、字段、构造器、注解和父类。Kotlin继承Java反射,并添加了来自kotlin.reflect包的KClass、KFunction、KProperty。
import kotlin.reflect.full.declaredMemberFunctions
data class User(
val name: String,
val email: String
)
fun inspectClass() {
val kClass = User::class
val properties = kClass.declaredMemberProperties
val functions = kClass.declaredMemberFunctions
properties.forEach { prop ->
println("属性:${prop.name},类型:${prop.returnType}")
}
}
在此示例中,KClass提供data class User的元数据。declaredMemberProperties返回属性列表及其类型和getter。Kotlin反射与协程紧密集成:KFunction支持suspend修饰符,从而可以通过反射调用异步方法。
Java反射与Class<?>、Method.setAccessible()和Field.get()一起工作。setAccessible(true)会关闭private元素的Java语言访问控制检查。这是一个强大但危险的机制:在Android上,从API 28开始,对隐藏系统方法调用setAccessible可能导致InaccessibleObjectException。
// Java反射:调用私有方法
Class> clazz = Class.forName("com.example.MyClass");
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getDeclaredMethod("privateMethod", String.class);
method.setAccessible(true);
method.invoke(instance, "reflection test");
该代码演示了Class.forName()——按字符串名称动态加载类。这是插件架构的基础:类可能在编译阶段未知,但在运行时通过反射加载并执行。getDeclaredMethod(„privateMethod”,...)按名称和参数类型查找方法,invoke则执行它。
Objective-C runtime提供class_copyMethodList、class_copyPropertyList、objc_getAssociatedObject函数。与Java不同,Objective-C默认不隐藏私有方法——运行时可以看到类的所有方法。这解释了为什么method swizzling不需要setAccessible:运行时在元数据层面没有封装。
Reflection被用于移动开发的关键库中。JSON序列化(Gson、Moshi、Kotlinx.serialization)通过反射获取对象属性,并将其与JSON键对应。依赖注入(Dagger、Koin、Swinject)分析构造器和字段以自动注入依赖。ORM库(Room、Realm)使用反射将类映射到数据库表。
这些应用中的每一个都恰恰在运行时工作——代码事先不知道会遇到哪些类。Reflection提供了一种通用机制来应对这种未知,代价是性能和安全性。
Reflection比直接调用方法慢10–100倍。原因——缺少JIT优化(devirtualization、inlining)、每次调用时的类型检查以及参数被打包到Object[]/varargs中。Android 14上的ART无法内联优化reflection调用,因为目标方法在执行前是未知的。
| 操作 | 直接调用 | 通过Reflection | 变慢 |
|---|---|---|---|
| 调用无参数方法 | ~3 ns | ~120 ns | 40x |
| 读取int字段 | ~1 ns | ~85 ns | 85x |
| 调用带2个参数的方法 | ~4 ns | ~250 ns | 62x |
| 通过构造器创建实例 | ~5 ns | ~180 ns | 36x |
| 根据字符串确定类 | — | ~800 ns | — |
数据是在Google Pixel 8(Android 14、ART)上获取的。Reflection的性能随着每个Android版本而改善:在Android 9上,通过Method.invoke()的调用比直接调用慢150倍,在Android 14上——40倍。ART使用内置的method handle机制进行优化。
对于性能关键的部分,开发人员用代码生成替代reflection:Dagger使用注解处理而不是运行时搜索,Kotlinx.serialization通过KSP生成序列化器,Moshi为编译时codegen适配@JsonClass(generateAdapter = true)。
注解处理(KAPT、KSP)和代码生成——移动开发中reflection的主要替代方案。它们将元数据分析从运行时转移到编译时:代码在应用启动前生成,从而消除了reflection开销并提高了性能。
// KSP:用代码生成替代反射
@Serializable
data class Config(
val apiUrl: String,
val timeout: Int
)
// KSP在无需反射的情况下生成ConfigSerializer
fun loadConfig(json: String): Config {
return Config.serializer().decodeFromString(json)
}
在此示例中,@Serializable是Kotlinx.serialization的注解。KSP(Kotlin符号处理)在编译阶段分析源代码,找到所有@Serializable类并生成序列化器。在应用运行期间不使用reflection——序列化器已被编译为机器代码。
代码生成提供更好的性能、类型安全性和更小的二进制文件大小(死代码消除会移除未使用的reflection依赖)。对于编译阶段类型未知的任务,reflection仍然是必需的:动态加载插件、运行时代理、测试插桩。根据Kotlin的数据,使用KSP的Kotlinx.serialization比基于reflection的Gson快3–5倍。
Reflection在移动平台上具有安全性和性能限制。Android从API 28(Pie)开始限制非SDK接口的setAccessible——尝试打开隐藏的系统方法会导致异常或警告。iOS的Swift不支持传统意义上的reflection:Swift Mirror API只提供属性读取(name、value),不能修改或调用方法。
Google Play拒绝使用reflection绕过平台限制的应用:替换系统服务、修改SELinux策略、读取受保护的权限。Apple也会阻止通过reflection调用私有API的应用——App Review检查会扫描二进制文件中带有已知私有选择器的objc_msgSend字符串签名。
ProGuard/R8——另一个限制。代码混淆和压缩将类和方法重命名为短名称(a、b、c)。如果代码使用Class.forName(„com.example.MyClass”),混淆后将无法工作。解决方案——proguard-rules.pro中的keep规则:
// 用于reflection的ProGuard keep规则
-keep class com.example.** { *; }
-keep class * implements java.io.Serializable { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName ;
}
-keepattributes Signature, InnerClasses, EnclosingMethod
-keep规则告诉R8不要重命名通过reflection使用的类。没有这些规则,混淆后的应用会因ClassNotFoundException而崩溃——运行时无法根据已更改的字符串名称找到类。
常见问题
是的,reflection比直接调用慢10–100倍。主要原因:缺少JIT优化(inlining、devirtualization)、参数打包和每次调用时的类型检查。对于生产代码,建议通过KSP或注解处理用代码生成替代reflection。
Java反射通过Class、Method、Field工作,并且对private成员需要setAccessible。Kotlin反射使用KClass、KFunction、KProperty,并支持sealed class、data class、协程(suspend函数)和null-safety。Kotlin反射基于Java反射,但增加了类型安全的API。
为通过reflection使用的类、方法和字段添加ProGuard/R8 keep规则。每个Class.forName()、getDeclaredMethod()、getDeclaredField()都必须有相应的-keep指令。像GreenDAO和Room这样的工具会自动生成keep规则。
Swift没有完全意义上的reflection。Mirror API(Swift 2+)允许读取结构体或类的属性:名称、值、类型。调用方法、修改字段和按类型创建实例是不可能的。为此,在从NSObject继承并带有@objc dynamic时使用Objective-C Runtime。
Gson(JSON序列化)、Retrofit(通过动态代理创建接口实现)、Mockito(创建mock)、Koin(依赖注入)、Room(在编译阶段通过KAPT检查Entity)、Firebase Crashlytics(堆栈跟踪分析)。大多数库正在转向使用KSP/KAPT的代码生成。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。