Gestão de memória em iOS e descobertas de Leaks com Instruments

xreee

xReee

Posted on May 4, 2020

Gestão de memória em iOS e descobertas de Leaks com Instruments

Gestão de memória em iOS e descobertas de Leaks com Instruments

Aut! Tenho certeza que você quer evitar erros inexplicáveis.

Imagine que seu app está rodando e de repente ele quebra em tempo de execução… “Erro de memória”. Você não sabe o que fazer, como consertar. E agora??

Você já ouviu falar em memory leak? Por que isso importa? Como descobrir se seu app tem isso? Muitas perguntas, estamos aqui para responder a todas elas.

1. Ciclo de vida de uma referência

Antes de mergulharmos nesse universo de gerencia de memória, é preciso entender antes o ciclo de vida de uma referência.

Funciona assim:

  1. Alocação — O programa pede para o computador um pedaço de espaço para guardar seu novo objeto

  2. Inicialização — Com o espaço reservado, o programa pode então inicializar seu objeto para poder utilizá-lo

  3. Uso — Com o objeto inicializado, o programa pode manusear o objeto da maneira que preferir

  4. Desinicialização — Ao fim do uso, o programa desinicializa o objeto e aquele espaço fica ali, esperando que o computador desaloque

  5. Desalocação — O espaço então é liberado e pode ser utilizado por qualquer programa disponível no computador

Você pode lembrar de inicializar, até de alocar, mas percebeu que não é preciso desalocar, e às vezes nem desinicializar, esses espaços? Não precisa se preocupar! As linguagens mais novas já tratam isso para você. No próximo ponto explicaremos como isso tudo ocorre!

2. Contagem de referencia e Arc

Swift e Objective-C possuem contagem de referências automáticas, mais conhecido como ARC ( Automatic reference counting). Ele é o responsável por desalocar memórias que já estão desinicializadas. Vamos aprofundar mais..

Já ouviu falar de Garbage Collector (GC) ? Se não, é um conceito simples: ele é exatamente o que o nome diz: um coletor de lixo! Se tem uma memória que foi alocada para algum objeto e ela não está sendo utilizada, ele vai lá e "Olha! Tem lixo aqui.", reciclando aquele espaço e liberando automaticamente para você, desenvolvedor, utilizar com novos dados. Se você quer entender mais afundo isso, sugiro esse link.

Mas nem tudo são flores, recolher todo esse lixo consome processamento. Em Java, por exemplo, existe um método que é chamado várias vezes para verificar a árvore de alocações e assim deletar tudo que não estiver sendo utilizado. Isso gera problemas que muitos autores escreveram tentando contornar.

Mas o GC tem relação com o ARC? Sim e não.. Em iOS (isso inclui Obj-c e Swift) o recolhimento desse lixo todo acontece de um modo diferente. Nesse novo universo, toda vez que você aponta uma referência para um objeto ele incrementa um contador, se esse contador é zero, significa que esse objeto não tem mais como se comunicar com ninguém e por isso ele é desinicializado ( e consequentemente desalocado). Tudo isso acontece de forma assíncrona e por isso não consome tanta memória.

Se o parágrafo anterior ficou confuso, não tema! Com exemplos tudo fica mais fácil. Vamos supor que criamos uma classe chamada Site:

class Site {
    let nome: String
    init(nome: String) {
        self.nome = nome
    }
    deinit {
        print("O site \(nome) foi desinicializado")
    }
}

Por enquanto, não existe nenhuma referência apontando para o Site, então seu contador é zero. Vamos criar alguns objetos, assim que inicializados, em swift, eles já automaticamente alocam espaço na memória…

var medium = Site(nome: "Medium") 
      // medium está ligado a ref. medium -> Referencia apontando para medium +1
      // ARC do Medium = 1
var copiaMedium1 = medium
      // copiaMedium1 está ligado a ref. medium -> Referencia apontando para medium +1
      // ARC do Medium = 2
var copiaMedium2 = medium
      // copiaMedium2 está ligado a ref. medium -> Referencia apontando para medium +1
      // ARC do Medium = 3

Adicionamos 3 sites na lista de "Referências apontando para Médium". O contador então está em 3. Isso faz com que os três objetos não desapareçam da memória e fiquem alocados até que sejam desinicializados:

medium = nil
      // medium está ligado a ref. medium -> Referencia apontando para medium -1
      // ARC do medium = 2
copiaMedium1 = nil
      // copiaMedium1 está ligado a ref. medium -> Referencia apontando para medium -1 
      // ARC do medium = 1
copiaMedium2 = nil
      // copiaMedium2 está ligado a ref. medium -> Referencia apontando para medium -1 
      // ARC do medium = 0

Como o número de referências apontando para Medium chegou a zero e então todo aquele espaço que foi utilizado pelas referências é automaticamente desalocado pelo ARC.

Se o ARC não consome tanto processamento então ele é perfeito? Apple arrasou? Hm… existe um probleminha conhecido chamado "Ciclo de referência".

3. O famoso problema do "Ciclo de Referências"

Como o ARC funciona muito bem, muitos iniciantes em desenvolvimento iOS não precisam se preocupar com tudo o que foi dito até agora. Mas o problema do ciclo de referências é algo que precisa de cuidado e, principalmente em projetos grandes, pode causar crashes inesperados (chega rimou). Entender o que acontece previne esses problemas e você vai descobrir que para consertar é muito simples! (E é tão importante que é cobrado em algumas entrevistas de emprego).

Até agora, vimos que o ARC conta as referências e só desaloca quando o número chega a zero. O problema é quando esse número nunca chega a zero. Isso acontece quando um objeto prende a outra na memória e vice-versa. Vamos voltar ao exemplo, primeiro adicionamos uma classe Pessoa e logo em seguida referenciamos ela na classe Site:

class Site {
    var pessoa: Pessoa?
    let nome: String

    init(nome: String, pessoa: Pessoa?) {
        self.nome = nome
        self.pessoa = pessoa
    }
    deinit {
        print("O site \(nome) foi desinicializado")
    }
}

class Pessoa {
    let nome: String

    init(nome: String) {
        self.nome = nome
    }
    deinit {
       print("A pessoa \(nome) foi desinicializada")
    }
}

Vamos entender melhor como vai funcionar as referências nesse caso:

Quando iniciarmos um objeto de Site e um de Pessoa, atribuiremos uma ligação entre os dois. Nesse caso, a Pessoa só poderá ser desinicializado após a desinicialização do Site ( Já que esse aponta para ela, contando +1 para o ARC).

var renata: Pessoa?
    var medium: Site?

    func iniciarReferencias() {
        renata = Pessoa(nome: "Renata")
        medium = Site(nome: "Medium", pessoa: renata)
    }
    func desiniciarReferencias() {
        renata = nil // Não está desinicializada ainda está viva aqui
        medium = nil //Agora sim, o contador de referencias para Pessoa é igual a zero
        // renata & medium foram desinicializados e desalocados
    }

Logo de cara você percebe um problema: Se existir uma ligação ativa entre dois objetos, você só consegue desinicializar um se também apagar aquele que recebe a ligação. Mas isso não é necessáriamente um problema, pois em alguns casos até faz sentido ter.. o real problema é quando também existe uma ligação de volta!

Agora vamos adicionar o Site em pessoa:

class Pessoa {
    var site: Site?
    let nome: String

    init(nome: String, site: Site?) {
        self.nome = nome
        self.site = site
    }
    deinit {
       print("A pessoa \(nome) foi desinicializada")
    }
}

E o desenho das referências fica assim:

Se antes a Pessoa só podia ser desinicializada depois do Site sumir, agora o Site só some se a Pessoa desinicializar… E agora? Vai ficar nesse vai e volta eternamente. Isso é o que chamamos de vazamento de memória ou pra quem gosta dos termos em inglês: Memory leak.

4. Tipos de referência: strong, weak e unowned

Apesar de assustar no inicio, é muito simples consertar o problema do ciclo de referências. Swift e Objective-c possuem uma especie de "Grau" de ligação. Até então estávamos fazendo ligações fortes, que são default. Vamos entender o que cada grau representa:

Strong — Uma referência forte será contada pelo ARC. Em resumo, ela garante a existência de um objeto que possua ligação.

Weak — É exatamente o contrário da strong, uma referência weak permite a comunicação das duas classes mas não é contabilizada pelo ARC

Unowned — É muito parecido com a weak, pois não é contabilizada pelo ARC, porém, quando uma referência é fraca, existe uma chance dela ser desinicializada antes de seu uso, o que faz com que ela seja opcional. Diferente de quando ela "não tem dono", ligações unowned garantem que aquela variável não será opcional mas também não seguram o objeto na memória.

Sabendo disso, podemos mudar uma das duas classes, explicitando que uma ligação é weak, e assim contaremos apenas uma vez:

class Pessoa {
    var weak site: Site? // Agora a ligação é fraca
    let nome: String

    init(nome: String, site: Site?) {
        self.nome = nome
        self.site = site
    }
    deinit {
       print("A pessoa \(nome) foi desinicializada")
    }
}

O desenho vai ficar desse jeito:

Agora, se o Site for desinicializado, a Pessoa estará livre para também sair da memória! E com uma vantagem: teremos acesso a pessoa do mesmo jeito! Incrível não?

Na minha classe, pessoa está como opcional, se você quer que ela não seja opcional, você pode trocar o weak por unowned que funcionará do mesmo jeito.

5. Mas e na vida real?

Até agora usei exemplos lúdicos para explicar toda a problemática que envolve o ciclo de referencia. Quando eu estava aprendendo sobre isso, ficava me perguntando o porquê disso, não parece algo aplicável e também não te dá pistas do problema que você pode estar passando. Pois bem, nesse ponto quero falar um pouco de como problemas de memória surgem na vida real. Vamos considerar o exemplo abaixo:

protocol DelegateAleatorio {
    func queroComer()
}

class ViewFaminta: UIView {
  var delegate: DelegateAleatorio?
}

class ExemploViewController: UIViewController, DelegateAleatorio {
    override func viewDidLoad() {
       super.viewDidLoad()
       let viewFaminta = ViewFaminta()
       viewFaminta.delegate = self
       self.view.addSubview(viewFaminta)
     }

    func queroComer() {
        print("Pai, tenho fome")
    }
}

Está um pouco implícito, mas veja: Você tem um objeto que possui um delegate:

No entanto, a ViewFaminta está sendo inicializada por uma Controller que aplica o protocolo. E então temos um ciclo de referência:

E vocês já sabem como consertar não é? Basta adicionar weak antes do delegate na classe da ViewFaminta!

Se você tentou implementar, vai notar um probleminha: Weak só pode ser adicionado à classes, logo esse protocolo precisa explicitar que é aplicável apenas à classes, para consertar isso é simples: basta indicar isso no protocolo.

Com as mudanças acima, o resultado final é esse:

protocol DelegateAleatorio: class {
    func queroComer()
}
class ViewFaminta: UIView {
  weak var delegate: DelegateAleatorio? //Agora essa ligação é fraca
}

class ExemploViewController: UIViewController, DelegateAleatorio {
    override func viewDidLoad() {
       super.viewDidLoad()
       let viewFaminta = ViewFaminta()
       viewFaminta.delegate = self
       self.view.addSubview(viewFaminta)
     }

    func queroComer() {
        print("Pai, tenho fome")
    }
}

Além de delegate, existem muitos outros casos que podem causar memory leaks, como clousures, por exemplo.

6. Encontrando leaks com o Xcode

Agora que você sabe as causas, vamos descobrir como achar ciclo de referencias. Com aquele código de Site e Pessoa, vou gerar o problema de propósito. Com o código rodando, vou pressionar o botão do lado do debuger de interface. Ele se chama View Memory Graph Hierarchy, e como vocês já devem imaginar, a função é mostrar a hierarquia de memória.

No inicio é um pouco difícil entender, mas você vai ver que é simples. Vai abrir um menu do seu lado esquerdo. Esse menu mostra toda a hierarquia de memória existente na aplicação.

Dentro do circulo azul está um exemplo de warning. Ela avisa que ali tem problemas de memória, se você clicar nela, terá mais detalhes.

Dentro do circulo vermelho temos um filtro que mostra apenas referencias que possuem warnings, pressionando ele fica bem mais fácil ver onde tem problemas.

Ao apertarmos em cima da warnings, teremos os detalhes do que possivelmente está acontecendo. No meu caso, existe um ciclo de referencia entre dois objetos Pessoa e Site. Entre os dois existe um "2", indicando que aquela ligação está sendo contada duas vezes.

7. Encontrando leaks com o Instruments

Parece com encontrar leaks no Xcode, porém muito mais fácil de entender e ainda de encontrar qual o trecho do código que está causando o problema. Para iniciar o Instruments pressione as teclas command⌘+i, ou no menu superior → Product → Profile.

Com o Instruments aberto, selecione Leaks e depois em choose. E após isso, pressione o botão de record (Esse com um circulo vermelho) :

Seu app irá rodar no simulador que você escolheu (no meu caso iPhone 8), utilize seu app normalmente e espere os registros aparecerem na timeline. Em cima aparecerá todas as alocações, em baixo possíveis leaks. Tudo isso de forma automática.

Aquele X vermelho indica a presença de Leaks. Existem outros indicadores:

  • O verde indica que não existem leaks

  • O vermelho indica que existem novos leaks

  • O cinza indica que existem leaks, mas eles já foram descobertos antes

A cada X segundo é tirado um Snapshot, onde são verificados a presença de leaks, novos ou velhos, e assim aparecerão na timeline. O instruments ainda oferece opções de customização do intervalo entre snapshots. (No menu no canto inferior)

Ao apertar nos indicadores (ou no grande X vermelho) eu consigo descobrir quais as razões dos meus leaks. Nesse caso, são estes:

E assim a causa foi descoberta! É possível navegar em tipos de menu diferentes, por exemplo, abaixo estou trocando de "Leaks by Backtrace" por "Cycles and Roots".

Perceba ainda que eu utilizo o menu lateral direito para me mostrar qual o exato local em que o problema está. Basta selecionar uma das funções em que o Instruments está detectando o leak e dar dois cliques que ele abrirá o trecho do código.

É isso pessoal! Espero que tenham gostado. Até a próxima 🥰

Referências:
https://medium.com/computed-comparisons/garbage-collection-vs-automatic-reference-counting-a420bd4c7c81
https://krakendev.io/blog/weak-and-unowned-references-in-swift
https://somekindofcode.com/memory-leaks-by-custom-delegate/

💖 💪 🙅 🚩
xreee
xReee

Posted on May 4, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related