Evitando retenções cíclicas em Swift
Se você está interessado em desenvolver um aplicativo para iOS de alta performance, mais cedo ou mais tarde você terá que pensar em como os seus componentes estão consumindo a memória disponível e como você pode otimizá-los.
Um problema comum relacionado a memória é o de retenção cíclica. Mas antes de definirmos o que é uma retenção cíclica, que tal revisarmos um pouco de como o iOS gerencia a sua memória?
ARC
O método de gerenciamento automático de memória utilizado pela Apple é o ARC (Automatic Reference Counting). Como o nome diz, o número de referências é utilizado para definir se um bloco de memória deve ou não ser desalocado.
Quando um objeto é criado, sua contagem de referências começa em 1 e vai aumentando conforme o objeto é referenciado. Quando uma referência a esse objeto deixa de ser referenciado, a contagem diminui. Por fim, quando ela chega a 0, o objeto é desalocado da memória.
Um exemplo desse ciclo de vida pode ser visto abaixo:
Referências fortes e fracas
Bom, agora é um bom momento de explicar o que são referências fortes e referências fracas. Na declaração de uma variável é definido se ela é forte ou fraca, sendo forte por padrão.
Variáveis fortes incrementam a contagem de sua referência. Por exemplo, se um objeto está com 2 referências e é atribuído para uma nova variável forte, ele passará a ter 3 referências.
Já variáveis fracas não incrementam essa contagem. Se um objeto está com 2 referências e é atribuído para uma variável fraca, ele continuará com 2 referências.
Na prática, isso significa que é garantido que, enquanto uma variável forte está ativa, o componente referenciado permanecerá em memória. Já para o caso da variável fraca, não há garantia que, durante o seu ciclo de vida, o componente referenciado sempre existirá.
Retenções cíclicas e memory leaks
Provavelmente você já deve ter visto alguma modelagem em que uma entidade A pode ter múltiplas instâncias de entidade B e que cada instância da entidade B está associada à uma entidade A. Por exemplo, um livro pode ter várias páginas mas cada página está associada a apenas um livro.
Em uma implementação bem simplificada, esses modelos poderiam ficar assim:
Aparentemente, tudo certo, né? Errado!
Repare que o livro tem uma referência forte para as páginas e a página tem uma referência forte para o livro. O que você acha que acontece agora? Isso mesmo, um memory leak.
Um memory leak ocorre quando um conteúdo permanece na memória mesmo após deixar de ser utilizado pelo aplicativo.
No caso do livro e da página, o memory leak é causado por duas referências fortes que se referenciam. A esse problema chamamos de retenção cíclica (retain cycle).
Explicado o problema, vamos entender agora algumas soluções.
Structs e Classes
Uma escolha entre usar uma struct ou usar uma class pode ou não causar uma retenção cíclica.
Tanto structs quanto classes podem ter constantes, variáveis, funções. Ambas podem adotar protocolos. Então, qual a diferença entre elas?
A maior diferença entre elas é que structs utilizam passagem por valor e classes utilizam passagem por referência. Tá, mas o que isso significa?
Na passagem por valor, o conteúdo de uma variável é copiado e atribuído a outra variável, mantendo a variável original intacta.
Olhe o código abaixo:
Em um playground, temos a seguinte saída:
Element(name: “A”, number: 1)
Element(name: “B”, number: 2)
Mesmo gerando a segunda variável a partir da primeira e depois alterar os seus valores, a variável original continua intacta. Isso ocorre porque a segunda variável é uma cópia da primeira.
Agora, na passagem referência, ao atribuir uma variável a outra variável, a segunda não é uma cópia da primeira e sim sua nova referência. Isso significa que se alterarmos uma das variáveis, a outra refletirá a mesma mudança.
E se usarmos uma class em vez de uma struct?
Agora a saída passará a ser:
Element(name: B, number: 2)
Element(name: B, number: 2)
Note que agora, efetuando os mesmos passos, ambas as variáveis passaram a ter os mesmos valores. Ou seja, a variável original foi modificada.
Ok, mas por que a passagem por valor e a passagem por referência seriam tão importantes?
Lembra daquele papo de que precisamos decidir se usarmos structs ou classes e que isso pode gerar memory leaks?
Então, e se usarmos structs no exemplo do livro e da páginas?
Dessa forma não haverá um problema de referência cíclica porque o livro armazenado na página será uma cópia do livro passado no construtor.
Também podemos continuar usando classes para esse casos mas utilizando uma variável fraca:
Assim, o livro armazenado na página torna-se um opcional, já que em algum momento ele pode ficar nulo.
Protocols
Protocols são amplamente usados em Swift e podem ser adotados por classes, structs e enums. Porém, em alguns casos, se não forem tomados alguns cuidados, também podem ocorrer problemas de retenção cíclica com eles.
Imagine um delegate genérico assim:
Nesse caso a variável delegate da ListViewController é do tipo forte e pode ser atribuída para qualquer tipo que conforme com seu protocolo. Porém, por ser uma variável forte, dependendo da implementação, pode ocorrer um risco de retenção cíclica. E como minimizar esse risco?
Poderíamos usar uma variável fraca, mas se usarmos a keyword weak no código acima teremos um erro de compilação, pois structs e enums não são compatíveis com variáveis fracas. E agora?
Bom, quando declaramos um protocolo, é possível especificar que eles só poderão ser implementados por tipos de classes. Dessa forma, podemos utilizar variáveis fracas normalmente.
Ficaria assim:
Closures
Closures são um dos recursos mais robustos de Swift mas também não são imunes à problemas de retenção cíclica.
Closures podem ocasionar retenções cíclicas por uma simples razão: por padrão elas referenciam as classes nas quais são chamadas de maneira forte.
Abaixo temos um exemplo de retenção cíclica com closures:
Aqui a class tem uma referência forte para a closure e a closure, por padrão, uma referência closure para o objeto da class.
E como tratar esse comportamento das closures?
Existem duas maneiras de tornar a referência ao objeto da classe das closures uma referência fraca. A primeira é usar [unowned self]:
Agora a closure não possui mais uma referência forte. Porém, tome cuidado ao usar [unowned self], pois se a closure for chamada e depois o seu objeto ser deslocado, ocorrerá um crash no seu aplicativo.
A segunda maneira é usar [weak self]:
[weak self] funciona da mesma maneira que [unowned self], porém ele trata a referência ao objeto como um opcional.
Se você já chegou aqui, já sabe o básico sobre como tratar referências cíclicas em Swift. Lembre-se dessas dicas quando for declarar variáveis, protocolos e implementar closures e evitará muitos problemas.
Sou o André. Um desenvolvedor mobile apaixonado por tecnologia. Para qualquer dúvida ou sugestão, estou por aqui.