Reflection (phản chiếu) là cơ chế runtime cho phép mã nguồn kiểm tra cấu trúc của chính nó: lấy các lớp, phương thức, trường và chú thích mà không cần biết kiểu tại thời điểm biên dịch. Công cụ này là nền tảng của nhiều framework di động — tuần tự hóa JSON (Gson, Moshi), tiêm phụ thuộc (Dagger, Koin) và trình chạy kiểm thử (JUnit, XCTest). Theo Hướng dẫn Reflection của Oracle Java, 2024, reflection là thành phần bắt buộc của nền tảng Java, được mọi thư viện lớn sử dụng.
Điểm chính
Reflection là khả năng của chương trình quan sát và sửa đổi cấu trúc cũng như hành vi của chính nó trong khi thực thi. Trong các ngôn ngữ hướng đối tượng, điều này có nghĩa là lấy các đối tượng Class, Method, Field và Constructor, biểu diễn các phần tử của chương trình dưới dạng dữ liệu sẵn sàng để đọc và gọi.
Thuật ngữ “reflection” được giới thiệu trong cộng đồng trí tuệ nhân tạo vào năm 1982 (Brian Cantwell Smith) và được triển khai trong ngôn ngữ Smalltalk. Trong phát triển di động, reflection xuất hiện lần đầu tiên trong Java ME và Objective-C (1986, NextStep). Ngày nay mọi nền tảng di động lớn đều có API reflection riêng: Java/Kotlin cho Android, Objective-C Runtime cho iOS, Swift Mirror API cho Swift.
Cơ chế reflection dựa trên siêu dữ liệu mà trình biên dịch lưu trong bytecode hoặc tệp nhị phân. Android lưu thông tin đầy đủ về các lớp trong tệp DEX, iOS lưu trong phần __objc_classlist của phân đoạn Mach-O. Runtime tải siêu dữ liệu này vào bộ nhớ và cung cấp API để duyệt qua chúng.
Java Reflection API được xây dựng quanh lớp java.lang.Class. Bất kỳ đối tượng nào trong Java đều có thể được chuyển thành Class qua .getClass() hoặc Class.forName(). Từ Class, tất cả các phương thức, trường, hàm tạo, chú thích và lớp cha được trích xuất. Kotlin kế thừa reflection của Java và bổ sung KClass, KFunction, KProperty riêng từ gói kotlin.reflect.
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("Thuộc tính: ${prop.name}, kiểu: ${prop.returnType}")
}
}
Trong ví dụ này, KClass cung cấp siêu dữ liệu cho data class User. declaredMemberProperties trả về danh sách các thuộc tính kèm kiểu và getter. Reflection của Kotlin được tích hợp chặt chẽ với coroutines: KFunction hỗ trợ bổ nghĩa suspend, cho phép gọi các phương thức bất đồng bộ qua reflection.
Reflection của Java làm việc với Class<?>, Method.setAccessible() và Field.get(). setAccessible(true) vô hiệu hóa kiểm tra kiểm soát truy cập ngôn ngữ Java đối với các phần tử private. Đây là cơ chế mạnh mẽ nhưng nguy hiểm: trên Android từ API 28, gọi setAccessible trên các phương thức hệ thống ẩn có thể gây ra InaccessibleObjectException.
// Java reflection: gọi một phương thức private
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");
Đoạn mã minh họa Class.forName() — tải lớp động theo tên chuỗi. Đây là nền tảng của kiến trúc plugin: một lớp có thể không được biết tại thời điểm biên dịch, nhưng có thể được tải và thực thi qua reflection tại runtime. getDeclaredMethod(“privateMethod”, ...) tìm phương thức theo tên và kiểu tham số, còn invoke thực thi nó.
Runtime của Objective-C cung cấp các hàm class_copyMethodList, class_copyPropertyList, objc_getAssociatedObject. Khác với Java, Objective-C không che giấu các phương thức private theo mặc định — runtime nhìn thấy tất cả các phương thức của lớp. Điều này giải thích vì sao method swizzling hoạt động mà không cần setAccessible: runtime không có tính đóng gói ở cấp độ siêu dữ liệu.
Reflection được sử dụng trong các thư viện chủ chốt của phát triển di động. Tuần tự hóa JSON (Gson, Moshi, Kotlinx.serialization) lấy thuộc tính của đối tượng qua reflection và đối chiếu với khóa JSON. Tiêm phụ thuộc (Dagger, Koin, Swinject) phân tích hàm tạo và trường để tiêm phụ thuộc tự động. Các thư viện ORM (Room, Realm) dùng reflection để ánh xạ lớp sang bảng cơ sở dữ liệu.
Mỗi ứng dụng này hoạt động chính xác tại runtime — mã nguồn không biết trước mình sẽ gặp những lớp nào. Reflection cung cấp cơ chế phổ quát để vượt qua sự không chắc chắn này, đánh đổi bằng hiệu suất và bảo mật.
Reflection chậm hơn lời gọi phương thức trực tiếp từ 10–100 lần. Nguyên nhân là thiếu tối ưu hóa JIT (devirtualization, inlining), kiểm tra kiểu trong mỗi lần gọi và đóng gói tham số vào Object[]/varargs. ART trên Android 14 không thể tối ưu hóa inline các lời gọi reflection vì phương thức đích không được biết cho đến thời điểm thực thi.
| Thao tác | Gọi trực tiếp | Qua Reflection | Chậm hơn |
|---|---|---|---|
| Gọi phương thức không tham số | ~3 ns | ~120 ns | 40x |
| Đọc trường int | ~1 ns | ~85 ns | 85x |
| Gọi phương thức có 2 tham số | ~4 ns | ~250 ns | 62x |
| Tạo đối tượng qua hàm tạo | ~5 ns | ~180 ns | 36x |
| Xác định lớp theo chuỗi | — | ~800 ns | — |
Dữ liệu thu được trên Google Pixel 8 (Android 14, ART). Hiệu suất của reflection cải thiện qua mỗi phiên bản Android: trên Android 9, lời gọi qua Method.invoke() chậm hơn gọi trực tiếp 150 lần, trên Android 14 là 40 lần. ART sử dụng cơ chế method handle tích hợp để tối ưu hóa.
Đối với các đoạn nhạy cảm về hiệu suất, nhà phát triển thay reflection bằng tạo mã nguồn: Dagger dùng xử lý chú thích thay vì tìm kiếm tại runtime, Kotlinx.serialization tạo bộ tuần tự hóa qua KSP, Moshi áp dụng @JsonClass(generateAdapter = true) cho codegen tại thời điểm biên dịch.
Xử lý chú thích (KAPT, KSP) và tạo mã nguồn là những giải pháp thay thế chính cho reflection trong phát triển di động. Chúng chuyển việc phân tích siêu dữ liệu từ runtime sang thời điểm biên dịch: mã được tạo trước khi ứng dụng khởi động, loại bỏ chi phí của reflection và cải thiện hiệu suất.
// KSP: tạo mã nguồn thay vì reflection
@Serializable
data class Config(
val apiUrl: String,
val timeout: Int
)
// KSP tạo ConfigSerializer mà không dùng reflection
fun loadConfig(json: String): Config {
return Config.serializer().decodeFromString(json)
}
Trong ví dụ này, @Serializable là chú thích của Kotlinx.serialization. KSP (Kotlin Symbol Processing) phân tích mã nguồn tại thời điểm biên dịch, tìm tất cả các lớp @Serializable và tạo ra các bộ tuần tự hóa. Trong lúc ứng dụng chạy, reflection không được sử dụng — bộ tuần tự hóa đã được biên dịch thành mã máy.
Tạo mã nguồn mang lại hiệu suất tốt hơn, an toàn kiểu và kích thước tệp nhị phân nhỏ hơn (loại bỏ mã chết sẽ xóa các phụ thuộc reflection không dùng). Reflection vẫn cần thiết cho các tác vụ mà kiểu không được biết tại thời điểm biên dịch: tải plugin động, proxy tại runtime, trang bị dụng cụ kiểm thử. Theo Kotlin, Kotlinx.serialization với KSP nhanh hơn Gson dựa trên reflection từ 3–5 lần.
Reflection trên các nền tảng di động có những hạn chế về bảo mật và hiệu suất. Android từ API 28 (Pie) hạn chế setAccessible đối với các giao diện không thuộc SDK — cố mở một phương thức hệ thống ẩn sẽ gây ra ngoại lệ hoặc cảnh báo. iOS với Swift không hỗ trợ reflection theo nghĩa cổ điển: Swift Mirror API chỉ cung cấp việc đọc thuộc tính (name, value) mà không sửa đổi hay gọi phương thức.
Google Play từ chối các ứng dụng dùng reflection để vượt qua hạn chế của nền tảng: thay thế dịch vụ hệ thống, sửa đổi chính sách SELinux, đọc các quyền được bảo vệ. Apple cũng chặn các ứng dụng gọi API riêng qua reflection — kiểm tra App Review quét tệp nhị phân để tìm chữ ký chuỗi của objc_msgSend với các bộ chọn riêng đã biết.
ProGuard/R8 là một hạn chế nữa. Việc làm rối và thu nhỏ mã nguồn đổi tên các lớp và phương thức thành tên ngắn (a, b, c). Nếu mã sử dụng Class.forName(“com.example.MyClass”), nó sẽ hỏng sau khi làm rối. Giải pháp là các quy tắc keep trong proguard-rules.pro:
// Quy tắc keep của ProGuard cho reflection
-keep class com.example.** { *; }
-keep class * implements java.io.Serializable { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName ;
}
-keepattributes Signature, InnerClasses, EnclosingMethod
Các quy tắc -keep yêu cầu R8 không đổi tên các lớp được sử dụng qua reflection. Không có các quy tắc này, ứng dụng bị làm rối sẽ gặp sự cố với ClassNotFoundException — runtime không thể tìm thấy lớp theo tên chuỗi đã thay đổi.
Các câu hỏi thường gặp
Có, reflection chậm hơn lời gọi trực tiếp từ 10–100 lần. Lý do chính: thiếu tối ưu hóa JIT (inlining, devirtualization), đóng gói tham số và kiểm tra kiểu trong mỗi lần gọi. Đối với mã sản xuất, nên thay reflection bằng tạo mã nguồn qua KSP hoặc xử lý chú thích.
Reflection của Java hoạt động qua Class, Method, Field và yêu cầu setAccessible cho các thành viên private. Reflection của Kotlin sử dụng KClass, KFunction, KProperty và hỗ trợ sealed class, data class, coroutines (hàm suspend) và null-safety. Reflection của Kotlin dựa trên reflection của Java nhưng bổ sung API an toàn kiểu.
Thêm quy tắc keep của ProGuard/R8 cho các lớp, phương thức và trường được sử dụng qua reflection. Với mỗi Class.forName(), getDeclaredMethod(), getDeclaredField() phải có chỉ thị -keep tương ứng. Các công cụ như GreenDAO và Room tự động tạo quy tắc keep.
Swift không có reflection theo nghĩa đầy đủ. Mirror API (Swift 2+) cho phép đọc các thuộc tính của struct hoặc class: tên, giá trị, kiểu. Không thể gọi phương thức, sửa đổi trường và tạo đối tượng theo kiểu. Để làm điều đó, người ta dùng Objective-C Runtime khi kế thừa từ NSObject với @objc dynamic.
Gson (tuần tự hóa JSON), Retrofit (tạo triển khai giao diện qua proxy động), Mockito (tạo mock), Koin (tiêm phụ thuộc), Room (kiểm tra Entity tại thời điểm biên dịch qua KAPT), Firebase Crashlytics (phân tích stack trace). Hầu hết các thư viện đang chuyển sang tạo mã nguồn với KSP/KAPT.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm