Resolver LMKs no seu jogo do Unity é um processo sistemático:

Receber um instantâneo da memória
Use o Unity Profiler para receber um instantâneo da memória gerenciada pelo Unity. A Figura 2 mostra as camadas de gerenciamento de memória que o Unity usa para processar a memória no jogo.

Memória gerenciada
O gerenciamento de memória do Unity implementa uma camada de memória controlada que usa um heap gerenciado e um coletor de lixo para alocar e atribuir memória automaticamente. O sistema de memória gerenciada é um ambiente de scripting em C# baseado em Mono ou IL2CPP. O benefício do sistema de memória gerenciada é que ele usa um coletor de lixo para liberar automaticamente as alocações de memória.
Memória não gerenciada do C#
A camada de memória C# não gerenciada oferece acesso à camada de memória nativa, permitindo controle preciso sobre as alocações de memória ao usar código C#. Essa camada de gerenciamento de memória pode ser acessada pelo namespace Unity.Collections e por funções como UnsafeUtility.Malloc e UnsafeUtility.Free.
Memória nativa
O núcleo interno C/C++ do Unity usa um sistema de memória nativo para gerenciar cenas, recursos, APIs de gráficos, drivers, subsistemas e buffers de plug-in. Embora o acesso direto seja restrito, você pode manipular dados com segurança usando a API C# do Unity e aproveitar o código nativo eficiente. A memória nativa raramente exige interação direta, mas é possível monitorar o impacto dela no desempenho usando o Profiler e ajustar as configurações para otimizar a performance.
A memória não é compartilhada entre C# e código nativo, conforme mostrado na Figura 3. Os dados exigidos pelo C# são alocados no espaço de memória gerenciado sempre que necessário.
Para que o código do jogo gerenciado (C#) acesse os dados de memória nativa do mecanismo, por exemplo, uma chamada para GameObject.transform faz uma chamada nativa para acessar dados de memória na área nativa e retorna valores para C# usando Bindings. As vinculações garantem convenções de chamada adequadas para cada plataforma e processam a transmissão automática de tipos gerenciados para seus equivalentes nativos.
Isso acontece apenas na primeira vez, já que o shell gerenciado para acessar a propriedade transform é preservado no código nativo. O armazenamento em cache da propriedade "transform" pode reduzir o número de chamadas de ida e volta entre código gerenciado e nativo, mas a utilidade do armazenamento em cache depende da frequência com que a propriedade é usada. Além disso, o Unity não copia partes da memória nativa para a memória gerenciada quando você acessa essas APIs.

Para saber mais, consulte Introdução à memória no Unity.
Além disso, estabelecer um orçamento de memória é crucial para manter o jogo funcionando sem problemas, e implementar um sistema de análise ou relatórios de consumo de memória garante que cada nova versão não exceda o orçamento de memória. Integrar testes do Play Mode à sua integração contínua (CI) para verificar o consumo de memória em áreas específicas do jogo é outra estratégia para ter melhores insights.
Gerenciar recursos
Essa é a parte mais impactante e prática do consumo de memória. Crie um perfil o mais cedo possível.
O uso de memória em jogos Android pode variar muito dependendo do tipo de jogo, do número e dos tipos de recursos e das estratégias de otimização de memória. No entanto, os colaboradores comuns para o uso da memória geralmente incluem texturas, malhas, arquivos de áudio, shaders, animações e scripts.
Detectar recursos duplicados
A primeira etapa é detectar ativos mal configurados e duplicados usando o Memory Profiler, uma ferramenta de relatório de build ou o Project Auditor.
Texturas
Analise a compatibilidade do jogo com dispositivos e decida o formato de textura correto. É possível dividir os pacotes de textura para dispositivos de ponta e de baixo custo usando o Play Asset Delivery, o Addressable ou um processo mais manual com um AssetBundle.
Siga as recomendações mais conhecidas disponíveis em Otimizar a performance do seu jogo para dispositivos móveis e na postagem de discussão Otimizar as configurações de importação de textura do Unity. Em seguida, tente estas soluções:
Compacte texturas com formatos ASTC para reduzir o uso de memória e teste uma taxa de blocos mais alta, como 8x8.
Se for necessário usar o ETC2, compacte as texturas no Atlas. Colocar várias texturas em uma só garante a potência de dois (POT), reduz as chamadas de extração e acelera a renderização.
Otimize o formato e o tamanho da textura RenderTarget. Evite texturas de alta resolução desnecessárias. Usar texturas menores em dispositivos móveis economiza memória.
Use o agrupamento de canais de textura para economizar memória de textura.
Malhas e modelos
Comece verificando as configurações fundamentais (página 27) e as configurações de importação de malha:
- Mesclar malhas redundantes e menores.
- Reduza a contagem de vértices para objetos em cenas (por exemplo, objetos estáticos ou distantes).
- Gere grupos de nível de detalhe (LOD) para recursos de alta geometria.
Materiais e sombreadores
- Remova as variantes de shader não utilizadas de maneira programática durante o processo de build.
- Consolide variantes de sombreador usadas com frequência em uber shaders para evitar a duplicação.
- Ative o carregamento dinâmico de shader para resolver o grande uso de memória dos shaders pré-carregados na VRAM/RAM. No entanto, preste atenção se a compilação de shader estiver causando falhas nos frames.
- Use o carregamento dinâmico de shader para evitar que todas as variantes sejam carregadas. Para mais informações, consulte a postagem do blog Melhorias nos tempos de build de shader e no uso de memória.
- Use a instanciação de materiais corretamente aproveitando
MaterialPropertyBlocks
.
Áudio
Comece verificando as configurações fundamentais (página 41) e confira estas configurações de importação de malha:
- Remova referências
AudioClip
não usadas ou redundantes ao empregar mecanismos de áudio de terceiros, como FMOD ou Wwise. - Pré-carregar dados de áudio. Desative o pré-carregamento de clipes que não são necessários imediatamente durante a execução ou a inicialização da cena. Isso ajuda a reduzir o overhead de memória durante a inicialização da cena.
Animações
- Ajuste as configurações de compressão de animação do Unity para minimizar o número de keyframes e eliminar dados redundantes.
- Redução de frames-chave: remove automaticamente os frames-chave desnecessários
- Compactação de quaternions: compacta dados de rotação para reduzir o uso de memória.
É possível ajustar as configurações de compactação em Configurações de importação de animação nas guias Rig ou Animação.
Reutilize clipes de animação em vez de duplicá-los para diferentes objetos.
Use Controladores de substituição de animador para reutilizar um controlador de animador e substituir clipes específicos por personagens diferentes.
Renderize animações baseadas em física: se as animações forem baseadas em física ou procedurais, renderize-as em clipes de animação para evitar cálculos de tempo de execução.
Otimize o rig de esqueleto: use menos ossos no rig para reduzir a complexidade e o consumo de memória.
- Evite usar muitos ossos em objetos pequenos ou estáticos.
- Se alguns ossos não forem animados ou necessários, remova-os do rig.
Reduza o comprimento do clipe de animação.
- Corte os clipes de animação para incluir apenas os frames necessários. Evite armazenar animações não usadas ou muito longas.
- Use animações em loop em vez de criar clipes longos para movimentos repetidos.
Verifique se apenas um componente de animação está anexado ou ativado. Por exemplo, desative ou remova os componentes de Animação legada se você estiver usando o Animator.
Evite usar o Animator se ele não for necessário. Para efeitos visuais simples, use bibliotecas de interpolação ou implemente o efeito visual em um script. O sistema de animação pode consumir muitos recursos, principalmente em dispositivos móveis de baixo custo.
Use o Job System para animações ao lidar com um grande número delas, já que esse sistema foi totalmente reformulado para ser mais eficiente em termos de memória.
Cenas
Quando novas cenas são carregadas, elas trazem recursos como dependências. No entanto, sem o gerenciamento adequado do ciclo de vida dos recursos, essas dependências não são monitoradas por contadores de referência. Como resultado, os recursos podem permanecer na memória mesmo depois que as cenas não utilizadas são descarregadas, causando fragmentação da memória.
- Use o pool de objetos do Unity para reutilizar instâncias de GameObject em
elementos recorrentes de jogabilidade, porque o pool de objetos usa uma pilha para manter uma
coleção de instâncias de objetos para reutilização e não é seguro para linhas de execução. Minimizar
Instantiate
eDestroy
melhora o desempenho da CPU e a estabilidade da memória. - Descarregando recursos:
- Faça o descarregamento estratégico de recursos durante momentos menos críticos, como telas de apresentação ou de carregamento.
- O uso frequente de
Resources.UnloadUnusedAssets
causa picos no processamento da CPU devido a grandes operações internas de monitoramento de dependências. - Verifique se há picos grandes de CPU no marcador de perfil GC.MarkDependencies.
Remova ou reduza a frequência de execução e descarregue manualmente recursos específicos usando Resources.UnloadAsset em vez de depender do
Resources.UnloadUnusedAssets()
abrangente.
- Reestruture cenas em vez de usar constantemente Resources.UnloadUnusedAssets.
- Chamar
Resources.UnloadUnusedAssets()
paraAddressables
pode descarregar sem querer pacotes carregados dinamicamente. Gerencie com cuidado o ciclo de vida dos recursos carregados dinamicamente.
Diversos
Fragmentação causada por transições de cena: quando o método
Resources.UnloadUnusedAssets()
é chamado, o Unity faz o seguinte:- Libera memória para recursos que não estão mais em uso
- Executa uma operação semelhante a um coletor de lixo para verificar o heap de objetos gerenciados e nativos em busca de recursos não utilizados e os descarrega.
- Limpa a memória de textura, malha e recursos, desde que não haja uma referência ativa
AssetBundle
ouAddressable
: fazer mudanças nessa área é complexo e exige um esforço coletivo da equipe para implementar as estratégias. No entanto, depois que essas estratégias são dominadas, elas melhoram significativamente o uso da memória, reduzem o tamanho do download e diminuem os custos da nuvem. Para mais informações sobre o gerenciamento de recursos no Unity, consulteAddressables
.Dependências compartilhadas centralizadas: agrupe dependências compartilhadas, como shaders, texturas e fontes, de forma sistemática em pacotes ou grupos
Addressable
dedicados. Isso reduz a duplicação e garante que os recursos desnecessários sejam descarregados de maneira eficiente.Use
Addressables
para rastreamento de dependências. Os Addressables simplificam o carregamento e o descarregamento, podendo descarregar automaticamente dependências que não são mais referenciadas. A transição paraAddressables
no gerenciamento de conteúdo e na resolução de dependências pode ser uma solução viável, dependendo do caso específico do jogo. Analise as cadeias de dependência com a ferramenta "Analisar" para identificar duplicatas ou dependências desnecessárias. Como alternativa, consulte as ferramentas de dados do Unity se estiver usando AssetBundles.TypeTrees
: se oAddressables
e oAssetBundles
do jogo forem criados e implantados usando a mesma versão do Unity que o jogador e não exigirem compatibilidade com versões anteriores de outros builds do jogador, considere desativar a gravação deTypeTree
, o que deve reduzir o tamanho do pacote e a presença na memória do objeto de arquivo serializado. Modifique o processo de build na configuração do pacote Addressables local definindo ContentBuildFlags como DisableWriteTypeTree.
Escrever código compatível com o coletor de lixo
O Unity usa a coleta de lixo (GC, na sigla em inglês) para gerenciar a memória, identificando e liberando automaticamente a memória não utilizada. Embora a coleta de lixo seja essencial, ela pode causar problemas de desempenho (por exemplo, picos de frame rate) se não for processada corretamente, já que esse processo pode pausar momentaneamente o jogo, levando a problemas de desempenho e uma experiência do usuário abaixo do ideal.
Consulte o manual do Unity (em inglês) para conhecer técnicas úteis de redução da frequência de alocações de heap gerenciadas e a UnityPerformanceTuningBible (em inglês), página 271, para ver exemplos.
Reduza as alocações do coletor de lixo:
- Evite LINQ, lambdas e closures, que alocam memória de heap.
- Use
StringBuilder
para strings mutáveis em vez de concatenação de strings. - Reutilize coleções chamando
COLLECTIONS.Clear()
em vez de instanciá-las novamente.
Para mais informações, consulte o e-book Guia definitivo para criação de perfil de jogos do Unity (link em inglês).
Gerenciar atualizações da tela da interface:
- Mudanças dinâmicas nos elementos da interface — quando elementos da interface, como propriedades de texto, imagem ou
RectTransform
, são atualizados (por exemplo, mudando o conteúdo do texto, redimensionando elementos ou animando posições), o mecanismo pode alocar memória para objetos temporários. - Alocações de strings: elementos da interface, como texto, geralmente exigem atualizações de strings, já que elas são imutáveis na maioria das linguagens de programação.
- Tela suja: quando algo em uma tela muda (por exemplo, redimensionamento, ativação e desativação de elementos ou modificação de propriedades de layout), toda a tela ou uma parte dela pode ser marcada como suja e ser reconstruída. Isso pode acionar a criação de estruturas de dados temporárias (por exemplo, dados de malha, buffers de vértices ou cálculos de layout), o que aumenta a geração de lixo.
- Atualizações complexas ou frequentes: se a tela tiver um grande número de elementos ou for atualizada com frequência (por exemplo, a cada frame), essas recriações podem levar a uma rotatividade significativa da memória.
- Mudanças dinâmicas nos elementos da interface — quando elementos da interface, como propriedades de texto, imagem ou
Ative a coleta incremental de lixo para reduzir picos de coleta grandes espalhando limpezas de alocação em vários frames. Crie um perfil para verificar se essa opção melhora o desempenho e o uso de memória do jogo.
Se o jogo exigir uma abordagem controlada, defina o modo de coleta de lixo como manual. Em seguida, em uma mudança de nível ou em outro momento sem jogabilidade ativa, chame a coleta de lixo.
Invoque chamadas manuais de coleta de lixo GC.Collect() para transições de estado do jogo (por exemplo, troca de nível).
Otimize matrizes começando com práticas de programação simples e, se necessário, usando matrizes nativas ou outros contêineres nativos para matrizes grandes.
Monitore objetos gerenciados usando ferramentas como o Unity Memory Profiler para rastrear referências de objetos não gerenciados que persistem após a destruição.
Use um marcador do Profiler para enviar à ferramenta de relatórios de performance e ter uma abordagem automatizada.
Evitar vazamentos e fragmentação de memória
Vazamentos de memória
No código C#, quando uma referência a um objeto do Unity existe depois que
o objeto é destruído, o objeto wrapper gerenciado, conhecido como Managed
Shell, permanece na memória. A memória nativa associada à
referência é liberada quando a cena é descarregada ou quando o GameObject a que a
memória está anexada, ou qualquer um dos objetos pai dela, é destruído pelo método
Destroy()
. No entanto, se outras referências à cena ou ao GameObject não forem
limpas, a memória gerenciada poderá persistir como um objeto
de shell vazado. Para mais detalhes sobre objetos de shell gerenciados, consulte
o manual Objetos de shell gerenciados.
Além disso, vazamentos de memória podem ser causados por assinaturas de eventos, lambdas e closures, concatenações de strings e gerenciamento inadequado de objetos em pool:
- Para começar, consulte Encontrar vazamentos de memória para comparar corretamente os snapshots de memória do Unity.
- Verifique se há assinaturas de eventos e vazamentos de memória. Se os objetos se inscreverem em eventos (por exemplo, por delegados ou UnityEvents), mas não cancelarem a inscrição corretamente antes de serem destruídos, o gerenciador ou editor de eventos poderá reter referências a esses objetos. Isso impede que esses objetos sejam coletados como lixo, causando vazamentos de memória.
- Monitore eventos de classe global ou singleton que não são descadastrados na destruição de objetos. Por exemplo, cancele a inscrição ou desvincule delegados em destruidores de objetos.
- Verifique se a destruição de objetos agrupados anula totalmente as referências a componentes de malha de texto, texturas e GameObjects principais.
- Ao comparar snapshots do Unity Memory Profiler e observar uma diferença no consumo de memória sem um motivo claro, a diferença pode ser causada pelo driver de gráficos ou pelo próprio sistema operacional.
Fragmentação de memória
A fragmentação de memória ocorre quando muitas alocações pequenas são liberadas em uma ordem aleatória. As alocações de heap são feitas sequencialmente, o que significa que novos blocos de memória são criados quando o bloco anterior fica sem espaço. Consequentemente, novos objetos não preenchem as áreas vazias de blocos antigos, o que leva à fragmentação. Além disso, alocações temporárias grandes podem causar fragmentação permanente durante a sessão de um jogo.
Esse problema é particularmente problemático quando alocações grandes de curta duração são feitas perto de alocações de longa duração.
Agrupe as alocações com base na vida útil. O ideal é que as alocações de longa duração sejam feitas juntas, no início do ciclo de vida do aplicativo.
Observadores e organizadores de eventos
- Além do problema mencionado na seção (Vazamentos de memória)77, com o tempo, os vazamentos de memória podem contribuir para a fragmentação, deixando memória não utilizada alocada para objetos que não estão mais em uso.
- Verifique se a destruição de objetos agrupados anula totalmente as referências a
componentes de malha de texto, texturas e
GameObjects
pai. - Os organizadores de eventos costumam criar e armazenar listas ou dicionários para gerenciar as inscrições. Se eles aumentarem e diminuírem dinamicamente durante a execução, poderão contribuir para a fragmentação da memória devido a alocações e desalocações frequentes.
Código
- As corrotinas às vezes alocam memória, o que pode ser facilmente evitado armazenando em cache a instrução de retorno do IEnumerator em vez de declarar um novo a cada vez.
- Monitore continuamente os estados do ciclo de vida dos objetos agrupados para evitar manter
referências fantasmas
UnityEngine.Object
.
Recursos
- Use sistemas de substituição dinâmica para experiências de jogos baseadas em texto e evite pré-carregar todas as fontes para casos multilíngues.
- Organize os recursos (por exemplo, texturas e partículas) por tipo e ciclo de vida esperado.
- Condense recursos com atributos de ciclo de vida inativos, como imagens redundantes da interface e malhas estáticas.
Alocações com base no período
- Alocar recursos de longa duração no início do ciclo de vida do aplicativo para garantir alocações compactas.
- Use NativeCollections ou alocadores personalizados para estruturas de dados transientes ou que exigem muita memória (por exemplo, clusters de física).
Ação de memória relacionada a código e executáveis
O executável e os plug-ins do jogo também afetam o uso da memória.
Metadados IL2CPP
O IL2CPP gera metadados para todos os tipos (por exemplo, classes, genéricos e delegados) no momento da build, que são usados no tempo de execução para reflexão, verificação de tipos e outras operações específicas do tempo de execução. Esses metadados são armazenados na memória e podem contribuir significativamente para o total de memória do aplicativo. O cache de metadados do IL2CPP contribui muito para os tempos de inicialização e carregamento. Além disso, o IL2CPP não remove duplicidades de determinados elementos de metadados (por exemplo, tipos genéricos ou informações serializadas), o que pode resultar em um uso de memória inflacionado. Isso é agravado pelo uso repetitivo ou redundante de tipos no projeto.
Os metadados do IL2CPP podem ser reduzidos das seguintes maneiras:
- Evite o uso de APIs de reflexão, já que elas podem contribuir significativamente para alocações de metadados do IL2CPP.
- Como desativar pacotes integrados
- Implementação do compartilhamento genérico completo do Unity 2022, que deve ajudar a reduzir a sobrecarga causada por genéricos. No entanto, para reduzir ainda mais as alocações, diminua o uso de tipos genéricos.
Remoção de código
Além de reduzir o tamanho do build, a remoção de código também diminui o uso de memória. Ao criar com o back-end de script IL2CPP, a remoção de bytecode gerenciado (ativada por padrão) remove o código não usado de assemblies gerenciados. O processo funciona definindo assemblies raiz e usando análise de código estático para determinar qual outro código gerenciado esses assemblies raiz usam. Todo código inacessível é removido. Para mais informações sobre a remoção de código gerenciado, consulte a postagem do blog TTales from the optimization trenches: Better managed code stripping with Unity 2020 LTS (em inglês) e a documentação Managed code stripping (em inglês).
Alocadores nativos
Teste alocadores de memória nativos para ajustar os alocadores de memória. Se o jogo estiver com pouca memória, use blocos menores, mesmo que isso envolva alocadores mais lentos. Consulte o exemplo de alocador de heap dinâmico para saber mais.
Gerenciar plug-ins e SDKs nativos
Encontre o plug-in problemático: remova cada plug-in e compare os snapshots de memória do jogo. Isso envolve desativar muitas funcionalidades de código com Scripting Define Symbols e refatorar classes altamente acopladas com interfaces. Confira Melhore seu código com padrões de programação de jogos para facilitar o processo de desativação de dependências externas sem tornar o jogo injogável.
Entre em contato com o autor do plug-in ou SDK. A maioria dos plug-ins não é de código aberto.
Reproduza o uso de memória do plug-in: você pode escrever um plug-in simples (use este plug-in do Unity como referência) que faça alocações de memória. Inspecione os snapshots de memória usando o Android Studio (já que o Unity não rastreia essas alocações) ou chame a classe
MemoryInfo
e o métodoRuntime.totalMemory()
no mesmo projeto.
Um plug-in do Unity aloca memória Java e nativa. Veja como fazer isso:
Java
byte[] largeObject = new byte[1024 * 1024 * megaBytes];
list.add(largeObject);
Nativo
char* buffer = new char[megabytes * 1024 * 1024];
// Random data to fill the buffer
for (int i = 1; i < megabytes * 1024 * 1024; ++i) {
buffer[i] = 'A' + (i % 26); // Fill with letters A-Z
}