WebView exécute du code natif sur plusieurs processus pour afficher du contenu Web dans votre application Android. Si les instances WebView ne sont pas gérées, cela peut entraîner des fuites de mémoire, des plantages dus à une mémoire insuffisante et une dégradation des performances de l'application.
Ce document explique le modèle de mémoire multiprocessus de 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 de WebView
Pour gérer efficacement la mémoire de WebView, vous devez comprendre comment Android alloue les ressources pour le contenu Web :
Exécution multiprocessus : sur Android 8.0 (niveau d'API 26) ou version ultérieure,
WebViewsé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 dans lequel s'exécutent votre
Activityet votre code Java ou Kotlin. - Processus de rendu isolé : processus distinct en bac à sable (
SandboxedProcessService) qui analyse le code HTML et CSS, exécute le code JavaScript et affiche les pages Web.
- Processus hôte (navigateur) : processus principal de l'application dans lequel s'exécutent votre
Espace mémoire natif utilisé : la majeure partie de la mémoire
WebView, y compris les graphiques affichés, 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éelle utilisée par le contenu Web.Impact de la mémoire native sur le système : contrairement aux allocations de segments de mémoire Java, qui sont limitées par la limite
maxHeapde l'application et échouent rapidement avec une erreurOutOfMemoryError, la mémoire native peut augmenter silencieusement jusqu'à atteindre plusieurs 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 fonctionnement multitâche global de l'appareil avant de finir par arrêter l'application au premier plan.
Gérer le cycle de vie de WebView
Une bonne gestion 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 l'arrêt d'un Activity libère automatiquement sa mémoire.
Pour vous assurer que les références de contexte Java et les ressources de rendu natives sont complètement nettoyées, vous devez orchestrer explicitement une séquence de suppression dans le cycle de vie de votre composant hôte (comme onDestroy()), en arrêtant l'exécution de la page active, en dissociant 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 des ressources lorsque votre Activity ou Fragment est détruit, procédez comme suit :
- Supprimez
WebViewde son conteneur parent (ViewGroup). - Arrêtez le chargement actif et effacez l'historique de navigation.
- Appelez
destroy(). - 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 des vues et arrête le travail en arrière-plan sur le Web. Toutefois, vous constaterez peut-être que la mémoire physique du processus (taille de l'ensemble résident) ne revient pas immédiatement à son niveau 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ésidents 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'éviter les fuites de mémoire Activity cumulées lorsque les utilisateurs accèdent à des écrans Web et les quittent.
Métriques de débogage clés
Lorsque vous analysez la consommation de mémoire de 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é 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 le tas de mémoire natif et les allocations de mémoire d'exécution JavaScript). Cela représente le coût de mémoire principal de votre contenu Web (libellé Allocated dans le profileur Android Studio).
Espace mémoire privé utilisé (PMF) : somme du RSS anonyme et de l'espace d'échange (zRAM). La PMF reflète la charge de mémoire réelle non é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é. Un contenu Web lourd entraîne des pics principalement dans le processus du moteur de rendu.
Nombre d'objets actifs (
WebViews,Activities,Views) : nombre d'instances d'UI, de contexte et deWebViewactives conservées en mémoire. Le suivi de ces allocations permet de déterminer si l'augmentation de la mémoire est due à des références Java conservées ou à des allocations exclusivement natives.Private Other and Native Heap (Autre tas de mémoire privé et natif) : dans
dumpsys meminfo, les allocations C/C++ natives et les mappages de mémoire personnalisés (commePartitionAllocde Chromium ou les tas de mémoire d'exécution JavaScript intégrés) apparaissent sous "Native Heap" (Tas de mémoire natif) et "Private Other" (Autre tas de mémoire privé) plutôt que sous "Java Heap" (Tas de mémoire Java).
Pour en savoir plus sur les compteurs de mémoire des processus et leurs catégories, consultez le glossaire de la mémoire des 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 de
Activitylors 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 (comme RSS et RSS anonyme) afin d'observer l'augmentation globale de la mémoire. Notez que les allocations du moteur natif
WebViewne produisent pas de piles d'appels dans l'outil de profilage de tas de Perfetto. Utilisez les Outils pour les développeurs Chrome pour inspecter les instantanés de segment de mémoire JavaScript et les allocations DOM dans le contenu Web.
Inspecter le nombre d'objets actifs
Pour déterminer si l'augmentation de la mémoire est causée par des objets de framework Java conservés (tels que des composants d'interface utilisateur) 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 poignées IPC et d'allocations 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) de manière répétée et comparez les nombres :
Fuite d'instance : si
WebViewsouActivitiesaugmente à chaque navigation et ne revient pas à la valeur de référence, votre application présente une fuite de l'instance JavaWebViewou de l'hôteActivity(par exemple, en raison d'unViewGroup.removeView()manquant ou de références d'écouteur conservées). Étant donné qu'unActivityqui a fuité épingle l'intégralité de son arborescence de vues et de ses ressources d'image décodées en mémoire, les visites répétées épuisent rapidement le tas Java et provoquent des plantagesOutOfMemoryError.Fuite native ou DOM : si
WebViewsetActivitiesrestent constants alors que le RSS total du processus et Private Other continuent d'augmenter, la fuite provient de ressources natives non publiées, d'éléments DOM ou de liaisons de moteur JavaScript. Comme ces allocations résident dans la mémoire native et contournent le récupérateur de mémoire 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 arrête l'application.
Profiler le processus de moteur 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 :
Recherchez l'ID de processus (PID) du service de moteur 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:...}Inspectez la répartition de la mémoire du processus de moteur de rendu à l'aide de son PID :
adb shell dumpsys meminfo <var>RENDERER_PID</var>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 mappages et les allocations de mémoire
Pour voir quels sous-systèmes ou allocateurs natifs occupent de la mémoire anonyme, inspectez les mappages 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 anonyme courants et leur pertinence par rapport à l'augmentation de la mémoire :
| Tag de mémoire | Sous-système | Pertinence par rapport au contenu de l'application et au contenu Web | Cause courante de l'augmentation de la mémoire ? |
|---|---|---|---|
[anon:partition_alloc] |
Chromium PartitionAlloc | Allocations pour les arbres DOM, les tampons de rendu, le tas JavaScript V8 et l'exécution WebAssembly dans WebView. |
Oui (élevé) : le chargement de pages Web lourdes, de DOM riches en contenus multimédias ou l'échec de l'appel de destroy() sur des instances WebView supprimées gonfle directement cette balise. |
[anon:scudo...] ou [anon:libc_malloc] |
Allocateurs de segments de mémoire natifs Android (Scudo / jemalloc) | Allocations natives C/C++ générales utilisées par les bibliothèques NDK, les ponts JNI et les pipelines graphiques natifs. | Oui (modérée à élevée) : la croissance se produit lorsque des wrappers JNI natifs ou des dépendances C++ tierces conservent des allocations non libérées d'une navigation à l'autre. |
[anon:...] (par exemple, [anon:quickjs_heap...]) |
Scripts personnalisés ou environnements d'exécution natifs | Moteurs JavaScript intégrés, environnements d'exécution WebAssembly personnalisés ou pools de tampons natifs personnalisés. | Oui (selon le contexte) : courant dans les applications hybrides qui exécutent des moteurs de script parallèlement aux vues natives et ne parviennent pas à nettoyer les liaisons d'exécution. |
Limites des API de mémoire intégrées à l'application
Les API de mémoire intégrées (comme Debug.getMemoryInfo ou ActivityManager.getProcessMemoryInfo) ne mesurent que le processus d'appel.
En mode multiprocessus, ces API ne peuvent pas capturer la mémoire consommée par le processus de moteur de rendu isolé. Pour une évaluation précise de la mémoire totale, utilisez des outils système tels que dumpsys meminfo, Perfetto ou le profileur Android Studio.
Trier l'utilisation élevée de la mémoire dans une application hybride
Lorsque vous diagnostiquez une augmentation inexpliquée de la mémoire lors d'interactions WebView récurrentes (par exemple, 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 :
Isolez le type de fuite (Java ou native) : exécutez
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"avant et après les transitions utilisateur répétées (par exemple, l'ouverture et la fermeture d'articles Web ou le balayage de flux).- Observation : Si les nombres
ActivitiesetWebViewsrestent stables (par exemple, une ou deux instances actives), l'application ne présente pas de fuite de contextesActivityni d'instances JavaWebView.
- Observation : Si les nombres
Mesurer le delta de mémoire entre les interactions (suivi des séries temporelles) : capturez des instantanés
dumpsys meminfosur plusieurs interactions utilisateur pour calculer le taux d'allocation par transition :- Observation : Le tas de mémoire Java reste limité et sain (pic pendant l'utilisation et baisse 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 situe entièrement dans la mémoire native, en dehors de l'environnement d'exécution ART.
Les empreintes de la mémoire Java standards (
.hprof) n'indiquent aucun problème.
- Observation : Le tas de mémoire Java reste limité et sain (pic pendant l'utilisation et baisse 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 situe entièrement dans la mémoire native, en dehors de l'environnement d'exécution ART.
Les empreintes de la mémoire Java standards (
Inspecter les mappages de mémoire anonymes : examinez les mappages de mémoire du processus à l'aide d'ADB (voir Inspecter les mappages de mémoire et les allocations) :
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"- Observation : L'augmentation de la mémoire est concentrée dans les tas de mémoire
[anon:partition_alloc]ou du moteur de script intégré, et s'accompagne 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.
- Observation : L'augmentation de la mémoire est concentrée dans les tas de mémoire
Remédiation :
- Assurez-vous que chaque
WebViewrecyclé ou supprimé arrête explicitement les scripts actifs (stopLoading()), efface l'historique et appelledestroy(). - Supprimez les rappels de pont JavaScript personnalisés ou les références JNI globales associés aux vues ignorées.
- Vérifiez que
Private Otheret le RSS du processus se stabilisent après les transitions de navigation.
- Assurez-vous que chaque
Ressources supplémentaires
Pour en savoir plus sur le débogage et le profilage de la mémoire et des performances de WebView, consultez les ressources suivantes :
- Gérer la mémoire de votre application
- Présentation de la gestion de la mémoire
- Déboguer des applications Web