WebView et mémoire

WebView est un composant puissant qui vous permet d'afficher du contenu Web dans votre application Android. Toutefois, comme il s'agit essentiellement d'un moteur de navigateur complet (Chromium), il occupe une quantité de mémoire importante et possède une architecture multi-processus complexe.

Informations techniques : architecture multi-processus

Sur les appareils Android modernes, WebView utilise un modèle multi-processus pour améliorer la sécurité et la stabilité. Lorsque votre application utilise un composant WebView, la mémoire est répartie entre différents processus :

  1. Processus du navigateur (processus de l'application) : il s'agit du processus principal de votre application. Il contient l'objet Java WebView et la partie "navigateur" du moteur Chromium. Ce processus gère l'interface utilisateur, les requêtes réseau et le rendu GPU (intégré directement au pipeline de rendu HWUI Android). Contrairement à Chrome, WebView ne dispose pas d'un processus GPU distinct.
  2. Processus de rendu : ce processus est responsable de l'analyse du code HTML, de l'exécution du code JavaScript et de la mise en page. Il est isolé du reste du système pour des raisons de sécurité. Actuellement, les applications n'obtiennent qu'un seul processus de rendu pour tous les composants WebView (à l'exception de quelques cas particuliers rares), contrairement à Chrome qui utilise souvent des processus de rendu distincts pour différents sites.

Architecture WebView

Utilité pour la mémoire

Lorsque vous utilisez dumpsys meminfo <your_package>, vous ne voyez que la mémoire utilisée par le processus du navigateur (processus de votre application). La mémoire utilisée par le processus de rendu est comptabilisée séparément.

Dans le processus du navigateur, la mémoire WebView est répartie comme suit :

  • Tas Java : contient le wrapper Java WebView et les objets associés.
  • Tas natif : contient les structures de données internes , les caches et l'état du moteur de navigateur Chromium. Notez qu'en raison de l'utilisation de PartitionAlloc, certaines allocations natives WebView peuvent ne pas être comptabilisées sous "Native Heap" (Tas natif) dans dumpsys meminfo et peuvent apparaître sous "Other" (Autre) ou "Unknown" (Inconnu).
  • Mémoire partagée : utilisée pour partager des tampons graphiques et d'autres données. Elle peut ne pas être clairement classée par dumpsys meminfo.

Outils de dépannage

Outils pour les développeurs Chrome

L'outil le plus puissant pour analyser la mémoire dans WebView (le processus de rendu) est celui des outils pour les développeurs Chrome.

  1. Activez le débogage WebView dans votre application :

    // NOTE: In production, this should be gated behind a developer setting
    // or only enabled for debuggable builds to prevent reverse engineering.
    WebView.setWebContentsDebuggingEnabled(true);
    
  2. Connectez votre appareil à l'aide d'un câble USB.

  3. Ouvrez Chrome sur votre machine hôte et accédez à chrome://inspect/#devices.

  4. Recherchez votre application, puis cliquez sur inspect (inspecter).

  5. Dans la fenêtre des outils pour les développeurs, accédez à l'onglet Memory (Mémoire) pour prendre des instantanés du tas ou enregistrer des chronologies d'allocation pour le tas JavaScript.

dumpsys meminfo

Utilisez adb shell dumpsys meminfo --all <package> pour afficher une répartition de la mémoire. Recherchez la catégorie WebView dans le résultat et le nombre d'objets.

Profiler le moteur de rendu

Étant donné que le moteur de rendu s'exécute dans un processus distinct, vous ne pouvez pas profiler son tas natif en profilant simplement votre application. Vous devez identifier spécifiquement le PID du processus de rendu.

Pour identifier le PID de rendu correct lorsque plusieurs composants WebView sont actifs :

  1. Utilisez dumpsys activity :

    adb shell dumpsys activity processes <your_package_name>
    

    Recherchez la section mConnections. Vous verrez un ConnectionRecord qui associe votre application à un SandboxedProcessService. Le PID de ce processus est votre moteur de rendu. Exemple :

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. Vérifiez les noms des processus : les processus de rendu sont généralement nommés com.google.android.webview:sandboxed_processX ou similaire. Si une seule application utilise un composant WebView, il n'y en aura probablement qu'un seul.

Une fois que vous disposez du PID, vous pouvez le profiler à l'aide de heapprofd.

Bonnes pratiques pour la mémoire WebView

Destruction explicite

Les applications sont censées appeler WebView.destroy() pour indiquer quand elles ont terminé avec une instance.

Bien que WebView tente de s'assurer que les instances peuvent être collectées comme déchets et libérer automatiquement toutes leurs ressources, il est difficile de le garantir dans 100% des cas. Même lorsque la récupération automatique de mémoire fonctionne, elle peut être considérablement retardée, ce qui fait que l'application conserve les ressources beaucoup plus longtemps que prévu.

Si une application appelle WebView.destroy() au moment approprié (par exemple, dans Activity.onDestroy()), la conservation d'une référence à l'objet WebView lui-même ne fuira aucune ressource native importante. Il n'est pas strictement nécessaire de définir des références nulles à l'objet WebView dans les champs d'activité après sa destruction, car il sera nettoyé lorsque l'activité elle-même sera collectée comme déchets.

Exercices : pratique avec la mémoire WebView

Exercice 1 : observer l'empreinte multi-processus

  1. Lancez MemoryLab et effectuez une mesure de référence de la mémoire de votre application :

    adb shell dumpsys meminfo com.android.memorylab
    

    Exemple de référence (rango) : TOTAL PSS: 18915 KB

  2. Appuyez sur Launch WebView (Normal) (Lancer WebView [Normal]).

  3. Dans WebView, appuyez plusieurs fois sur Allocate JS Memory (1000 DIVs) (Allouer de la mémoire JS [1 000 DIV]).

  4. Vérifiez à nouveau la mémoire de l'application :

    adb shell dumpsys meminfo com.android.memorylab
    
  5. Notez que la mémoire du processus de votre application n'augmente pas de manière significative par rapport à la référence. En effet, les éléments DOM se trouvent dans le processus de rendu.

  6. Recherchez le processus de rendu :

    adb shell ps -A | grep webview | grep sandboxed
    

    Exemple de résultat :

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. Vérifiez la mémoire du processus de rendu (à l'aide de son PID) :

    adb shell dumpsys meminfo 14227
    
  8. Observez le PSS TOTAL élevé du processus de rendu. Dans notre exemple d'exécution, il est passé à ~55 Mo après quelques allocations. Notez que les allocations JavaScript (gérées par le moteur V8) contribuent généralement aux sections Private Other (Autre privé) ou Unknown (Inconnu) (mmap) de dumpsys meminfo, plutôt qu'au tas Dalvik.

Exercice 2 : fuite WebView côté Java

Une erreur courante consiste à conserver une instance WebView dans un champ statique ou dans un objet à longue durée de vie qui fuit. Étant donné que l'objet WebView est une "ancre" lourde qui conserve les ressources natives et potentiellement des processus de rendu entiers, sa fuite est très coûteuse.

Impact de la fuite WebView

  1. Dans MemoryLab, appuyez sur Launch WebView (Java Leak) (Lancer WebView [Fuite Java]).
  2. L'activité se ferme automatiquement une fois la page chargée (simulant une navigation répétée et une accumulation de fuites).
  3. Appuyez quatre fois sur le bouton.
  4. Vérifiez le nombre d'instances WebView dans votre application :

    adb shell dumpsys meminfo com.android.memorylab
    

    Recherchez la section Objects (Objets) en bas de la page. Vous verrez que le nombre de WebViews est passé à 4.

    Exemple de résultat (4 instances ayant fui) sur rango :

     Objects
               Views:       51         ViewRootImpl:        5
         AppContexts:       14           Activities:        5
              Assets:       38        AssetManagers:        0
       Local Binders:       55        Proxy Binders:       77
       Parcel memory:       41         Parcel count:       68
    Death Recipients:        3             WebViews:        4
    
  5. Capturez une empreinte de la mémoire et utilisez AHAT pour trouver la fuite. Si ahat ne figure pas dans votre chemin d'accès, vous pouvez le créer à partir de l'arborescence Android :

    # Dump heap from device
    adb shell am dumpheap com.android.memorylab /data/local/tmp/heap.hprof
    adb pull /data/local/tmp/heap.hprof
    # Run ahat using the built JAR (found in out/host/linux-x86/framework/)
    java -jar out/host/linux-x86/framework/ahat.jar -p 8888 heap.hprof
    
  6. Dans l'interface Web AHAT (localhost:8888), cliquez sur le lien allocations (allocations) ou sites dans le menu supérieur pour afficher l'utilisation globale de la mémoire.

    Attributions AHAT

  7. Recherchez la classe android.webkit.WebView. Cliquez sur son instance count (nombre d'instances) pour afficher toutes les instances actives. Plusieurs instances devraient s'afficher dans la liste.

    Instances WebView AHAT

  8. Cliquez sur l'une des instances WebView ayant fui. Faites défiler la page jusqu'à la section Sample Path from GC Root (Exemple de chemin d'accès à partir de la racine GC). Vous verrez qu'elle est conservée par la liste sLeakedWebViews dans com.android.memorylab.WebViewActivity.

    Chemin AHAT vers la racine GC


← Natif | ↑ Haut | Code de l'application →