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,
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 où s'exécutent votre code
Activityet 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.
- Processus hôte (navigateur) : processus principal de l'application où s'exécutent votre code
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
maxHeapde l'application et échouent rapidement avec unOutOfMemoryError, 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 :
- Supprimez
WebViewde son conteneur parent (ViewGroup). - Arrête le chargement actif et efface 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 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 etWebViewactives 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 ouPartitionAllocde 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
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 (tels que RSS et RSS anonyme) afin d'observer la croissance globale de la mémoire. Notez que les allocations de moteur natif
WebViewne 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
WebViewsouActivitiesaugmentent à chaque navigation et ne reviennent pas à la ligne de base, cela signifie que votre application fuit l'instanceWebViewJava ou l'hôteActivity(par exemple, en raison d'une référence d'écouteur manquanteViewGroup.removeView()ou conservée). Étant donné qu'unActivitydivulgué é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 plantagesOutOfMemoryError.Fuite native ou DOM : si
WebViewsetActivitiesrestent 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 :
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:...}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>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 :
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
Activitieset deWebViewsreste stable (par exemple, une ou deux instances actives), l'application ne présente pas de fuite de contextesActivityni d'instancesWebViewJava.
- Observation : Si le nombre de
Mesurer le delta de mémoire entre les interactions (suivi des séries temporelles) : Capturez des instantanés
dumpsys meminfolors 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.
- 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 (
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.
- Observation : La croissance de la mémoire est concentrée dans les tas du moteur de script
Résolution :
- Assurez-vous que chaque
WebViewrecyclé ou supprimé arrête explicitement les scripts actifs (stopLoading()), efface l'historique et appelledestroy(). - 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 Otheret le processus RSS 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 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