Gérer la mémoire de votre application

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

La mémoire vive (RAM) est une ressource précieuse dans tout environnement de développement logiciel, mais 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.

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.

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

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

  1. 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.
  2. 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.
  3. Enregistrez les allocations de mémoire de votre application, puis 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

Comme décrit dans la présentation de la gestion de mémoire Android, Android peut récupérer la mémoire de votre application de plusieurs manières ou fermer celle-ci si nécessaire afin de libérer de la mémoire pour des tâches critiques. Pour équilibrer davantage la mémoire système et éviter de supprimer le processus de l'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 lorsque votre application est au premier plan ou en arrière-plan, puis 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.

Par exemple, vous pouvez intégrer le rappel onTrimMemory() pour répondre à différents événements liés à la mémoire, comme illustré ici :

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 was raised.
     */
    override fun onTrimMemory(level: Int) {

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

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

                   The user interface has moved to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory that 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 will
                   begin killing 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 will be one of
                   the first to be terminated.
                */
            }

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

                  The app received 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 was raised.
     */
    public void onTrimMemory(int level) {

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

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved 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 that 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 will
                   begin killing 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 will be one of
                   the first to be terminated.
                */

                break;

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

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

Le rappel onTrimMemory() a été ajouté dans Android 4.0 (niveau 14 d'API). Pour les versions antérieures, vous pouvez utiliser onLowMemory(), qui correspond à peu près à l'événement TRIM_MEMORY_COMPLETE.

Vérifier la quantité de mémoire à utiliser

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 taille de tas de mémoire varie selon les appareils, en fonction de la quantité de RAM dont ils disposent. Si votre application a atteint la capacité des segments de mémoire et tente d'allouer davantage 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(). 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 une valeur booléenne simple, lowMemory, qui vous indique si l'appareil manque de mémoire.

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

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see 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 to see 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

Laisser un service s'exécuter lorsqu'il n'est pas nécessaire constitue 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 effectuer des tâches en arrière-plan, ne la laissez pas s'exécuter, sauf si elle doit exécuter une tâche. N'oubliez pas d'arrêter le service lorsqu'il a terminé sa tâche. Sinon, vous risquez sans le vouloir 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 général, é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 JobScheduler. Pour savoir comment planifier des processus en arrière-plan à l'aide de JobScheduler, consultez Optimisations en arrière-plan.

Si vous devez utiliser un service, le meilleur moyen de limiter sa durée de vie consiste à utiliser un élément IntentService, qui s'arrête automatiquement dès qu'il a terminé de traiter l'intent qui l'a démarré. Pour en savoir plus, consultez Exécution dans un service d'arrière-plan.

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 très synthétique.

Attention aux abstractions de code

Les développeurs utilisent souvent les abstractions simplement en tant que bonne pratique de programmation, car elles leur permettent d'améliorer la flexibilité et la maintenance du code. Cependant, les abstractions ont un coût important : elles nécessitent généralement beaucoup plus de code à exécuter, ce qui requiert davantage de temps et de mémoire RAM pour que ce code soit mappé dans la mémoire. Par conséquent, si vos abstractions ne présentent pas un vrai avantage, é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 simples et plus rapides. Si vous décidez d'utiliser des tampons de protocole pour vos données, vous devez toujours utiliser 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 types de 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 la section "Version simplifiée" du fichier Lisez-moi.

Éviter la saturation de la mémoire

Comme indiqué précédemment, 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.

Bien sûr, vous devez identifier dans votre code les endroits où la perte de mémoire est élevée avant de pouvoir les corriger. Pour ce faire, utilisez Memory Profiler dans Android Studio.

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.

Une autre possibilité consiste à évaluer si les pools d'objets tirent avantage 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, elle pourra être acquise auprès du pool plutôt qu'allouée.

Une évaluation complète des performances est essentielle 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 peut entraîner des frais généraux. Enfin, la soumission d'un plus grand nombre d'instances d'objets que souhaité dans le pool alourdit la charge sur le récupérateur de mémoire. Bien que les pools d'objets réduisent le nombre d'appels du récupérateur de mémoire, ils augmentent la quantité de travail à effectuer à 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 engloutir 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 réduction du code, telles que la compilation R8. (Android Studio version 3.3 et antérieures utilisent ProGuard au lieu de la compilation R8.)

Pour savoir comment réduire la taille globale de votre application, consultez le guide sur la réduction de la taille de votre application.

Injection de dépendances avec 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 Dagger 2. Dagger n'utilise pas la réflexion pour scanner le code de votre application. L'intégration statique de temps de compilation de Dagger permet une utilisation 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 tendent à initialiser 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. Si vous décidez d'utiliser une bibliothèque externe, vous devrez peut-être l'optimiser pour les appareils mobiles. Planifiez ce travail à l'avance et analysez la bibliothèque en termes de taille de code et d'empreinte RAM avant de décider 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 aura tendance à avoir de nombreuses dépendances) lorsque les bibliothèques utilisent la réflexion (ce qui est courant et signifie que vous devez passez beaucoup de temps à configurer manuellement ProGuard pour qu'il fonctionne), etc.

Évitez également 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.