Detecção automática de vazamento de memória no iOS

julho 20, 2016 6:00 pm Publicado por Deixe um comentário

Você sabe trabalhar corretamente com o gerenciamento de memória? Essa é uma dificuldade recorrente em nossas aplicações. Neste artigo, eu trago para vocês como o Facebook trabalha com o gerenciamento de memória em aplicativos iOS. Além de um pequeno tutorial do algoritmo, este artigo também traz ferramentas abertas no GitHub por parte deles! Confira na sequência!

A memória em dispositivos móveis é um recurso compartilhado. Aplicativos que gerenciam de forma inadequada esse recurso acabam ficando sem memória, travam e sofrem drasticamente com queda no desempenho.

O Facebook para iOS tem muitas características em que todos compartilham o mesmo espaço de memória. Se alguma característica específica começa a consumir muita memória, ela pode afetar o aplicativo todo. Isso acontece, por exemplo, se uma característica acidentalmente introduz um vazamento de memória.

Vazamentos de memória geralmente acontecem quando alocamos uma determinada parcela da nossa memória para um conjunto de objetos e nos esquecemos de liberar depois que terminarmos de usar. Isso significa que o sistema nunca recupera a memória a fim de utilizá-la para outra coisa, o que eventualmente significa que sua aplicação ficará sem memória.

No Facebook, assim como provavelmente na sua empresa, existem muitos engenheiros que trabalham em diferentes partes da base de código. Sendo assim, é inevitável que vazamentos de memória aconteçam e, quando acontecem, é necessário encontrar o vazamento rapidamente e corrigi-lo.

Algumas ferramentas para localização de vazamentos já existem, mas elas exigem uma grande quantidade de trabalho manual, como no exemplo abaixo:

  1. Abra o Xcode e fazer um build para perfil.
  2. Abra o Instruments.
  3. Use o aplicativo, tentando reproduzir o maior número possível de cenários e comportamentos.
  4. Preste atenção em picos/vazamentos de memória.
  5. Descubra onde estão as fontes dos vazamentos de memória.
  6. Corrija o problema.

Isso significa muito trabalho e retrabalho o tempo todo. Por causa disso, algumas vezes nós não conseguimos localizar e corrigir vazamentos de memória no início do ciclo de desenvolvimento.

Automatizar esse processo nos permite encontrar vazamentos de memória mais rápido, sem muito envolvimento do desenvolvedor. Para resolver esse problema, o Facebook construiu um conjunto de ferramentas que lhes permitiu a automatização do processo e corrigiu uma série de problemas em sua própria base de código. Atualmente, eles se dizem animados em anunciar que estão liberando essas ferramentas: FBRetainCycleDetector, FBAllocationTracker e FBMemoryProfiler.

Retendo ciclos

Objective-C usa a contagem de referência para gerenciar a memória e liberar objetos não utilizados. Qualquer objeto na memória pode “reter” um outro objeto, que mantém o outro objeto na memória, desde que o primeiro objeto precise. Uma maneira de olhar para isso é que os objetos “possuem” outros objetos.

Isso funciona bem na maioria das vezes, mas chega-se a um impasse quando dois objetos acabam por “possuir” um ao outro, diretamente ou, mais comumente, indiretamente, através de objetos que os conectam. Esse ciclo de referências de propriedades é chamado de ciclo de retenção.

ciclo

Reter ciclos pode causar uma variedade de problemas. Na melhor das hipóteses, desperdiça apenas um pouco mais de memória se os objetos estão ocupando espaço na memória RAM indefinidamente. Se os objetos que vazaram estão ativamente fazendo coisas triviais, menos memória está disponível para outras partes do aplicativo. Na pior das hipóteses, o aplicativo pode falhar se os vazamentos queriam usar mais memória do que a disponível.

Durante o manual profiling, o Facebook descobriu que tem a tendência a ter uma abundância de retenção de ciclos. É fácil introduzi-los, e pode ser difícil encontrá-los mais tarde. O detector de retenção de ciclo torna mais fácil a tarefa de encontrá-los.

Detecção de ciclo de retenção em tempo de execução

Encontrar ciclos de retenção no Objective-C é análogo a encontrar ciclos em um gráfico direcionado acíclico em que os nodes são objetos e as bordas são referências entre objetos (então, se o objeto A retém o objeto B, existe a referência de A para B). Nossos objetos do Objective-C já estão em nosso gráfico; tudo o que temos a fazer é atravessar com uma busca em profundidade.

Confira neste vídeo.

É uma abstração muito simples que funciona muito bem. Assim, o Facebook tem certeza de que pode usar objetos como nodes e que, para cada objeto, é possível obter todos os objetos que são referenciados. Essas referências podem ser fracas ou fortes. Ciclos de referência são causados apenas por referências fortes. Para cada objeto, é necessário descobrir como encontrar apenas essas referências.

Felizmente, o Objective-C oferece uma poderosa e introspectiva biblioteca de tempo de execução, que pode fornecer dados suficientes para ir a fundo no gráfico.

Um node no gráfico pode ser um objeto ou um bloco. Vamos discutir atravessando-os separadamente.

Objetos

O tempo de execução tem uma série de ferramentas que nos permitem introspecção dos objetos e aprender muito sobre eles.

A primeira coisa que podemos fazer é pegar o layout de todas as variáveis de instância de um objeto (o “layout de ivar”).

    const char *class_getIvarLayout(Class cls);
    const char *class_getWeakIvarLayout(Class cls);

Para um determinado objeto, um layout Ivar descreve onde devemos olhar para outros objetos que ele referencia. Ele irá fornecer com um “index,” que representa um deslocamento que precisa ser acrescentado ao endereço do objeto, a fim de obter o endereço de um objeto que faz referência. O que o tempo de execução também permite fazer é pegar um “layout de ivar fraco”, que é um layout de todas as variáveis de instância fracas desse objeto. Assim, é possível supor que a diferença entre estes dois layouts será um layout forte.

Há também suporte parcial para o Objective-C++. Em Objective-C++, podemos definir objetos em structs, e aqueles que não podem ser pegos em um layout ivar. Runtime oferece “type encoding” para lidar com isso. Para cada variável de instância, o type encoding descreve como a variável é estruturada. Se é um struct, descreve quais campos e types ele consiste. O Facebook analisou o type encoding para descobrir quais as variáveis de instância são objetos do Objective-C. Eles calcularam seus deslocamentos e, como em layouts, pegaram os endereços dos objetos para os quais eles apontam.

Existem também alguns casos extremos nos quais eles não entram profundamente. Estes são principalmente coleções que agem de forma diferente, e eles realmente precisam enumerar através deles mesmos para obter os seus objetos acumulados, o que potencialmente poderia gerar alguns efeitos colaterais.

Blocos

Os blocos são um pouco diferentes de objetos. O tempo de execução não deixa olhar facilmente para o seu layout, mas ainda se pode tentar adivinhar.

Ao lidar com blocos, o Facebook tem utilizado a ideia apresentada por Mike Ash no seu projeto Circle: o projeto que inspirou FBRetainCycleDetector em primeiro lugar.

O que é possível utilizar é a aplicação de interface binária para blocos (ABI). Ela descreve como o bloco vai ficar na memória. Se sabemos que a referência com a qual estamos lidando é um bloco, podemos lançá-lo em uma estrutura falsa que imita um bloco. Depois de lançar o bloco para uma C-struct, sabemos onde os objetos retidos pelo bloco são mantidos. Não sabemos, infelizmente, se essas referências são fortes ou fracas.

Para resolver esse problema, o Facebook está utilizando uma técnica blackbox. Eles criaram um objeto que finge ser um bloco no qual eles têm interesse de investigar. Porque eles conhecem a interface do bloco, sabem onde procurar referências que esse bloco detém. No lugar dessas referências, o falso objeto terá “release detectors”. Release detectors são pequenos objetos que estão observando as mensagens de release enviadas para eles. Essas mensagens são enviadas para referências fortes quando um proprietário quer abdicar da propriedade. Assim, é possível verificar que os detectores receberam essa mensagem quando o objeto falso é desalocado. Sabendo quais índices desses detectores estão no objeto falso, podemos encontrar objetos reais que são propriedades do bloco original.

bloco

Automação

A ferramenta realmente brilha quando é executada de forma contínua e automaticamente sobre os trabalhadores do build interno.

Automatizar a parte do lado do cliente é simples. Os engenheiros do Facebook instalaram o Retain Cycle Detector com temporizador e periodicamente verificam uma parte da memória para encontrar retais cycles. Não era inteiramente sem problemas. A primeira vez que eles executaram o detector, perceberam que não poderiam atravessar todo o espaço de memória rápido o suficiente. Era necessário fornecer um conjunto de objetos escolhidos a partir do qual ele iria começar a detecção.

Para fazer isso de forma eficiente, eles construíram o FBAllocationTracker. É uma ferramenta que monitora proativamente todas as alocações e de-alocações de quaisquer subclasses de NSObject. Ela pode rapidamente buscar qualquer instância de qualquer classe em qualquer momento, com mínima sobrecarga no desempenho.

Tendo que a automação do lado do cliente significa simplesmente usar FBRetainCycleDetector em um NSTimer, com a adição de pegar instâncias que queremos inspecionar com o FBAllocationTracker.

Agora vamos dar uma olhada de perto no que acontece no backend.

Ciclos de retenção podem consistir de qualquer número de objetos. As coisas ficam muito mais complexas quando muitos ciclos são criados por causa de um link ruim:

ciclo-1

A → B é um link ruim em um ciclo, e dois tipos de ciclos são criados por causa disso: A-B-C-D e A-B-C-E.

Isso forma dois problemas:

  1. Nós não queremos marcar dois ciclos de retenção separadamente, se eles são causados pelo mesmo link ruim.
  2. Nós não queremos marcar dois ciclos de retenção juntos, se eles podem representar dois problemas, mesmo se eles compartilham um mesmo link.

Então, precisamos definir clusters para ciclos de retenção. O Facebook escreveu um algoritmo para encontrar esses ciclos usando heurística:

  1. Reúna todos os ciclos detectados em um determinado dia.
  2. Para cada ciclo, extraia nomes de classes específicas do Facebook.
  3. Para cada ciclo, encontre o ciclo mínimo que foi reportado e está contido nesse ciclo.
  4. Adicione cada ciclo a um grupo representado pelo ciclo mínimo descrito acima.
  5. Relate apenas os ciclos mínimos.

Tendo isso, a última parte é descobrir quem poderia ter acidentalmente introduzido um ciclo de retenção em primeiro lugar. Eles fizeram isso usando ‘git/hg blame’ em partes do código a partir do ciclo e supondo que ele é provavelmente a mudança mais recente que poderia ter causado o problema. Aquela pessoa que mudou o código por último recebe uma tarefa pedindo para corrigir o problema.

Todo o sistema pode ser visualizado abaixo:

ciclo-2

Manual profiling

Enquanto automação ajuda a simplificar o processo de encontrar ciclos de retenção e reduzir a sobrecarga no desenvolvedor, manual profiling ainda tem seu lugar. Outra ferramenta que eles construíram permite que qualquer pessoa veja o uso de memória de um aplicativo, mesmo sem ter que conectar o seu telefone a um computador.

FBMemoryProfiler pode ser facilmente adicionado em qualquer aplicativo e permite que você faça o perfil manualmente e execute o detector de ciclos de retenção de dentro do aplicativo. Ele faz isso através da alavancagem  do FBAllocationTracker e do FBRetainCycleDetector.

Confira neste vídeo.

Generations

Uma das grandes características que o FBMemoryProfiler oferece é “generation tracking”, semelhante ao generation tracker do Instruments da Apple. Generations são simplesmente snapshots de todos os objetos vivos que foram alocados entre dois marcadores de tempo.

Usando a UI do FBMemoryProfiler, é possível marcar uma generation e, por exemplo, alocar três objetos. Em seguida, marca-se uma outra generation e continua-se alocando objetos. A primeira generation contém os primeiros três objetos. Se qualquer objeto é desalocado, ele é removido da segunda generation.

generation

Generation tracking é útil quando temos uma tarefa repetitiva que achamos que pode ser vazamento de memória, por exemplo, navegando para dentro e fora de uma View Controller. O Facebook marca uma generation cada vez que começa a sua tarefa e, em seguida, investiga o que sobra em cada generation. Se um objeto vive mais do que deveria, é possível ver isso claramente na UI do FBMemoryProfiler.

Dê uma olhada nelas

Se seu aplicativo é grande ou pequeno, tem muitas características diferentes ou apenas algumas, bom gerenciamento de memória é uma boa higiene de engenharia. Com estas ferramentas, o Facebook tem sido capaz de encontrar e corrigir vazamentos de memória muito mais facilmente, gastando muito menos tempo em processos manuais e mais tempo em escrever um código melhor. Gostaram das novidades, querem conhecer as ferramentas que o Facebook disponibilizou para o público? Acessem FBRetainCycleDetector, FBAllocationTracker e FBMemoryProfiler.

***

Fonte: https://code.facebook.com/posts/583946315094347/automatic-memory-leak-detection-on-

Mensagem do anunciante:

Conheça a Umbler, startup de Cloud Hosting por demanda feita para agências e desenvolvedores. Experimente grátis!

Source: IMasters

Categorizados em:

Este artigo foi escrito pormajor

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *