Fuite mémoire dans les applications mobiles — définition, causes et méthodes de détection

Auteur : IT Sectr Publié le : 2026-03-29 Temps de lecture : 9 min

Une fuite mémoire (Memory Leak) est une situation où l’application conserve des références vers des objets qui ne sont plus nécessaires, empêchant le ramasse-miettes (GC) de libérer la mémoire occupée. Selon LeakCanary, même dans les applications bien écrites, on trouve 3 à 5 fuites pour 10 000 lignes de code. Chaque fuite réduit progressivement la mémoire disponible, entraînant des ralentissements et OutOfMemoryError.

Points clés

  • Memory Leak — un objet reste en mémoire alors qu’il n’y a plus de références actives depuis la logique de l’application
  • Références statiques vers Activity ou Context — la cause la plus fréquente de fuites sous Android
  • LeakCanary — l’outil standard de détection automatique des fuites sous Android
  • WeakReference et Application Context — les techniques de base pour prévenir les fuites
  • Les composants Lifecycle-aware éliminent toute une classe de fuites liées aux abonnements

Qu’est-ce qu’une fuite mémoire

Une fuite mémoire (Memory Leak) est une situation dans laquelle un objet reste accessible via une chaîne de références fortes (Strong Reference), bien qu’il ne soit plus logiquement nécessaire à l’application. Le ramasse-miettes (GC) considère un tel objet comme vivant et ne libère pas la mémoire qu’il occupe. En conséquence, la mémoire disponible du tas (Heap) diminue constamment et la fréquence des pauses GC augmente.

Contrairement aux langages avec gestion manuelle de la mémoire (C, C++), en Java/Kotlin une fuite n’est pas un free() oublié, mais une référence oubliée. Tant qu’il existe une strong reference depuis une racine GC (GC Root) jusqu’à l’objet fuyant, le GC le considère comme nécessaire. Racines GC typiques : champs statiques, threads actifs, pile d’appels, références globales JNI.

Le danger des fuites est leur effet cumulatif. Une fuite de 100 Ko est imperceptible, mais 100 fuites de ce type occupent 10 Mo, et l’application commence à ralentir à cause des GC fréquents. Une masse critique de fuites conduit à OutOfMemoryError et au plantage de l’application. Symptômes d’une fuite : croissance constante de la consommation mémoire sur le graphique du Profileur, pauses GC fréquentes avec STW (Stop The World) et dégradation des performances de l’interface utilisateur.

Types courants de fuites dans les applications mobiles

Cinq types de fuites couvrent 95 % des cas dans le développement mobile. Chacun a sa propre cause et un motif de code caractéristique.

Référence statique à Activity ou Context

La plus connue des fuites sous Android est le stockage d’une référence statique vers une Activity ou un Context. Code typique : un champ statique Activity qui n’est pas mis à null dans onDestroy(). Tant que le champ statique vit, toute l’Activity vit avec son arbre View, qui peut occuper 1 à 10 Mo. C’est la fuite classique que LeakCanary trouve en premier.

Solution : ne stockez jamais Activity ou Context dans des champs statiques. Utilisez Application Context pour les singletons qui survivent à l’Activity. Si vous avez besoin d’une référence vers une Activity, utilisez WeakReference<Activity>.

kotlin
object MySingleton {
    private var weakActivity: WeakReference<Activity>? = null

    fun attach(activity: Activity) {
        weakActivity = WeakReference(activity)
    }
}

Classes internes avec référence implicite

Les classes anonymes et les classes internes non statiques conservent implicitement une référence vers la classe conteneur. Un Runnable passé à un Handler qui s’exécute après onDestroy() retient toute l’Activity. Un callback Retrofit qui capture une Activity fait de même. C’est le type de fuite le plus insidieux — la référence implicite n’est pas visible dans le code.

Les expressions object et les lambdas en Kotlin capturent également des références vers la classe externe. Rendez les classes internes statiques (ou de niveau supérieur en Kotlin) et passez les références externes via WeakReference. Pour les lambdas, utilisez une approche Lifecycle-aware avec viewLifecycleOwner.

Écouteurs et abonnements non résilies

L’abonnement aux services système sans résilication est une fuite directe. SensorManager, LocationManager, NotificationListener enregistrés dans onResume() sans appel à unregister dans onPause() retiennent l’Activity. De même : un Disposable RxJava non ajouté à CompositeDisposable et une coroutine lancée via GlobalScope.

Utilisez des composants Lifecycle-aware : observe() avec LifecycleOwner se désabonne automatiquement à onDestroy(). Pour RxJava — viewLifecycleOwner.lifecycle.addObserver avec DisposableObserver. Pour les coroutines — lifecycleScope.launch() est lié au cycle de vie.

kotlin
// désabonnement automatique via Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// coroutines avec lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap sans recycle

Un Bitmap occupe une quantité significative de mémoire du tas : un bitmap FullHD fait 1920 × 1080 × 4 octets = 8,3 Mo. Si un Bitmap est créé pour chaque élément de liste et que recycle() n’est pas appelé lors du masquage, la mémoire s’épuise rapidement. Sur les anciennes versions d’Android (avant 3.0), le Bitmap était stocké dans la mémoire native, mais sur les versions modernes, il se trouve dans le tas Dalvik/ART, et le GC ne peut le libérer que s’il n’y a pas de strong reference.

Utilisez Glide ou Coil pour charger les images — ces bibliothèques gèrent automatiquement la mise en cache et le recyclage. Si vous travaillez directement avec Bitmap, appelez bitmap.recycle() pour les grandes images qui ne sont plus affichées et utilisez inSampleSize pour charger des copies réduites.

Référence Fragment après onDestroyView

Un Fragment a deux cycles de vie : celui du Fragment lui-même et celui de sa View. Après onDestroyView(), l’arbre View est détruit, mais le Fragment lui-même peut rester en mémoire s’il y a une référence externe. Une erreur typique est de stocker une référence vers un Fragment dans un adaptateur ViewPager ou dans un graphe de navigation qui n’est pas effacé lors de la destruction.

Ne stockez jamais une référence vers un Fragment dans les champs d’objets à longue durée de vie. Utilisez childFragmentManager pour les fragments imbriqués et observe() avec LifecycleOwner pour le transfert de données entre eux. ViewPager2 a résolu ce problème au niveau de l’API : FragmentTransactionAdapter gère correctement le cycle de vie.

Comment détecter une fuite mémoire

La détection d’une fuite nécessite de vérifier deux faits : la mémoire ne revient pas après la durée de vie attendue et le nombre d’objets d’un certain type augmente sans diminuer. Le processus de diagnostic comprend trois étapes.

La première étape est une vérification visuelle via le Memory Profiler dans Android Studio. Ouvrez l’onglet Memory, effectuez l’action cible (ouvrez et fermez l’écran), appuyez sur GC (Garbage Collection) et vérifiez si la mémoire revient au niveau initial. Si après 3 à 4 cycles d’ouverture-fermeture la mémoire augmente constamment — il y a une fuite.

La deuxième étape est la prise d’un Heap Dump. Dans le Memory Profiler, appuyez sur Dump Java Heap. Ouvrez le fichier .hprof résultant dans Android Studio : vous verrez tous les objets dans le tas avec leurs tailles et références. Recherchez les classes dont le compte devrait être zéro après la fermeture de l’écran. Par exemple, MainActivity avec un compte de 2 après la fermeture est une fuite évidente.

La troisième étape est l’analyse du Retained Size et de la GC Root. Dans Android Studio, analysez le Retained Size : quelle quantité de mémoire sera libérée si vous supprimez cet objet. Le chemin de la GC Root à l’objet montre ce qui le retient : Static field → HashMap → Activity — et vous voyez le point de fuite. Le panneau Reference montre tous les détenteurs de l’objet.

Outils de recherche de fuites

Quatre outils couvrent la recherche de fuites de la détection automatique à l’analyse approfondie du Heap Dump.

OutilMéthodeFormat des résultats
LeakCanarySurveillance automatiqueHeap Dump + stack trace de la fuite
Android Memory ProfilerSurveillance manuelleGraphique mémoire + Heap Dump
MAT (Eclipse)Analyse approfondieRapport Dominator Tree + chemin GC Root
PerfettoTraçage systèmeChronologie + mémoire native

LeakCanary est indispensable pour tout projet Android. Il détecte automatiquement les fuites à la fin du cycle de vie Activity/Fragment et montre l’emplacement exact de la fuite avec une stack trace. Intégration : une ligne dans build.gradle. LeakCanary 2.x ne nécessite pas d’initialisation manuelle — il enregistre automatiquement l’Application Watcher.

Comment prévenir les fuites mémoire

La prévention des fuites est intégrée au processus de développement via un ensemble de règles et d’outils qui vérifient le code à chaque étape.

Règle des références fortes

Ne stockez jamais une référence vers une Activity, un Fragment ou une View dans un champ statique, un singleton ou un objet à longue durée de vie. Si la référence est inévitable, utilisez WeakReference ou stockez les données via ViewModel, qui vit exactement aussi longtemps que nécessaire et ne retient pas une View directement.

Architecture Lifecycle-aware

ViewModel et LiveData d’Android Architecture Components résolvent le problème du cycle de vie au niveau architectural. ViewModel survit à la rotation de l’écran et ne contient pas de références vers View. LiveData désabonne automatiquement l’observateur à onDestroy(). Utilisez-les au lieu d’un abonnement manuel aux services système.

Revue de code centrée sur GC Root

Lors de la revue de code, faites attention aux : champs statiques avec des types Context/View, classes anonymes, lambdas capturant une Activity, abonnements manuels, RxJava Disposable sans composite, stockage de Fragment via Bundle. En Kotlin, vérifiez supplémentairement les coroutines lancées sans liaison au cycle de vie.

Vérification automatique en CI

LeakCanary peut fonctionner dans le cadre du pipeline de test : exécutez des tests d’acceptation avec LeakCanary et faites échouer la construction si une fuite est trouvée. Cela empêche les fuites d’atteindre la production. Complétez la vérification avec la règle StaticFieldLeak d’Android Lint — elle trouve les fuites potentielles au niveau de l’analyse statique.

kotlin
// LeakCanary dans les tests
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // échouer s’il y a une fuite
    }
}

Foire aux questions

Quelle est la différence entre une fuite mémoire et OutOfMemoryError ?

La fuite est la cause, et OutOfMemoryError est la conséquence. Une seule fuite ne conduit pas à OOM, mais l’accumulation de dizaines de fuites épuise le Heap. OOM est une exception fatale, tandis qu’une fuite est un motif qui y mène avec le temps.

Comment trouver une fuite sans LeakCanary ?

Via le Android Memory Profiler : ouvrez et fermez l’écran 5 fois, après chaque fermeture invoquez GC. Si la mémoire ne revient pas au niveau de base — il y a une fuite. Prenez un Heap Dump et trouvez dans la liste la classe Activity dont le compte est supérieur à 0 après la fermeture.

Kotlin peut-il prévenir les fuites au niveau du langage ?

Partiellement. Kotlin résout le problème de null-safety mais ne gère pas les strong references. Les coroutines avec lifecycleScope et viewModelScope préviennent les fuites des tâches d’arrière-plan, tandis que sealed class et data class réduisent le nombre d’états menant aux fuites. La protection principale vient des motifs architecturaux, pas des fonctionnalités du langage.

Pourquoi LeakCanary trouve-t-il une fuite qui n’existe pas ?

LeakCanary donne parfois des faux positifs : un objet peut être temporairement retenu par le système (par exemple, InputMethodManager retient la dernière View). Vérifiez manuellement : si la Retained Size est < 1 Ko et que la GC Root est un service système, il s’agit probablement d’un faux positif.

Les fuites mémoire existent-elles seulement sur Android ?

Non. Les fuites sont possibles sur toute plateforme avec un GC : iOS (Swift/Objective-C), Flutter (Dart), navigateurs web (JavaScript). Les mécanismes sont les mêmes — strong reference depuis une GC Root. Sur iOS, ARC gère automatiquement la mémoire, mais les cycles de rétention (retain cycles) entre objets créent la même fuite.

Résumé

  • Memory Leak — un objet que le GC ne peut pas libérer à cause d’une strong reference oubliée
  • Références statiques à Activity et Context — la cause la plus fréquente de fuites
  • Références implicites via les classes anonymes, lambdas et abonnements RxJava sont plus insidieuses que les explicites
  • LeakCanary trouve automatiquement les fuites et montre la stack trace exacte
  • Les composants Lifecycle-aware (ViewModel, LiveData, lifecycleScope) éliminent une classe de fuites
  • Heap Dump et analyse Retained Size — la principale méthode de diagnostic manuel
  • La prévention inclut une revue de code centrée sur les strong references et une vérification CI avec LeakCanary

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.

Discuter du projet

Lisez aussi