Reflection (réflexion) est un mécanisme d'exécution qui permet au code d'inspecter sa propre structure : obtenir des classes, des méthodes, des champs et des annotations sans connaître les types au moment de la compilation. Cet outil est à la base de nombreux frameworks mobiles — la sérialisation JSON (Gson, Moshi), l'injection de dépendances (Dagger, Koin) et les runners de tests (JUnit, XCTest). Selon le Tutoriel de Reflection d'Oracle Java, 2024, reflection est un élément obligatoire de la plateforme Java, utilisé par toutes les grandes bibliothèques.
Points clés
Reflection est la capacité d'un programme à observer et à modifier sa propre structure et son comportement pendant l'exécution. Dans les langages orientés objet, cela signifie obtenir des objets Class, Method, Field et Constructor, qui représentent les éléments du programme comme des données disponibles pour la lecture et l'appel.
Le terme « reflection » a été introduit dans la communauté de l'intelligence artificielle en 1982 (Brian Cantwell Smith) et implémenté dans le langage Smalltalk. Dans le développement mobile, reflection est apparu pour la première fois dans Java ME et Objective-C (1986, NextStep). Aujourd'hui, chaque grande plateforme mobile possède sa propre API de reflection : Java/Kotlin pour Android, Objective-C Runtime pour iOS, Swift Mirror API pour Swift.
Le mécanisme de reflection repose sur les métadonnées que le compilateur enregistre dans le bytecode ou le binaire. Android stocke des informations complètes sur les classes dans les fichiers DEX, iOS dans la section __objc_classlist du segment Mach-O. Le runtime charge ces métadonnées en mémoire et fournit une API pour les parcourir.
Java Reflection API est construite autour de la classe java.lang.Class. N'importe quel objet en Java peut être converti en Class via .getClass() ou Class.forName(). À partir de Class, on extrait toutes les méthodes, champs, constructeurs, annotations et superclasses. Kotlin hérite de la reflection de Java et ajoute ses propres KClass, KFunction, KProperty du package 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("Propriété : ${prop.name}, type : ${prop.returnType}")
}
}
Dans cet exemple, KClass fournit les métadonnées de la data class User. declaredMemberProperties renvoie une liste de propriétés avec leurs types et getters. La reflection de Kotlin est étroitement intégrée aux coroutines : KFunction prend en charge le modificateur suspend, ce qui permet d'appeler des méthodes asynchrones via reflection.
La reflection de Java travaille avec Class<?>, Method.setAccessible() et Field.get(). setAccessible(true) désactive les contrôles de contrôle d'accès du langage Java pour les éléments privés. C'est un mécanisme puissant mais dangereux : sur Android à partir de l'API 28, appeler setAccessible sur des méthodes système cachées peut provoquer InaccessibleObjectException.
// Java reflection : appel d'une méthode privée
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");
Le code démontre Class.forName() — le chargement dynamique d'une classe par nom de chaîne. C'est la base des architectures de plugins : une classe peut être inconnue au moment de la compilation, mais chargée et exécutée via reflection au moment de l'exécution. getDeclaredMethod(“privateMethod”, ...) trouve une méthode par nom et types de paramètres, et invoke l'exécute.
Le runtime d'Objective-C fournit les fonctions class_copyMethodList, class_copyPropertyList, objc_getAssociatedObject. Contrairement à Java, Objective-C ne cache pas les méthodes privées par défaut — le runtime voit toutes les méthodes de la classe. Cela explique pourquoi le method swizzling fonctionne sans setAccessible : le runtime n'a pas d'encapsulation au niveau des métadonnées.
Reflection est utilisé dans les bibliothèques clés du développement mobile. La sérialisation JSON (Gson, Moshi, Kotlinx.serialization) obtient les propriétés de l'objet via reflection et les compare aux clés JSON. L'injection de dépendances (Dagger, Koin, Swinject) analyse les constructeurs et les champs pour l'injection automatique de dépendances. Les bibliothèques ORM (Room, Realm) utilisent reflection pour mapper les classes sur les tables de base de données.
Chacune de ces applications fonctionne précisément au moment de l'exécution — le code ne sait pas à l'avance avec quelles classes il va rencontrer. Reflection fournit un mécanisme universel pour surmonter cette incertitude au prix des performances et de la sécurité.
Reflection est 10 à 100 fois plus lent que les appels directs de méthodes. La raison est l'absence d'optimisations JIT (devirtualization, inlining), la vérification des types à chaque appel et le regroupement des paramètres dans Object[]/varargs. ART sur Android 14 ne peut pas optimiser en inline les appels de reflection car la méthode cible est inconnue jusqu'au moment de l'exécution.
| Opération | Appel direct | Via Reflection | Ralentissement |
|---|---|---|---|
| Appel d'une méthode sans paramètres | ~3 ns | ~120 ns | 40x |
| Lecture d'un champ int | ~1 ns | ~85 ns | 85x |
| Appel d'une méthode avec 2 paramètres | ~4 ns | ~250 ns | 62x |
| Création d'une instance via le constructeur | ~5 ns | ~180 ns | 36x |
| Résolution d'une classe par chaîne | — | ~800 ns | — |
Les données ont été obtenues sur un Google Pixel 8 (Android 14, ART). Les performances de reflection s'améliorent à chaque version d'Android : sur Android 9, un appel via Method.invoke() était 150 fois plus lent qu'un appel direct, sur Android 14, il est 40 fois. ART utilise des mécanismes intégrés de method handle pour l'optimisation.
Pour les sections critiques en performances, les développeurs remplacent reflection par la génération de code : Dagger utilise le traitement d'annotations au lieu de la recherche au moment de l'exécution, Kotlinx.serialization génère les sérialiseurs via KSP, Moshi adapte @JsonClass(generateAdapter = true) pour le codegen au moment de la compilation.
Le traitement d'annotations (KAPT, KSP) et la génération de code sont les principales alternatives à reflection dans le développement mobile. Ils déplacent l'analyse des métadonnées du moment de l'exécution au moment de la compilation : le code est généré avant le démarrage de l'application, ce qui élimine la surcharge de reflection et améliore les performances.
// KSP : génération de code au lieu de reflection
@Serializable
data class Config(
val apiUrl: String,
val timeout: Int
)
// KSP génère ConfigSerializer sans reflection
fun loadConfig(json: String): Config {
return Config.serializer().decodeFromString(json)
}
Dans cet exemple, @Serializable est l'annotation de Kotlinx.serialization. KSP (Kotlin Symbol Processing) analyse le code source au moment de la compilation, trouve toutes les classes @Serializable et génère les sérialiseurs. Pendant l'exécution de l'application, reflection n'est pas utilisé — le sérialiseur est déjà compilé en code machine.
La génération de code offre de meilleures performances, la sécurité des types et une taille de binaire réduite (la suppression du code mort élimine les dépendances de reflection inutilisées). Reflection reste nécessaire pour les tâches où les types sont inconnus au moment de la compilation : chargement dynamique de plugins, proxies au moment de l'exécution, instrumentation des tests. Selon Kotlin, Kotlinx.serialization avec KSP est 3 à 5 fois plus rapide que Gson, basé sur reflection.
Reflection sur les plateformes mobiles a des limitations de sécurité et de performances. Android à partir de l'API 28 (Pie) restreint setAccessible pour les interfaces non-SDK — tenter d'ouvrir une méthode système cachée provoque une exception ou un avertissement. iOS avec Swift ne prend pas en charge reflection au sens classique : la Swift Mirror API ne fournit que la lecture des propriétés (name, value), sans modification ni appels de méthodes.
Google Play rejette les applications qui utilisent reflection pour contourner les restrictions de la plateforme : remplacement des services système, modification des politiques SELinux, lecture des autorisations protégées. Apple bloque également les applications qui appellent des API privées via reflection — le contrôle d'App Review scanne le binaire à la recherche de signatures de chaîne de objc_msgSend avec des sélecteurs privés connus.
ProGuard/R8 est une autre limitation. L'obfuscation et la minification du code renomment les classes et les méthodes en noms courts (a, b, c). Si le code utilise Class.forName(“com.example.MyClass”), il se brisera après l'obfuscation. La solution est les keep rules dans proguard-rules.pro :
// Règles keep de ProGuard pour reflection
-keep class com.example.** { *; }
-keep class * implements java.io.Serializable { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName ;
}
-keepattributes Signature, InnerClasses, EnclosingMethod
Les règles -keep indiquent à R8 de ne pas renommer les classes utilisées via reflection. Sans ces règles, une application obfusquée plantera avec ClassNotFoundException — le runtime ne pourra pas trouver la classe par le nom de chaîne qui a changé.
Questions fréquemment posées
Oui, reflection est 10 à 100 fois plus lent qu'un appel direct. Les principales raisons : l'absence d'optimisations JIT (inlining, devirtualization), le regroupement des paramètres et la vérification des types à chaque appel. Pour le code de production, il est recommandé de remplacer reflection par la génération de code via KSP ou le traitement d'annotations.
La reflection de Java fonctionne via Class, Method, Field et exige setAccessible pour les membres privés. La reflection de Kotlin utilise KClass, KFunction, KProperty et prend en charge sealed class, data class, les coroutines (fonctions suspend) et la null-safety. La reflection de Kotlin est basée sur celle de Java, mais ajoute une API type-safe.
Ajouter des keep rules ProGuard/R8 pour les classes, méthodes et champs utilisés via reflection. Pour chaque Class.forName(), getDeclaredMethod(), getDeclaredField(), il doit y avoir une directive -keep correspondante. Des outils comme GreenDAO et Room génèrent automatiquement des keep rules.
Swift n'a pas de reflection au sens plein. Mirror API (Swift 2+) permet de lire les propriétés d'une structure ou d'une classe : nom, valeur, type. Appeler des méthodes, modifier des champs et créer des instances par type sont impossibles. Pour cela, on utilise l'Objective-C Runtime lors de l'héritage de NSObject avec @objc dynamic.
Gson (sérialisation JSON), Retrofit (création d'implémentations d'interfaces via proxy dynamique), Mockito (création de mocks), Koin (injection de dépendances), Room (vérification d'Entity au moment de la compilation via KAPT), Firebase Crashlytics (analyse de stack trace). La plupart des bibliothèques passent à la génération de code avec KSP/KAPT.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi