WebView e memória

O WebView é um componente avançado que permite mostrar conteúdo da Web no seu app Android. No entanto, como ele é essencialmente um mecanismo de navegador completo (Chromium), tem um consumo de memória significativo e uma arquitetura multiprocesso complexa.

Informações técnicas: arquitetura multiprocesso

Em dispositivos modernos com tecnologia Android, o WebView usa um modelo multiprocesso para melhorar a segurança e a estabilidade. Quando o app usa um WebView, a memória é distribuída em diferentes processos:

  1. Processo do navegador (o processo do app): é o processo principal do aplicativo. Ele contém o objeto WebView Java e a parte do "navegador" do mecanismo do Chromium. Esse processo gerencia a interface, as solicitações de rede e a renderização da GPU (integrada diretamente ao pipeline de renderização HWUI do Android). Ao contrário do Chrome, o WebView não tem um processo de GPU separado.
  2. Processo do renderizador: esse processo é responsável por analisar o HTML, executar o JavaScript e o layout. Ele é isolado do restante do sistema por segurança. Atualmente, os apps recebem apenas um processo de renderizador para todos os WebViews (exceto em alguns casos especiais raros), ao contrário do Chrome, que geralmente usa processos de renderizador separados para diferentes sites.

Arquitetura do WebView

Por que isso é importante para a memória

Ao usar dumpsys meminfo <your_package>, você só vê a memória usada pelo processo do navegador (o processo do app). A memória usada pelo processo do renderizador é contabilizada separadamente.

Dentro do processo do navegador, a memória do WebView é distribuída como:

  • Heap Java: contém o wrapper Java WebView e objetos relacionados.
  • Heap nativo: contém as estruturas de dados internas , os caches e o estado do mecanismo do navegador Chromium. Devido ao uso do PartitionAlloc, algumas alocações nativas do WebView podem não ser contabilizadas em "Heap nativo" em dumpsys meminfo e podem aparecer em "Outros" ou "Desconhecido".
  • Memória compartilhada: usada para compartilhar buffers gráficos e outros dados. Isso pode não ser categorizado claramente por dumpsys meminfo.

Ferramentas de solução de problemas

Chrome DevTools

A ferramenta mais avançada para analisar a memória dentro do WebView (o processo do renderizador) é o Chrome DevTools.

  1. Ative a depuração do WebView no seu app:

    // 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. Conecte o dispositivo via USB.

  3. Abra o Chrome na máquina host e acesse chrome://inspect/#devices.

  4. Encontre o app e clique em inspect.

  5. Na janela do DevTools, acesse a guia Memory para fazer snapshots de heap ou gravar cronogramas de alocação para o heap JavaScript.

dumpsys meminfo

Use adb shell dumpsys meminfo --all <package> para conferir uma detalhamento da memória. Procure a categoria WebView na saída e as contagens de objetos.

Como criar um perfil do renderizador

Como o renderizador é executado em um processo separado, não é possível criar um perfil do heap nativo apenas criando um perfil do app. É necessário identificar o PID do processo do renderizador especificamente.

Para identificar o PID do renderizador correto quando vários WebViews estão ativos:

  1. Use dumpsys activity:

    adb shell dumpsys activity processes <your_package_name>
    

    Procure a seção mConnections. Você verá um ConnectionRecord vinculando seu app a um SandboxedProcessService. O PID desse processo é o renderizador. Exemplo:

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. Verifique os nomes dos processos: os processos do renderizador geralmente são nomeados como com.google.android.webview:sandboxed_processX ou semelhantes. Se apenas um app estiver usando um WebView, provavelmente haverá apenas um.

Depois de ter o PID, é possível criar um perfil dele usando heapprofd.

Práticas recomendadas para a memória do WebView

Destruição explícita

Espera-se que os apps chamem WebView.destroy() para indicar quando terminarem de usar uma instância.

Embora o WebView tente garantir que as instâncias possam ser coletadas como lixo e liberar todos os recursos automaticamente, isso é difícil de garantir em 100% dos casos. Mesmo quando a coleta automática de lixo funciona, ela pode ser significativamente atrasada, fazendo com que o app mantenha os recursos por muito mais tempo do que o esperado.

Se um app chamar WebView.destroy() no momento apropriado (por exemplo, em Activity.onDestroy()), manter uma referência ao próprio objeto WebView não vai vazar recursos nativos significativos. Não há necessidade estrita de referências nulas ao objeto WebView nos campos de atividade após a destruição, porque ele será limpo quando a própria atividade for coletada como lixo.

Exercícios: prática com a memória do WebView

Exercício 1: observação da pegada multiprocesso

  1. Inicie o MemoryLab e faça uma medição de linha de base da memória do app:

    adb shell dumpsys meminfo com.android.memorylab
    

    Linha de base de exemplo (rango): TOTAL PSS: 18915 KB

  2. Toque em Launch WebView (Normal).

  3. No WebView, toque em Allocate JS Memory (1000 DIVs) várias vezes.

  4. Verifique a memória do app novamente:

    adb shell dumpsys meminfo com.android.memorylab
    
  5. Observe que a memória no processo do app não aumenta significativamente em comparação com a linha de base. Isso ocorre porque os elementos DOM estão no processo do renderizador.

  6. Encontre o processo do renderizador:

    adb shell ps -A | grep webview | grep sandboxed
    

    Exemplo de saída:

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. Verifique a memória do processo do renderizador (usando o PID):

    adb shell dumpsys meminfo 14227
    
  8. Observe o PSS TOTAL alto do processo do renderizador. Na nossa execução de amostra, ele saltou para ~55 MB após algumas alocações. As alocações de JavaScript (processadas pelo mecanismo V8) normalmente contribuem para as seções Private Other ou Unknown (mmap) de dumpsys meminfo, em vez do heap Dalvik.

Exercício 2: vazamento do WebView do lado Java

Um erro comum é manter uma instância WebView em um campo estático ou em um objeto de longa duração que vaza. Como o objeto WebView é uma "âncora" pesada que mantém recursos nativos e potencialmente processos de renderizador inteiros, o vazamento é muito caro.

Impacto do vazamento do WebView

  1. No MemoryLab, toque em Launch WebView (Java Leak).
  2. A atividade será fechada automaticamente após o carregamento da página (simulando a navegação repetida e o acúmulo de vazamentos).
  3. Toque no botão 4 vezes.
  4. Verifique o número de instâncias WebView no seu app:

    adb shell dumpsys meminfo com.android.memorylab
    

    Procure a seção Objects na parte de baixo. Você verá que a contagem de WebViews aumentou para 4.

    Exemplo de saída (4 instâncias vazadas) no 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. Capture um heap dump e use o AHAT para encontrar o vazamento. Se você não tiver ahat no caminho, poderá criá-lo na árvore do 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. Na interface da Web do AHAT (localhost:8888), clique no link allocations (ou sites) no menu superior para conferir o uso geral da memória.

    Alocações do AHAT

  7. Pesquise a classe android.webkit.WebView. Clique na contagem de instâncias para conferir todas as instâncias ativas. Você verá várias instâncias na lista.

    Instâncias do WebView do AHAT

  8. Clique em uma das instâncias WebView vazadas. Role para baixo até a seção Sample Path from GC Root. Você verá que ela está sendo mantida pela lista sLeakedWebViews em com.android.memorylab.WebViewActivity.

    Caminho do AHAT para a raiz do GC


← Nativo | ↑ Acima | Código do app →