Depois de entender o gerenciamento de memória do Android e configurar ferramentas para medir o uso de memória do jogo, a próxima etapa é reduzir e otimizar ativamente. Manter-se dentro dos limites estritos do Android ajuda a evitar que o sistema feche o jogo, impede longos tempos de inicialização e garante que o jogo funcione bem em todos os dispositivos.
Este guia oferece técnicas práticas para reduzir o consumo de memória do jogo, com foco específico na otimização de recursos no nível de ativos, configurações específicas do mecanismo e práticas recomendadas de gerenciamento de memória.
Reduzir a memória no Unity
Devido ao design arquitetônico do Unity, quando os alocadores de blocos nativos e o heap gerenciado são expandidos, o mecanismo tende a reter essas páginas de memória para reutilização em vez de retorná-las imediatamente ao sistema operacional (SO), mesmo depois que os recursos são liberados. Especificamente, o espaço de endereço virtual (memória reservada) permanece reservado durante todo o processo, e a memória física (RSS) não é recuperada imediatamente até que vários ciclos de coleta de lixo (GC) e corte ocorram. Consequentemente, um pico de memória temporário pode fazer com que a memória residente permaneça inflacionada por um longo período, mesmo depois que o uso real diminui. Esse comportamento aumenta o risco de falhas de falta de memória (OOM) em dispositivos de baixa qualidade e degrada a estabilidade geral do ambiente de execução.
Portanto, a otimização da memória do Unity precisa ser abordada por três pilares principais adaptados a esses comportamentos do mecanismo:
- Controlar o uso da memória ativa para evitar picos de memória.
- Gerenciar formatos de textura e variantes de sombreador para garantir que recursos desnecessários e recursos nativos não sejam instanciados.
- Refatorar estruturas de código de ambiente de execução para eliminar alocações desnecessárias no heap gerenciado para minimizar a frequência de GC e a expansão do heap.
Para mais informações, consulte Otimização de memória do Unity.
Reduzir a memória no Unreal Engine
No Unreal Engine, pipelines de renderização de alta fidelidade e gráficos de dependência de objetos complexos podem aumentar significativamente a pressão de memória RSS anônima e com suporte a arquivos. Em particular, confiar em referências fixas e hierarquias de herança de Blueprint profundas força o carregamento de recursos conectados não utilizados na memória. Além disso, permutações excessivas de sombreadores, pools de transmissão de textura não otimizados e tabelas de realocação ELF não compactadas contribuem para impressões de memória de linha de base altas, aumentando o risco de encerramentos de LMK (Low Memory Killer).
Portanto, a otimização da memória do Unreal Engine precisa ser abordada por três pilares principais adaptados a esses comportamentos do mecanismo:
- Desacoplar dados e lógica, substituindo referências fixas ou fortes por referências flexíveis ou fracas.
- Remover recursos de iluminação móvel não utilizados e opções de permutação para minimizar objetos de estado de pipeline (PSOs) e destinos de renderização redundantes, aplicando a compactação ASTC e personalizando pools de transmissão de textura com perfis de dispositivo.
- Ativar a compactação da tabela de realocação RELR e APS para reduzir o tamanho binário do ELF e reduzir o consumo de memória física do ambiente de execução.
Para mais informações, consulte Otimização de memória do Unreal.
Otimização de vários processos
O uso da memória de processos armazenados em cache é excluído da contabilização dos limites de memória porque não tem impacto nos apps ativos. Executar o serviço em um processo isolado separado ajuda o processo principal a fazer a transição para um estado armazenado em cache o mais rápido possível, melhorando a integridade do jogo.
Para mais informações, consulte Como rastrear o estado e a memória do processo, Como isolar um processo de serviço com o Unity e Como isolar um processo de serviço com o Unreal.
Reduzir o uso da memória em serviços percebidos pelo usuário
O jogo pode precisar executar a lógica em um serviço percebido pelo usuário para casos de uso como concluir um download grande ou para sistemas de chat de voz em segundo plano. Essas estratégias podem ajudar a gerenciar e reduzir o uso da memória durante esses cenários.
Estratégias para downloads grandes
Essas estratégias podem ser aplicadas a downloads grandes que você quer continuar mesmo depois que o usuário minimizar o jogo.
1. Isolar o processo de download
O quê:garanta que o SO possa recuperar imediatamente a memória que o app não está usando, realizando o download em um processo separado, já que é possível que o alocador de memória retenha páginas de pool de memória e mantenha a memória RSS anônima artificialmente alta, mesmo depois de liberar matrizes. Quando você encerra ou sai do serviço e processa explicitamente, a memória é retornada ao pool do SO e o processo principal permanece inalterado.
No Unity:descarregue o download para um
Servicenativo do Android declarado com um processo comoandroid:process=":downloader"em um manifesto personalizado e invoque-o com o JNIAndroidJavaClassdo Unity. Encerre o processo quando o download for concluído. Consulte Executar um serviço perceptível em um processo separado com o Unity para orientações mais detalhadas.No Unreal:declare um
Servicepersonalizado do Android com um processo comoandroid:process=":downloader"usando a linguagem de plug-in do Unreal e acione-o com o JNI C++. Encerre o processo quando o download for concluído. Consulte Executar um serviço perceptível em um processo separado com o Unreal para orientações mais detalhadas.Para o Android nativo: declare um
ServicenoAndroidManifestcom um processo comoandroid:process=":downloader". Execute o download nesse processo isolado e chameProcess.killProcess(Process.myPid())quando o download for concluído.
Como isso ajuda:reduz a duração da memória e permite que o download continue liberando a memória usada pelo processo principal maior.
2. Fazer o streaming de downloads diretamente para o disco
O quê:faça o streaming de dados diretamente do soquete de rede para o disco usando um buffer reutilizável pequeno e de tamanho fixo, em vez de acumular respostas de rede em uma matriz grande antes de gravar.
No Unity:evite usar
DownloadHandlerBufferpara pacotes de recursos ou arquivos grandes, já que ele aloca um buffer de memória nativo equivalente ao tamanho do arquivo (memória RSS anônima). Em vez disso, useDownloadHandlerFilepara transmitir bytes nativamente para o disco em uma linha de execução em segundo plano.No Unreal:canalize blocos de dados recebidos diretamente para um
FArchive(arquivo com suporte a arquivos usando o gerenciador de arquivos do Unreal) comSetResponseBodyReceiveStream()em vez de anexar payloads de umIHttpRequesta umTArray<uint8>.Para o Android nativo: canalize o
InputStreampara umFileOutputStreamusando um buffer agrupado em vez de chamar.readBytes()ou.string()em uma resposta HTTP.
Como isso ajuda:reduz o uso máximo de memória
3. Fazer o streaming da descompactação de arquivos baixados
O quê:se o download estiver compactado, envolva o fluxo de entrada de rede em um
descompactador de streaming, como ZipInputStream, em vez de baixar o
arquivo, carregá-lo na RAM e extraí-lo.
Como isso ajuda:reduz o uso máximo de memória
4. Delegar ao SO
O quê:para evitar o gerenciamento de memória em segundo plano, delegue o trabalho às APIs nativas do Android.
WorkManageré o wrapper moderno e recomendado para oJobSchedulerno nível do SO. No Android 14 e versões mais recentes, oWorkManagerprocessa automaticamente os downloads acionados pelo usuário como um job de transferência de dados iniciada pelo usuário (UIDT). Isso é executado no processo do app. Portanto, ainda é necessário fazer o streaming diretamente para o disco para minimizar o uso da memória. A UIDT protege o app contra falhas de memória insuficiente, permitindo que o SO pause e retome o download normalmente se os recursos do sistema ficarem limitados.DownloadManageré executado em um processo de sistema separado e não atribui o uso da memória para a transferência ao app. O app vai receber uma notificação de transmissão quando o arquivo for baixado e estiver pronto.
Como isso ajuda:o WorkManager ajuda a lidar com cenários de memória insuficiente, e o DownloadManager reduz o uso da memória do app.
Estratégias para serviços auxiliares
Essas estratégias podem ser aplicadas a serviços auxiliares que um jogo executa em paralelo com o processo principal, como chat de voz em segundo plano.
1. Isolar o processo
O quê:desacople o recurso, por exemplo, a solução de chat de voz, do mecanismo principal do jogo. Por exemplo, você pode executar a captura de microfone e
o streaming de rede em um serviço em primeiro plano do Android atribuído a um processo separado (declarado no manifesto, como com android:process=":voice").
Como isso ajuda:quando o app é minimizado, o processo principal do jogo pode cair em um estado armazenado em cache de prioridade mais baixa, enquanto o serviço auxiliar mais leve continua no estado de serviço percebido pelo usuário.
2. Cortar a memória não utilizada no processo
O quê:se o serviço auxiliar estiver muito integrado ao mecanismo do jogo para ser separado, tente reduzir o máximo possível o peso do processo assim que o jogo for colocado em segundo plano/minimizado. Considere limpar caches de textura, descarregar cenas não essenciais, reduzir o tick do mecanismo e renderizar taxas para 0 e chamar explicitamente a coleta de lixo.
No Unity:execute esse corte quando
OnApplicationPause()for acionado.Resources.UnloadUnusedAssets()pode ser útil.No Unreal: vincule a lógica de corte ao delegado
ApplicationWillEnterBackgroundDelegate.Para o Android nativo: execute o corte em
onPause()ouonStop(), conforme apropriado. O SO pode tentar chamar sua implementação deonTrimMemory()antes de recorrer ao encerramento de processos usando memória alta.
Como isso ajuda:reduz o uso da memória que não é necessário quando o jogo não está em primeiro plano.