Gérer et diagnostiquer la mémoire WebView

WebView exécute du code natif dans plusieurs processus pour afficher du contenu Web dans votre application Android. Si vous laissez des instances WebView non gérées, cela peut entraîner des fuites de mémoire, des plantages pour mémoire insuffisante et une dégradation des performances de l'application.

Ce document explique le modèle de mémoire multiprocessus WebView, décrit comment gérer correctement son cycle de vie pour éviter les fuites et fournit des workflows pratiques pour diagnostiquer les problèmes de mémoire.

Comprendre l'architecture de la mémoire WebView

Pour gérer efficacement la mémoire WebView, vous devez comprendre comment Android alloue les ressources pour le contenu Web :

  • Exécution multiprocessus : sur Android 8.0 (niveau d'API 26) et versions ultérieures, WebView sépare le contenu Web des fonctions principales de votre application sur plusieurs processus (sur les appareils à faible RAM, il peut revenir à un seul processus) :

    • Processus hôte (navigateur) : processus principal de l'application où s'exécutent votre code Activity et votre code Java ou Kotlin.
    • Processus de rendu isolé : processus sandboxé distinct (SandboxedProcessService) qui analyse le code HTML et CSS, exécute JavaScript et affiche les pages Web.
  • Empreinte mémoire native : la majeure partie de la mémoire WebView, y compris les graphiques rendus, l'arborescence DOM et la mémoire d'exécution JavaScript, est allouée dans la mémoire native, et non dans le tas de mémoire Java. Une empreinte de la mémoire Java (.hprof) n'affiche qu'un objet wrapper Java léger et ne capture pas la mémoire réellement utilisée par le contenu Web.

  • Impact de la mémoire native sur le système : contrairement aux allocations de tas Java, qui sont plafonnées par la limite maxHeap de l'application et échouent rapidement avec un OutOfMemoryError, la mémoire native peut croître silencieusement jusqu'à atteindre des gigaoctets. Lorsque la mémoire native non libérée remplit la RAM physique et l'espace d'échange (zRAM), le Low Memory Killer (LMK) d'Android commence à arrêter les processus en arrière-plan pour récupérer de la mémoire. Cela dégrade le multitâche global de l'appareil avant de finalement arrêter l'application au premier plan.

Gérer le cycle de vie WebView

Une gestion appropriée du cycle de vie est essentielle pour éviter les fuites de mémoire. Une erreur courante consiste à supposer que la suppression d'un WebView de votre mise en page ou la fin automatique d'un Activity libèrent sa mémoire.

Pour garantir un nettoyage complet des références de contexte Java et des ressources de rendu natives, vous devez orchestrer explicitement une séquence de démontage dans le cycle de vie du composant hôte (par exemple, onDestroy()), en arrêtant l'exécution de la page active, en détachant la vue de son conteneur et en libérant les liaisons natives.

Nettoyer les instances WebView

Pour assurer un arrêt propre et libérer les ressources lorsque votre Activity ou Fragment est détruit, procédez comme suit :

  1. Supprimez WebView de son conteneur parent (ViewGroup).
  2. Arrête le chargement actif et efface l'historique de navigation.
  3. Appelez destroy().
  4. Effacez la référence à null.

L'exemple suivant montre comment nettoyer correctement un WebView :

Kotlin

override fun onDestroy() {
    myWebView?.let {
        // Remove the WebView from its parent ViewGroup.
        (it.parent as? ViewGroup)?.removeView(it)
        // Stop active loading and clear history.
        it.stopLoading()
        it.clearHistory()
        // Destroy the instance.
        it.destroy()
    }
    myWebView = null
    super.onDestroy()
}

Java

@Override
protected void onDestroy() {
    if (myWebView != null) {
        // Remove the WebView from its parent ViewGroup.
        if (myWebView.getParent() instanceof ViewGroup) {
            ((ViewGroup) myWebView.getParent()).removeView(myWebView);
        }
        // Stop active loading and clear history.
        myWebView.stopLoading();
        myWebView.clearHistory();
        // Destroy the instance.
        myWebView.destroy();
    }
    myWebView = null;
    super.onDestroy();
}

Comprendre la mémoire post-destruction

Lorsque vous appelez destroy(), le système libère le contexte Activity, nettoie les hiérarchies de vues et arrête le travail en arrière-plan du Web. Toutefois, vous remarquerez peut-être que la mémoire physique du processus (taille de l'ensemble résident) ne redescend pas immédiatement à sa valeur de référence avant WebView.

Ce comportement est normal. Les caches d'exécution natifs, les bibliothèques partagées et les pages de mémoire allouées restent résidentes dans le processus jusqu'à ce que le système d'exploitation les récupère ou que le processus se termine. L'objectif principal de destroy() est d'empêcher les fuites de mémoire Activity cumulatives lorsque les utilisateurs accèdent à des écrans Web et en sortent.

Métriques de débogage clés

Lorsque vous analysez la consommation de mémoire WebView, concentrez-vous sur les métriques suivantes :

  • Taille de l'ensemble résident (RSS) : RAM physique totale mappée dans le processus, y compris le code et les bibliothèques partagés (libellés Total dans le Profileur Android Studio).

  • RSS anonyme (RssAnon) : mémoire allouée directement par le processus qui n'est pas sauvegardée par un fichier sur le disque (comme les allocations de tas natif et d'exécution JavaScript). Cela représente le coût de mémoire principal de votre contenu Web (libellé Alloué dans le profileur Android Studio).

  • Espace mémoire privé (PMF) : somme du RSS anonyme et de l'espace d'échange (zRAM). La PMF reflète la charge mémoire réelle et inévitable que votre application impose au système.

  • PMF du navigateur par rapport au PMF du moteur de rendu : mémoire utilisée par le processus principal de votre application par rapport à la mémoire utilisée par le processus de moteur de rendu isolé. Le contenu Web lourd provoque des pics principalement dans le processus de rendu.

  • Nombre d'objets actifs (WebViews, Activities, Views) : nombre d'instances UI, Context et WebView actives conservées en mémoire. Le suivi de ces éléments permet de déterminer si la croissance de la mémoire est due à des références Java conservées ou à des allocations natives uniquement.

  • Tas natif et autre tas privé : dans dumpsys meminfo, les allocations C/C++ natives et les mappages de mémoire personnalisés (tels que les tas d'exécution JavaScript intégrés ou PartitionAlloc de Chromium) apparaissent sous "Tas natif" et "Autre tas privé" plutôt que sous "Tas Java".

Pour en savoir plus sur les compteurs de mémoire de processus et leurs catégories, consultez le glossaire sur la mémoire de processus.

Workflows de diagnostic pratiques

Étant donné que WebView fonctionne sur plusieurs processus et alloue de la mémoire native, utilisez les outils et techniques suivants pour inspecter son empreinte :

Outils de profilage et de diagnostic

Pour inspecter les allocations de mémoire et diagnostiquer les fuites, utilisez les outils suivants :

  • Profileur de mémoire Android Studio : utilisez le Profileur de mémoire pour visualiser les allocations natives, suivre les catégories de mémoire au fil du temps et détecter les fuites Activity lors des transitions d'écran.

  • Suivi de la mémoire avec Perfetto : utilisez Perfetto pour enregistrer les compteurs de mémoire au niveau du système (tels que RSS et RSS anonyme) afin d'observer la croissance globale de la mémoire. Notez que les allocations de moteur natif WebView ne produisent pas de piles d'appels dans l'outil de profilage du tas de Perfetto. Utilisez les Outils pour les développeurs Chrome pour inspecter les instantanés du tas JavaScript et les allocations DOM dans le contenu Web.

Inspecter le nombre d'objets actifs

Pour déterminer si la croissance de la mémoire est due à des objets de framework Java conservés (tels que des composants d'UI) ou à des allocations natives, inspectez la section Objects de dumpsys meminfo :

adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"

Le résultat affiche le nombre d'objets actifs :

 Objects
               Views:     142         ViewRootImpl:        1
         AppContexts:       3           Activities:        1
              Assets:      12        AssetManagers:        0
       Local Binders:      32        Proxy Binders:       45
       Parcel memory:      15         Parcel count:       30
    Death Recipients:       2             WebViews:        1

Cette section affiche le nombre d'objets de framework actifs, de handles IPC et d'allocations de Parcel. Pour les diagnostics WebView, concentrez-vous principalement sur Activities et WebViews.

Effectuez l'interaction utilisateur cible (par exemple, ouvrir et fermer un écran Web) à plusieurs reprises et comparez les nombres :

  • Fuite d'instance : si WebViews ou Activities augmentent à chaque navigation et ne reviennent pas à la ligne de base, cela signifie que votre application fuit l'instance WebView Java ou l'hôte Activity (par exemple, en raison d'une référence d'écouteur manquante ViewGroup.removeView() ou conservée). Étant donné qu'un Activity divulgué épingle l'intégralité de son arborescence de vues et de ses ressources d'images décodées en mémoire, les visites répétées épuiseront rapidement le tas Java et provoqueront des plantages OutOfMemoryError.

  • Fuite native ou DOM : si WebViews et Activities restent constants alors que le RSS du processus total et Autre privé continuent d'augmenter, la fuite provient de ressources natives non publiées, d'éléments DOM ou de liaisons du moteur JavaScript. Comme ces allocations résident dans la mémoire native et contournent le garbage collector ART, elles restent invisibles pour les outils standards de détection des fuites Java et continuent de s'accumuler jusqu'à ce que le système d'exploitation mette fin à l'application.

Profiler le processus de rendu isolé à l'aide de la CLI

L'exécution de dumpsys meminfo avec le nom du package de votre application ne génère que la mémoire du processus hôte principal. Pour inspecter le processus de moteur de rendu isolé dans lequel les pages Web sont affichées :

  1. Recherchez l'identifiant de processus (PID) du service de rendu isolé :

    adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"

    Le résultat affiche l'enregistrement du processus isolé et son PID RENDERER_PID (par exemple, 22155) :

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. Inspectez la répartition de la mémoire du processus de rendu à l'aide de son PID :

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. Inspectez le processus de l'application hôte pour évaluer l'empreinte côté navigateur :

    adb shell dumpsys meminfo <var>PACKAGE_NAME</var>

Inspecter les cartes et les allocations de mémoire

Pour savoir quels sous-systèmes ou allocateurs natifs occupent la mémoire anonyme, inspectez les cartes de mémoire du processus :

adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"

Le tableau suivant liste les tags de mémoire anonymes courants et leur pertinence par rapport à la croissance de la mémoire :

Tag de mémoire Sous-système Pertinence par rapport au contenu de l'application et du site Web Quelle est la cause la plus fréquente de l'augmentation de la mémoire ?
[anon:partition_alloc] Chromium PartitionAlloc Allocations pour les arborescences DOM, les tampons de rendu, le tas de mémoire JavaScript V8 et l'exécution WebAssembly dans WebView. Oui (élevé) : ce tag est directement gonflé par le chargement de pages Web lourdes, de DOM riches en éléments multimédias ou le fait de ne pas appeler destroy() sur les instances WebView supprimées.
[anon:scudo...] ou [anon:libc_malloc] Allocateurs de tas natifs Android (Scudo / jemalloc) Allocations natives générales C/C++ utilisées par les bibliothèques NDK, les ponts JNI et les pipelines graphiques natifs. Oui (modéré à élevé) : la croissance se produit lorsque les wrappers JNI natifs ou les dépendances C++ tierces conservent les allocations non libérées lors des navigations.
[anon:...] (par exemple, [anon:quickjs_heap...]) Scripts personnalisés ou environnements d'exécution natifs Moteurs JavaScript intégrés, runtimes WebAssembly personnalisés ou pools de mémoire tampon natifs personnalisés. Oui (selon le contexte) : courant dans les applications hybrides qui exécutent des moteurs de script à côté des vues natives et ne parviennent pas à nettoyer les liaisons d'exécution.

Limites des API de mémoire intégrées aux applications

Les API de mémoire intégrées à l'application (telles que Debug.getMemoryInfo ou ActivityManager.getProcessMemoryInfo) ne mesurent que le processus appelant. En mode multiprocessus, ces API ne peuvent pas capturer la mémoire consommée par le processus de rendu isolé. Pour obtenir une évaluation précise de la mémoire totale, utilisez des outils système tels que dumpsys meminfo, Perfetto ou le Profileur Android Studio.

Triage de la mémoire élevée dans une application hybride

Lorsque vous diagnostiquez une croissance inexpliquée de la mémoire lors d'interactions WebView récurrentes (comme l'ouverture de liens Web ou la navigation dans des flux Web), utilisez le workflow de triage suivant pour déterminer si la fuite provient de la couche Java ou du moteur natif :

  1. Isolez le type de fuite (Java ou natif) : Exécutez dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" avant et après des transitions utilisateur répétées (par exemple, ouverture et fermeture d'articles Web ou balayage de flux).

    • Observation : Si le nombre de Activities et de WebViews reste stable (par exemple, une ou deux instances actives), l'application ne présente pas de fuite de contextes Activity ni d'instances WebView Java.
  2. Mesurer le delta de mémoire entre les interactions (suivi des séries temporelles) : Capturez des instantanés dumpsys meminfo lors de plusieurs interactions utilisateur pour calculer le taux d'allocation par transition :

    • Observation : Le tas Java reste plafonné et sain (avec des pics lors de l'utilisation et des baisses après la récupération de mémoire), mais Private Other et Native Heap augmentent régulièrement de plusieurs mégaoctets par transition. Cela prouve que la fuite se trouve entièrement dans la mémoire native en dehors de l'environnement d'exécution ART. Les vidages de tas Java standards (.hprof) n'indiquent aucun problème.
  3. Inspecter les cartes de mémoire anonymes : examinez les cartes de mémoire du processus à l'aide d'ADB (voir Inspecter les cartes et les allocations de mémoire) :

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Observation : La croissance de la mémoire est concentrée dans les tas du moteur de script [anon:partition_alloc] ou intégré, accompagnée d'une lente augmentation des références JNI globales. Cela indique que, bien que les vues Java aient été remplacées, les objets de page natifs ou les liaisons JavaScript sous-jacents n'ont pas été libérés.
  4. Résolution :

    • Assurez-vous que chaque WebView recyclé ou supprimé arrête explicitement les scripts actifs (stopLoading()), efface l'historique et appelle destroy().
    • Détruisez les rappels de pont JavaScript personnalisés ou les références globales JNI associées aux vues fermées.
    • Vérifiez que Private Other et le processus RSS se stabilisent après les transitions de navigation.

Ressources supplémentaires

Pour en savoir plus sur le débogage et le profilage de la mémoire et des performances WebView, consultez les ressources suivantes :