Gerenciar o estado do WebView com eficiência

Ao gerenciar o ciclo de vida de um app Android, preservar o estado do usuário durante a recuperação de recursos em segundo plano é um componente essencial de uma experiência do usuário perfeita. Para apps que incorporam fluxos de trabalho da Web, WebView.saveState(Bundle) permite serializar o histórico de navegação e o estado de um WebView em um Bundle. Esses dados podem ser restaurados posteriormente usando WebView.restoreState(Bundle).

No entanto, as implementações padrão podem ter limitações de tamanho de transação em sessões de navegação pesadas. Esta página descreve essas limitações arquitetônicas e oferece estratégias para evitar exceções relacionadas à memória, mantendo o histórico de navegação.

O limite de transação de 1 MB e a limpeza de estado

O Android impõe um limite estrito de 1 MB no volume total de dados que podem ser armazenados em savedInstanceState. Esse orçamento de 1 MB é compartilhado em todo o processo do app. Se um app incorporar várias instâncias de WebView, o estado e o histórico de navegação coletivos delas precisarão caber nessa única alocação compartilhada. Exceder esse limite aciona uma TransactionTooLargeException, resultando em uma falha do app.

Uma estratégia de mitigação comum, mas problemática, envolve monitorar o tamanho do pacote de estado do WebView e limpar completamente o histórico do WebView se ele ultrapassar um limite de segurança arbitrário (como 300 KB). Embora isso evite uma falha, ele introduz regressões graves na experiência do usuário:

  • Perda da navegação para trás: o Android geralmente encerra processos de apps em segundo plano para recuperar memória para outras tarefas. Você pode usar saveState(Bundle) no callback do ciclo de vida onSaveInstanceState() para preservar o histórico de navegação. Se você limpar esse histórico para evitar o limite de transação de 1 MB, toda a pilha de navegação será perdida. Quando o usuário retorna ao app, o botão "Voltar" do sistema sai imediatamente do componente ou app porque não há contexto histórico para oferecer suporte à navegação para trás, independentemente de uma reinicialização do processo ter ocorrido.

  • Invalidação do BFCache: a limpeza do histórico impede que o app use o cache de retorno e avanço (BFCache), removendo a capacidade de renderizar instantaneamente as páginas visitadas anteriormente.

  • Latência aumentada: os usuários perdem o estado atual no WebView, exigindo uma nova navegação e reinicialização completas. Esse processo aumenta significativamente a sobrecarga de rede e a latência da transação.

Estratégias de mitigação arquitetônica

Para evitar falhas de TransactionTooLargeException sem degradar a experiência do usuário com a exclusão completa do histórico, é necessário manter um equilíbrio rigoroso entre a retenção de estado e a eficiência da memória. Ao implementar as seguintes estratégias de otimização, você pode gerenciar com segurança o orçamento de transação de 1 MB, preservando o histórico de navegação essencial e a integridade da sessão.

Aplicar limites de tamanho na serialização de estado

Em vez de limpar completamente a pilha de navegação quando ela fica muito grande, um padrão mais eficaz é truncar os dados históricos:

  • Política de descarte direcionada: use WebViewCompat.saveState() para serializar o estado, aplicando um limite de bytes específico (por exemplo, WebViewCompat.saveState(webView, outState, maxSizeBytes)). Essa API descarta automaticamente as entradas de navegação mais antigas sequencialmente até que o payload total se ajuste à alocação definida. É importante ressaltar que isso só trunca o Bundle serializado sem modificar ou limpar o histórico ativo do WebView ativo, garantindo que a navegação para trás imediata permaneça completamente intacta.

  • Remoção de entrada de encaminhamento: se a interface do aplicativo fornecer um botão "Voltar" mas não tiver um botão de navegação "avançar" dedicado, você poderá descartar todas as entradas de navegação para frente definindo o parâmetro includeForwardStatesaveState da API como false. Isso reduz significativamente o tamanho do payload sem afetar os caminhos de navegação disponíveis do usuário.

Gerenciar a latência de recursos com a API HTTP Cache Quota

Embora saveState gerencie o limite de Bundle de 1 MB para o histórico de navegação efêmero, a API HTTP Cache Quota oferece controle manual sobre recursos da Web persistentes (cache em disco) por perfil. Isso cria uma distinção clara entre o contexto de navegação de curto prazo e os recursos armazenados em cache de longo prazo.

A escolha de uma cota adequada envolve uma compensação de desempenho:

  • Cotas mais altas melhoram a disponibilidade off-line e a latência de carregamento de recursos, mantendo mais recursos no disco.
  • Cotas mais baixas minimizam a ocupação do disco do app e impedem a remoção do cache de outros dados críticos do app pelo SO.

Essas configurações são mantidas nas reinicializações do app e precisam ser configuradas na linha de execução principal.

A implementação a seguir demonstra como configurar uma cota de cache em disco para o perfil padrão:

Kotlin

if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) &&
    WebViewFeature.isFeatureSupported(WebViewFeature.HTTP_CACHE)) {
    val defaultProfile = ProfileStore.getInstance()
        .getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME)
    val httpCache = defaultProfile.httpCache

    // Set explicit cache size to 50MB (50 * 1024 * 1024 bytes)
    httpCache.setQuotaBytes(50L * 1024 * 1024)
}

Java

if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) &&
    WebViewFeature.isFeatureSupported(WebViewFeature.HTTP_CACHE)) {
    Profile defaultProfile = ProfileStore.getInstance()
        .getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME);
    HttpCache httpCache = defaultProfile.getHttpCache();

    // Set explicit cache size to 50MB (50 * 1024 * 1024 bytes)
    httpCache.setQuotaBytes(50L * 1024 * 1024);
}

Para mais informações sobre estratégias de dimensionamento de cotas, gerenciamento de ciclo de vida e limites de perfil, consulte Gerenciar a cota de cache HTTP no WebView.

Considerações importantes sobre o desempenho

Os pontos a seguir destacam as limitações técnicas e os comportamentos de dados internos que regem o comportamento do estado do WebView:

  • Blobs PageState opacos: aproximadamente 70% dos dados armazenados por saveState consistem em blobs PageState internos do mecanismo de renderização. Esses dados capturam estados de sessão granulares, incluindo entradas de formulário e posições de rolagem de iframe. Evite tentar analisar ou remover manualmente segmentos individuais desses blobs, porque isso representa riscos de segurança graves e interrompe a integridade da restauração da sessão.

  • Gerenciamento granular do histórico: a API WebBackForwardList padrão não oferece suporte nativo à remoção arbitrária de elementos históricos individuais. Para um gerenciamento de estado rigoroso, é necessário implementar estratégias de truncamento usando os parâmetros maxSizeBytes e includeForwardState em WebViewCompat.saveState() para garantir a segurança arquitetônica.