Gérer la mémoire de votre application

Cette page explique comment réduire de manière proactive l'utilisation de la mémoire dans votre application. Pour savoir comment le système d'exploitation Android gère la mémoire, consultez la présentation de la gestion de mémoire Android.

La mémoire vive (RAM) est une ressource précieuse dans tout environnement de développement logiciel et elle est encore plus utile sur un système d'exploitation mobile où la mémoire physique est souvent limitée. Bien qu'Android Runtime (ART) et la machine virtuelle Dalvik effectuent tous les deux une récupération de mémoire de routine, cela ne signifie pas que vous pouvez ignorer quand et où votre application alloue et libère de la mémoire. Vous devez toujours éviter les fuites de mémoire, généralement causées par la conservation de références d'objets dans des variables de membre statiques, et libérer des objets Reference au moment approprié, tels que définis par des rappels de cycle de vie.

Surveiller la mémoire disponible et l'utilisation de la mémoire

Vous devez identifier les problèmes d'utilisation de mémoire de votre application avant de pouvoir les résoudre. Le Profileur de mémoire d'Android Studio vous aide à identifier et à diagnostiquer les problèmes de mémoire de différentes manières :

  • Découvrez comment votre application alloue de la mémoire au fil du temps. Le Profileur de mémoire affiche un graphique en temps réel de la quantité de mémoire utilisée par votre application, du nombre d'objets Java alloués et de la récupération de mémoire.
  • Lancez des événements de récupération de mémoire et prenez un instantané du tas de mémoire Java pendant l'exécution de votre application.
  • Enregistrez les allocations de mémoire de votre application, inspectez tous les objets alloués, affichez la trace de la pile pour chaque allocation et accédez au code correspondant dans l'éditeur Android Studio.

Libérer la mémoire en fonction d'événements

Android peut récupérer la mémoire de votre application ou l'arrêter complètement si nécessaire afin de libérer de la mémoire pour les tâches critiques, comme expliqué dans la présentation de la gestion de mémoire. Pour équilibrer davantage la mémoire système et éviter d'arrêter le processus de votre application comme le requiert le système, vous pouvez intégrer l'interface ComponentCallbacks2 dans vos classes Activity. La méthode de rappel onTrimMemory() fournie permet à votre application d'écouter des événements liés à la mémoire lorsqu'elle est exécutée au premier plan ou en arrière-plan. Elle permet ensuite à votre application de libérer des objets en fonction du cycle de vie de l'application ou d'événements système qui indiquent que le système doit récupérer de la mémoire.

Vous pouvez implémenter le rappel onTrimMemory() pour répondre à différents événements liés à la mémoire, comme illustré dans l'exemple suivant :

Kotlin

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        // Determine which lifecycle or system event is raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

Java

import android.content.ComponentCallbacks2;
// Other import statements.

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    public void onTrimMemory(int level) {

        // Determine which lifecycle or system event is raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

Vérifier la quantité de mémoire dont vous avez besoin

Pour autoriser plusieurs processus en cours d'exécution, Android définit une limite stricte sur la taille du tas de mémoire alloué à chaque application. La limite exacte de cette taille varie selon les appareils, en fonction de la quantité de RAM dont ils disposent. Si votre application atteint sa capacité de tas de mémoire et tente d'allouer plus de mémoire, le système génère une erreur OutOfMemoryError.

Pour éviter de manquer de mémoire, vous pouvez interroger le système afin de déterminer l'espace disponible sur l'appareil actuel pour le tas de mémoire. Dans ce cas, vous pouvez interroger le système en appelant getMemoryInfo(). Cette opération renvoie un objet ActivityManager.MemoryInfo qui fournit des informations sur l'état de la mémoire actuelle de l'appareil, y compris la mémoire disponible, la mémoire totale et le seuil de mémoire (niveau de mémoire à partir duquel le système se met à arrêter les processus). L'objet ActivityManager.MemoryInfo expose également lowMemory, qui est une valeur booléenne simple qui vous indique si l'appareil manque de mémoire.

L'exemple d'extrait de code suivant montre comment utiliser la méthode getMemoryInfo() dans votre application.

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

Utiliser des constructions de code plus efficaces pour la mémoire

Certaines fonctionnalités Android, classes Java et constructions de code peuvent utiliser plus de mémoire que d'autres. Vous pouvez réduire la quantité de mémoire utilisée par votre application en choisissant des alternatives plus efficaces dans votre code.

Utiliser les services avec parcimonie

Nous vous recommandons vivement de ne pas laisser les services s'exécuter lorsque ce n'est pas nécessaire. Laisser les services s'exécuter inutilement est l'une des pires erreurs de gestion de la mémoire qu'une application Android puisse commettre. Si votre application a besoin d'un service pour fonctionner en arrière-plan, ne le laissez s'exécuter que s'il doit exécuter une tâche. Arrêtez le service lorsqu'il a terminé sa tâche. Sinon, vous risquez de provoquer une fuite de mémoire.

Lorsque vous démarrez un service, le système préfère continuer à exécuter le processus correspondant. Ce comportement rend les processus des services très gourmands, car la RAM utilisée par un service reste indisponible pour les autres processus. Cela réduit le nombre de processus mis en cache que le système peut conserver dans le cache LRU, ce qui rend le changement d'application moins efficace. Cela peut même entraîner un thrashing dans le système lorsque la mémoire est insuffisante et que le système ne peut gérer suffisamment de processus pour héberger tous les services en cours d'exécution.

En règle générale, évitez d'utiliser des services persistants, car ils demandent en permanence de la mémoire disponible. Utilisez plutôt une autre intégration, telle que WorkManager. Pour en savoir plus sur l'utilisation de WorkManager pour planifier des processus en arrière-plan, consultez la section Tâches persistantes.

Utiliser des conteneurs de données optimisés

Certaines classes fournies par le langage de programmation ne sont pas optimisées pour les appareils mobiles. Par exemple, l'intégration HashMap générique peut se révéler inefficace en mémoire, car elle a besoin d'un objet d'entrée distinct pour chaque mise en correspondance.

Le framework Android inclut plusieurs conteneurs de données optimisés, parmi lesquels SparseArray, SparseBooleanArray et LongSparseArray. Par exemple, les classes SparseArray sont plus efficaces, car elles évitent au système de compléter automatiquement la clé et parfois une valeur (ce qui crée un ou deux objet(s) de plus par entrée).

Si nécessaire, vous pouvez toujours basculer vers des tableaux bruts pour bénéficier d'une structure de données synthétique.

Attention aux abstractions de code

Les développeurs utilisent souvent les abstractions en tant que bonne pratique de programmation, car elles leur permettent d'améliorer la flexibilité et la maintenance du code. Cependant, les abstractions sont beaucoup plus coûteuses, car elles nécessitent généralement plus de code à exécuter, ce qui nécessite plus de temps et de RAM pour mapper le code en mémoire. Si vos abstractions ne sont pas vraiment bénéfiques, évitez-les.

Utiliser des tampons de protocole allégés pour les données sérialisées

Les tampons de protocole sont un mécanisme extensible indépendant du langage et de la plate-forme, conçu par Google pour sérialiser des données structurées. Semblables au format XML, ils sont plus légers, plus simples et plus rapides. Si vous utilisez des tampons de protocole pour vos données, utilisez toujours des versions allégées dans votre code côté client. Les tampons de protocole classiques génèrent un code extrêmement détaillé, ce qui peut entraîner de nombreux problèmes dans votre application, tels qu'une utilisation accrue de la RAM, une augmentation significative de la taille des APK et un ralentissement de l'exécution.

Pour en savoir plus, consultez le fichier README.

Éviter la saturation de la mémoire

Les événements de récupération de mémoire n'affectent pas les performances de votre application. Cependant, une accumulation d'événements de récupération de mémoire en peu de temps peut rapidement décharger la batterie et augmenter légèrement le temps de configuration des frames en raison des interactions nécessaires entre le récupérateur de mémoire et les threads d'application. Plus le système se consacre à la récupération de mémoire, plus la batterie se décharge vite.

Souvent, les saturations de la mémoire peuvent entraîner une multiplication d'événements de récupération de mémoire. En pratique, une saturation de la mémoire décrit le nombre d'objets temporaires alloués dans un certain laps de temps.

Par exemple, vous pouvez allouer plusieurs objets temporaires dans une boucle for. Vous pouvez également créer de nouveaux objets Paint ou Bitmap dans la fonction onDraw() d'une vue. Dans les deux cas, l'application crée beaucoup d'objets rapidement et à grande échelle. Celles-ci peuvent rapidement utiliser toute la mémoire disponible des appareils de dernière génération, ce qui provoque un événement de récupération de mémoire.

Utilisez le Profileur de mémoire pour identifier les endroits de votre code où la saturation de la mémoire est élevée avant de pouvoir les corriger.

Une fois que vous avez localisé les zones problématiques dans votre code, essayez d'y réduire le nombre d'allocations. Envisagez de déplacer les éléments depuis des boucles internes ou éventuellement de les déplacer vers une structure d'allocation basée sur une fabrique.

Vous pouvez également déterminer si les pools d'objets profitent de ce cas d'utilisation. Avec un pool d'objets, au lieu de laisser tomber une instance d'objet, vous la libérez dans un pool lorsqu'elle n'est plus nécessaire. La prochaine fois qu'une instance d'objet de ce type sera nécessaire, vous pourrez l'acquérir à partir du pool plutôt que de l'allouer.

Évaluez méticuleusement les performances pour déterminer si un pool d'objets convient à une situation donnée. Dans certains cas, les pools d'objets peuvent nuire aux performances. Même si les pools évitent les allocations, ils entraînent d'autres frais généraux. Par exemple, la maintenance du pool implique généralement une synchronisation qui a des coûts non négligeables. De plus, la suppression de l'instance d'objet mis en pool pour éviter les fuites de mémoire pendant la sortie, puis son initialisation lors de l'acquisition peuvent entraîner des frais généraux.

Le fait de bloquer plus d'instances d'objets que nécessaire dans le pool alourdit la charge de récupération de mémoire. Bien que les pools d'objets réduisent le nombre d'appels de récupération de mémoire, ils augmentent la quantité de travail nécessaire pour chaque appel, car elle est proportionnelle au nombre d'octets actifs (accessibles).

Supprimer les ressources et les bibliothèques utilisant beaucoup de mémoire

Certaines ressources et bibliothèques de votre code peuvent utiliser de la mémoire à votre insu. La taille globale de votre application, y compris les bibliothèques tierces ou les ressources intégrées, peut affecter la quantité de mémoire utilisée par votre application. Vous pouvez améliorer l'utilisation de la mémoire de votre application en supprimant de votre code tous les composants, ressources ou bibliothèques inutiles, redondants ou superflus.

Réduire la taille globale de l'APK

Vous pouvez réduire de manière significative l'utilisation de la mémoire de votre application en diminuant la taille globale de celle-ci. La taille du bitmap, les ressources, les frames d'animation et les bibliothèques tierces peuvent tous augmenter la taille de votre application. Android Studio et le SDK Android fournissent plusieurs outils pour vous aider à réduire la taille de vos ressources et des dépendances externes. Ces outils sont compatibles avec les méthodes modernes de minification de code, telles que la compilation R8.

Pour savoir comment réduire la taille globale de votre application, consultez la section Réduire la taille de votre application.

Injection de dépendances avec Hilt ou Dagger 2

Les frameworks d'injection de dépendances peuvent simplifier le code que vous écrivez et fournir un environnement adaptatif utile pour tester et modifier la configuration.

Si vous avez l'intention d'utiliser un framework d'injection de dépendances dans votre application, envisagez d'utiliser Hilt ou Dagger. Hilt est une bibliothèque d'injection de dépendances pour Android qui s'exécute sur Dagger. Dagger n'utilise pas la réflexion pour scanner le code de votre application. Vous pouvez utiliser l'implémentation statique du temps de compilation de Dagger dans des applications Android sans coûts d'exécution ni utilisation de mémoire inutiles.

D'autres frameworks d'injection de dépendances qui utilisent la réflexion initialisent des processus en scannant votre code à la recherche d'annotations. Ce processus peut nécessiter beaucoup plus de cycles de processeur et de mémoire RAM, et entraîner un retard notable au lancement de l'application.

Attention aux bibliothèques externes

Souvent, le code de bibliothèque externe n'est pas écrit pour les environnements mobiles et il peut être inefficace lorsqu'il est utilisé sur un client mobile. Lorsque vous utilisez une bibliothèque externe, vous devrez peut-être l'optimiser pour les appareils mobiles. Planifiez ce travail et analysez la bibliothèque en termes de taille de code et d'empreinte RAM avant de l'utiliser.

Même certaines bibliothèques optimisées pour mobiles peuvent poser problème en raison de leurs différentes intégrations. Par exemple, une bibliothèque peut utiliser des tampons de mémoire allégés rapides, tandis qu'une autre utilise des tampons de mémoire micro, ce qui entraîne deux intégrations de tampons différentes dans votre application. Cela peut se produire avec différentes intégrations de journalisation, d'analyse, de frameworks de chargement d'images, de mise en cache, et de bien d'autres éléments inattendus.

Bien que ProGuard puisse vous aider à supprimer les API et les ressources avec les indicateurs appropriés, il ne peut supprimer les grandes dépendances internes d'une bibliothèque. Les fonctionnalités que vous souhaitez dans ces bibliothèques peuvent nécessiter des dépendances de niveau inférieur. Cela devient particulièrement problématique lorsque vous utilisez une sous-classe Activity à partir d'une bibliothèque (qui peut avoir de nombreuses dépendances) lorsque les bibliothèques utilisent la réflexion, ce qui est courant et nécessite d'ajuster manuellement ProGuard pour qu'il fonctionne.

Évitez d'utiliser une bibliothèque partagée pour seulement une ou deux fonctionnalités sur des dizaines. N'importez pas une quantité importante de code et de frais généraux que vous n'utilisez pas. Lorsque vous envisagez d'utiliser une bibliothèque, recherchez l'intégration qui correspond le mieux à vos besoins. Sinon, créez votre propre intégration.