Entendendo [ weak self ] no Swift

Jonatan Medina
10 min readAug 3, 2022

No contexto do Swift, a utilização de [weak self] cria uma referência fraca ao próprio objeto (self) dentro de uma closure. Essa abordagem é primordial para evitar vazamentos de memória que podem ocorrer devido a ciclos de referências fortes.

No entanto, para compreender essa técnica de maneira abrangente, é fundamental ter uma compreensão sólida dos seguintes conceitos: Contagem Automática de Referência (ARC), Referência Forte (Strong Reference), Referência Fraca (Weak Reference) e Ciclo de Referências Fortes (Strong Reference Cycle).

Vamos explorar minuciosamente cada um desses conceitos, pois essa análise mais aprofundada proporcionará uma compreensão mais clara do motivo por trás do uso de [weak self] em clausuras.

Como era o gerenciamento de memória no Swift no passado?

Antigamente, a responsabilidade de gerenciar a memória de um aplicativo recaía inteiramente sobre o desenvolvedor. Isso significava que ao declarar um objeto, era necessário alocar manualmente espaço na memória para ele, de forma similar a linguagens como C++ e Pascal. Por exemplo, ao criar um objeto de carros (cars), era preciso alocar espaço na memória da seguinte forma:

Cars.alloc()

E quando fosse necessário descartar esse objeto, a memória consumida por ele também precisava ser liberada:

Cars.release()

Imagine realizar essa gestão em um aplicativo extenso, com possivelmente uma grande quantidade de objetos. Essa tarefa tornava-se monumental e praticamente inviável. Havia o risco de esquecer de liberar memória, resultando em vazamentos, ou liberar prematuramente, causando comportamentos inesperados.

Como solução para esses desafios, a Apple introduziu o Automatic Reference Counting (ARC). O ARC é um sistema integrado de gerenciamento de memória que aloca e libera automaticamente a memória. Esse avanço permitiu que os desenvolvedores se concentrassem mais na lógica do aplicativo e menos na complexa tarefa de gerenciar manualmente o uso da memória.

ARC — Automatic Reference Counting

ARC é um recurso integrado de gerenciamento de memória, em suma, ele analisa constantemente o estado do seu aplicativo, quando vê que um objeto não é mais necessário, ele libera automaticamente o objeto da memória. O ARC rastreia a contagem de referência de cada objeto: Uma contagem de referência é um número que revela "quantos objetos estão usando o objeto X", caso o número for zero, ARC entende que este objeto não esta sendo usando em nenhum lugar, então pode ser liberado da memória. Uma contagem de referência aumenta em 1 sempre que existe uma referência forte ao objeto. Quando essa referência é removida, a contagem diminui em 1.

Referências fortes em Swift

Quando você cria uma variável utilizando var no Swift, por padrão, está criando uma referência forte para um objeto. Vamos agora compreender o que é o ARC e como as referências fortes funcionam nesse contexto.

var name = "Bill Gates"

Por baixo dos panos, "Bill Gates" é um objeto do tipo String com uma referência forte vinculada a ele, por meio da variável denominada name. Isso aumenta a contagem do ARC associada a "Bill Gates" para 1..

Caso atribuamos nil à variável name, quebramos essa referência forte com "Bill Gates". Assim, a contagem associada a "Bill Gates" é reduzida para 0, já que não há mais nenhuma variável fazendo referência a ele. Novamente, nos bastidores, o ARC entra em ação, liberando a memória anteriormente ocupada por "Bill Gates".

Essa explicação fornece uma base fundamental para compreender o funcionamento do ARC e o conceito de referências fortes.

Para verificar efetivamente como o ARC está atuando em segundo plano, é possível criar um destrutor para uma classe. Vamos explorar esse conceito a seguir.

O Método deinit() no Swift

Uma maneira de observar o ARC realizando sua tarefa é executar um trecho de código antes que ele libere a memória. Isso é possível através do método deinit(). Esse método é acionado antes de o ARC iniciar a limpeza.

class Car {
var name = "Opala"
deinit() {
print("Memória será liberada =)").
}
}

Agora, podemos criar um objeto Car e associá-lo a uma variável chamada opala:

var opala: Car? = Car()

Agora temos a contagem de referência de Car em 1.

A seguir, atribuiremos nil a opala:

opala = nil

Como dito antes, isso reduz a contagem de referência do objeto Car para 0 e aciona ARC para liberar a memória em uso. É quando o método deinit() é executado e mostra no console a frase que colocamos.

Memória será liberada =)

Esse processo permite que você verifique se o ARC realizou efetivamente sua tarefa. Utilizar o deinit() dessa maneira será útil no próximo tópico que abordaremos.

Vamos agora analisar um cenário em que dois objetos estão fortemente interligados, impedindo o ARC de executar sua função corretamente.

Ciclo de Retenção (Retain Cycle)

Como vimos, o ARC libera a memória de um objeto quando sua contagem de referências atinge zero. Observamos também um exemplo de como o ARC age quando uma referência é eliminada de um objeto. No entanto, surge uma questão: o que acontece quando dois objetos estão fortemente interligados?

Nesse cenário, ocorre um ciclo de retenção (retain cycle), o qual impede o ARC de liberar a memória consumida pelos objetos, pois a contagem de referências para ambos permanece em 1.

Vamos ilustrar essa situação criando duas classes, Car e Owner:

class Car {
let name: String
var owner: Owner?
init(name: String) {
self.name = name
}
deinit() {
print("Car free")
}
}

class Owner {
let name: String
var car: Car?
init(name: String) { self.name = name }
deinit() { print("Owner free") }
}

Podemos observar que um objeto Car pode possuir um objeto Owner, e vice-versa. Isso leva a uma forte referência mútua. Agora, vamos criar um "ciclo de retenção" entre esses objetos:

var car: Car? = Car(name: "Dodge Charger")
var owner: Owner? = Owner(name: "Steve Jobs")
car?.owner = owner
owner?.car = car

Em seguida, ao atribuirmos nil a car e owner, esperaríamos que os métodos deinit() fossem acionados antes que o ARC limpasse os objetos:

car = nil
owner = nil

No entanto, a execução desse código não gera nenhuma saída no console. Os métodos deinit() dos objetos não foram chamados. Isso ocorre devido ao fato de o ARC não conseguir liberar a memória usada pelos objetos devido ao ciclo de retenção. A contagem de referências permanece sempre em 1 para ambos objetos, pois um faz referência ao outro.

Esse problema acarreta em um vazamento de memória. Ainda que os objetos Car e Owner apareçam como nulos quando impressos, o ARC não consegue desocupar o espaço ocupado por eles. Consequentemente, mesmo que não devesse, em algum ponto da memória, ainda há alocação para esses objetos. Esse tipo de vazamento de memória pode ser crítico, uma vez que pode consumir todo o espaço alocado para o aplicativo.

Felizmente, essa situação pode ser contornada utilizando referências fracas em vez de fortes.

Referências fracas em Swift

Referências fortes podem resultar em “ciclos de retenção” que levam a vazamentos de memória no seu aplicativo, já que essas referências impedem que o ARC libere a memória apropriada. Para superar esse obstáculo, é fundamental criar referências que não incrementem a contagem de referências, permitindo que o ARC funcione de maneira eficaz. É neste ponto que entram em cena as referências fracas.

Uma referência fraca é um tipo de ligação que não contribui para o aumento da contagem de referências, permitindo que o ARC libere o espaço ocupado pelo objeto a qualquer momento necessário. Ao desejar criar uma referência fraca, basta anteceder o nome da variável com a palavra-chave weak.

Voltando ao problema anterior, tínhamos um cenário em que um objeto Car e um objeto Owner criavam um "ciclo de retenção", no qual um Car fazia referência a um Owner, que, por sua vez, referenciava o mesmo Car. A solução para quebrar esse ciclo é declarar uma das referências como fraca, utilizando a marcação "WEAK".

Considere a implementação a seguir:

class Car {
let name: String
weak var owner: Owner?
init(name: String) {
self.name = name
}
deinit() {
print("Car free")
}
}

class Owner {
let name: String
var car: Car?
init(name: String) { self.name = name }
deinit() { print("Owner free") }
}

Em seguida, vamos executar novamente:

var car: Car? = Car(name: "Dodge Charger")
var owner: Owner? = Owner(name: "Steve Jobs")
car?.owner = owner
owner?.car = car
car = nil
owner = nil

A saída obtida:

Owner free
Car free

Agora, os métodos deinit() foram acionados com sucesso. Isso indica que o ARC conseguiu liberar a memória ocupada por esses objetos. O motivo é que car.owner é uma referência fraca, a qual não impacta na contagem de referências do Owner.

Até o momento, podemos fazer as seguintes considerações:

Uma referência forte é uma variável que aponta para um objeto em Swift.

Uma referência forte aumenta a contagem de referências em 1.

O ARC libera a memória alocada para um objeto quando sua contagem de referências atinge 0.

Um “ciclo de retenção” ocorre quando há referências bidirecionais entre object1 e object2, impedindo a contagem de referências de atingir 0 e causando vazamentos de memória.

Para evitar tais ciclos, recorra ao uso de referências fracas (weak).

Utilizando [weak self] no Swift

Em Swift, a utilização de [weak self] é uma abordagem fundamental para evitar vazamentos de memória em seu aplicativo, especialmente ao lidar com closures. Ao empregar [weak self], você instrui o compilador a criar uma referência fraca para self. Em outras palavras, isso permite que o ARC libere self da memória conforme necessário.

Closures e Ciclos de Referências Fortes (Strong Reference Cycles)

Em Swift, closures capturam o contexto em que são definidas, o que significa que qualquer elemento mencionado dentro de uma closure será fortemente referenciado por essa closure. Embora sejam uma ferramenta poderosa, é crucial exercer cautela no gerenciamento de memória ao utilizá-las. Semelhante às classes, as closures também são tipos de referência.

Ao ter uma closure dentro de uma classe que faz uso de self, essa closure manterá uma forte referência a self enquanto estiver em memória. Agora, imagine que self esteja relacionado a um controlador de visualização (view controller) e essa closure realize uma operação sem [weak self]. Isso resultará em um ciclo de referência forte entre a closure e o controlador de visualização.

Caso você remova o controlador de visualização da pilha de navegação, ele permanecerá retido pela closure. Em outras palavras, o controlador de visualização continuará a operar mesmo após ter sido removido. Esse cenário pode levar a comportamentos inesperados, como exceções ou falhas de memória.

Para evitar essa situação, a utilização de [weak self] na closure é a solução, já que isso torna a referência a self fraca, permitindo que o ARC libere a memória quando necessário.

É importante compreender que [weak self] é uma conveniente sintaxe para criar uma referência fraca a self. Por exemplo:

let makeSomeJoke = { [weak self] in
self?.randomJoke()
}

Em uma função, isso é equivalente a:

weak var self_ = self
let makeSomeJoke = {
self_?.randomJoke()
}

Vejamos um exemplo prático que envolve uma closure criando um “Ciclo de Referências Fortes”:

import Foundation
class StopWatch {
var elapsedTime: Int = 0
var timer: Timer?

func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.elapsedTime += 1
}
}
}

A classe StopWatch emprega uma closure que incrementa o tempo decorrido a cada segundo. No entanto, ao acessar a propriedade elapsedTime via self, essa closure estabelece uma forte referência a self. Além disso, o temporizador (timer) também possui uma forte referência à classe StopWatch (ou seja, self). Isso resulta em um "ciclo de retenção" (retain cycle).

Para superar esse ciclo de referência, adicionamos um método stop() que interrompe o temporizador:

func stop() {
timer?.invalidate()
timer = nil
}

No entanto, mesmo ao chamar stop(), o temporizador não libera a memória devido ao ciclo de referência entre as duas entidades. Como podemos quebrar esse "Ciclo de Referências Fortes"?

Uma maneira eficaz é empregar uma referência fraca entre a closure e self:

import Foundation
class StopWatch {
var elapsedTime: Int = 0
var timer: Timer?

func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.elapsedTime += 1
}
}

func stop() {
timer?.invalidate()
timer = nil
}
}

Com essa implementação, o “Ciclo de Referências Fortes” é quebrado com sucesso, permitindo que o ARC libere a memória ocupada por self.

Um ponto a ser observado é que, no último código, a acessibilidade de self ocorre através de ? (ou seja, self?). Isso se deve ao fato de que self agora é um valor opcional.

Para concluir, abordemos algumas formas de lidar com um self opcional dentro de uma closure.

Referências Fracas versus Opcionais (Weak References and Optionals)

Ao empregar [weak self] em uma closure, self passa a ser um valor opcional. Isso ocorre porque self pode se tornar nil a qualquer momento devido à referência fraca. Portanto, ao acessar self, a sintaxe é self?:

self?.elapsedTime += 1

No entanto, lidar com opcionais em múltiplos locais pode ser um tanto incômodo. Felizmente, existe uma forma de verificar se self é nil. Isso pode ser realizado diretamente dentro da closure, empregando guard let:

timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.elapsedTime += 1
}

Através do uso do guard let, certificamo-nos de que self não seja nil. Se a condição for atendida, a execução continua; caso contrário, a operação é interrompida.

Considerações finais

Ao explorar a utilização de [weak self] para prevenir vazamentos de memória em closures, mergulhamos em um conjunto essencial de conceitos, incluindo Automatic Reference Counting (ARC), referências fortes e fracas, bem como os desafios representados pelos "ciclos de retenção" (retain cycles).

Em resumo, identificamos que cada objeto em Swift é acompanhado por uma contagem de referências, indicando quantas variáveis ou objetos possuem referências fortes a esse objeto. O ARC entra em ação quando a contagem de referências chega a 0, liberando a memória associada. Além disso, compreendemos como “ciclos de retenção” podem surgir quando objetos referenciam-se mutuamente, bloqueando a liberação de memória.

A solução para quebrar esses ciclos reside na utilização de referências fracas. Introduzir uma referência fraca, como weak car: Car?, é suficiente para evitar a retenção excessiva de memória entre objetos.

Nossas explorações também abrangeram as implicações das closures, que capturam referências ao contexto no qual são definidas. Observamos que, quando uma closure dentro de uma classe chama propriedades dessa classe (utilizando self), uma referência forte é criada, dando origem a um ciclo de retenção. Esse ciclo pode ser interrompido com a introdução de [weak self], que transforma a referência da classe em uma opcional. Esse ajuste exige que lidemos com opcionais dentro da própria closure.

Ao compreender as nuances das referências em Swift, incluindo o uso criterioso de referências fortes e fracas, você está equipado para criar aplicativos mais confiáveis e eficientes. Manter um equilíbrio entre a criação de funcionalidades impactantes e a gestão responsável da memória é a chave para proporcionar experiências de usuário fluidas e evitar problemas que possam comprometer a estabilidade do aplicativo. Portanto, ao implementar estratégias inteligentes de gerenciamento de memória, você está capacitado para criar aplicativos de alta qualidade que atendam às expectativas e necessidades dos usuários.

Referências

ARC: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

Closures: https://docs.swift.org/swift-book/LanguageGuide/Closures.html

--

--

Jonatan Medina
Jonatan Medina

No responses yet