Weak self e otras cositas más

Fernando Luiz Goulart
8 min readMar 22, 2023

--

Como as escaping closures capturam variáveis no Swift

Imagem de dois meninos levantando pesos. Um deles está forte e levanta os pesos com facilidade. O outro está fraco e está suando e com uma cara de desespero. Imagem obtida no site Freepik de autoria de brgfx.

TL;DR

Escaping closures e gerenciamento de memória no Swift são assuntos difíceis de entender. Para observar o funcionamento na prática, criei o projeto CaptureListPlayground e disponibilizei nesse repositório.

🚨 Esse artigo é considerado avançado e requer que o leitor tenha algum conhecimento prévio de Swift como Optionals, Value Type, Reference Type, ARC, Weak, Escaping Closures e Non-Escaping Closures, Main Queue, Global Queue, Dispatch Sync e Dispatch Async.

Após analisar a execução do projeto CaptureListPlayground e estudar os artigos mencionados aqui, cheguei às seguintes conclusões:

Sobre capture list:

  • Conhecer e usar de forma consciente a capture list nos dá maior controle sobre a execução da closure e do gerenciamento de memória do nosso app.
  • Variáveis do tipo “value type” (ex: structs, strings, ints) quando são adicionadas na capture list geram novas cópias independentes das mesmas. É como uma “foto independente” das variáveis originais tirada no momento em que a closure foi enviada como parâmetro.
  • Por outro lado, variáveis do tipo “reference type” (ex: classes) são capturadas implicitamente estando ou não na capture list. A exceção é com o uso de weak ou unowned. Weak e unowned previnem que a closure mantenha essa instância em memória e cause algum possível vazamento. Por outro lado, isso pode trazer algum comportamento indesejado já que quando a closure executar, a instância poderá já ter sido removida da memória.
  • 🚨 Quando vc inclui na capture list uma variável optional ela será tratada sempre como um value type mesmo que guarde (em .some) uma variável “reference type”, por exemplo uma instância de classe. Ou seja, isso fará com que seja criada uma nova variável independente (seja uma cópia de um value type ou uma nova referência para um reference type).

Sobre o uso de [weak self] na capture list:

  • Não são todas as closures que necessitam de [weak self]. Apenas aquelas que obedecem a três critérios:
    1. São escaping
    2. São armazenadas em uma variável ou são enviadas como parâmetro para outra closure
    3. Possuem pelo menos um objeto com referência forte para a closure (ex: self quando a closure é uma variável da classe)

Introdução

Uma das coisas mais confusas no Swift, na minha opinião, é saber quando devemos utilizar [weak self] nas closures.
Para entender melhor isso, e também como as closures capturam as variáveis de um modo geral, criei o projeto CaptureListPlayground que disponibilizei nesse repositório.

Projeto CaptureListPlayground

Nesse projeto bem simples temos duas classes:

  • Classe A
  • Classe B

Classe A

A classe A (código mais abaixo) tem:

  • Dois atributos do tipo String: name e surname
  • Dois métodos setter: setName(_ newName:) e setSurname(_ newSurname:)
  • Um método para imprimir os atributos na console: printIt()
  • Um método deinit. Lembrando que o deinit é executado toda vez que uma instância dessa classe é removida da memória
  • O método run(_classB:, newName:, newSurname:) que é onde a mágica acontece. Discutiremos esse método com detalhes mais adiante.
class ClassA {
var name: String = "Empty FirstName"
var surname: String = "Empty LastName"

func setName(_ newName: String) {
self.name = newName
}

func setSurname(_ newSurname: String) {
self.surname = newSurname
}

func printIt() {
print("\(name) \(surname)")
}

func run(_ classB: ClassB, newName: String, newSurname: String) {
let otherRef: ClassA = ClassA()
classB.execute { [weak self, name, surname] in
DispatchQueue.global().async {
print("Please wait 5 seconds")
Thread.sleep(forTimeInterval: 5)
self?.setName(newName)
self?.setSurname(newSurname)
self?.printIt()
print("\(name) \(surname)")
otherRef.setName(newName)
otherRef.setSurname(newSurname)
otherRef.printIt()
print("Background Thread executed")
}
}
}

deinit {
print("Class A deinit")
}
}

Classe B

A classe B é ainda mais simples.
Possui apenas dois métodos:

  • execute: recebe uma closure como parâmetro e a executa
  • deinit: executado quando qualquer instância da classe B for removida da memória.
class ClassB {
func execute(completion: @escaping () -> Void) {
completion()
}

deinit {
print("Class B deinit")
}
}

Execução

A execução consiste de apenas 3 passos:

  1. Criação de duas instâncias optionals: classA e classB
  2. Chamada do método run da classeA, enviando a instância de B como parâmetro
  3. Atribuição de nil para as as variáveis classA e classB
var classA: ClassA? = ClassA()
var classB: ClassB? = ClassB()

classA?.run(classB!, newName: "John", newSurname: "Doe")
classA = nil
classB = nil

Entendendo o método run

A mágica do projeto CaptureListPlayground 🪄está aqui no método run. Então vamos analisá-lo com detalhes:

  • Ao chamar o método classB.execute, enviamos uma closure como parâmetro.
  • A lista de captura de variáveis dessa closure (capture list) é um array com três variáveis: weak self, name e surname
  • Todo o código da closure será enviado para execução em uma nova thread fora da Main Queue (comando DispatchQueue.global().async). Como é um envio assíncrono, o código do método classB.run não ficará bloqueado esperando a closure executar para seguir adiante.
  • Por motivos didáticos, a execução em si só começará depois de 5 segundos (Thread.sleep(forTimeInterval: 5)
let otherRef: ClassA = ClassA()
classB.execute { [weak self, name, surname] in
DispatchQueue.global().async {
print("Please wait 5 seconds")
Thread.sleep(forTimeInterval: 5)
self?.setName(newName)
self?.setSurname(newSurname)
self?.printIt()
print("\(name) \(surname)")
otherRef.setName(newName)
otherRef.setSurname(newSurname)
otherRef.printIt()
print("Background Thread executed")
}
}

Capture List da Closure

Vamos agora analisar com mais detalhes a capture list [weak self, name, surname].
Da direita para esquerda:

name e surname: Temos aqui uma "pegadinha". Isso porque repetimos os mesmos nomes dos atributos da classe A. Porém, ao serem capturados pela closure dessa forma explícita, name e surname viram variáveis totalmente diferentes de self.name e self.surname. A única coisa em comum, além claro do nome, é que name e surname levam os valores momentâneos de self.name e self.surname no momento da execução do método run de A.

weak self: Significa aqui que a closure irá capturar a instância da classe A, mas sem somar 1 na contagem de referências dela. Portanto, estamos sujeitos a que essa instância "suma" (vire nil) em algum momento da execução.

Saída da console

Ao executar o projeto, recebemos a saída abaixo na console:

Class A deinit
Class B deinit
Last line of main.swift 🎉
Please wait 5 seconds
Empty FirstName Empty LastName
John Doe
Background Thread executed
Class A deinit

Para entender tudo, precisamos levar em consideração que o código da closure só será executado depois de 5 segundos. Bem depois que a execução do código principal terminar.

Class A deinit: Quem imprime essa linha na console é o método deinit da classe A. E isso ocorre quando classA recebe nil no código principal. Nesse momento, a contagem de referências da instância class A zerou e class A é removida da memória. Isso significa que self dentro da closure é agora nil e nenhum dos 3 métodos que self deveria executar será chamado (self?.setName, self?.setSurname e self?.printIt)

Class B deinit: Quem imprime essa linha na console é o método deinit da classe B. Quando classB recebe nil, a contagem de referências da instância class B zera e class B é removida da memória.

Empty FirstName Empty LastName: É a saída do comando print(“\(name) \(surname)”) de dentro da closure. Lembrando que name e surname nesse contexto não são os atributos de self, mas sim uma cópia deles no momento em que classB.run foi executado.

John Doe: Ao contrário de self, otherSelf foi capturado implicitamente de forma forte (strong) e então os 3 métodos dele serão executados com sucesso: setName, setSurname e printIt. Lembrando que, ao contrário de self que já saiu da memória, otherSelf ficará preso na memória enquanto a closure e/ou as threads que a closure criar existirem.

Class A deinit: A última mensagem de deinit de A é causada pela remoção de otherSelf da memória depois que a closure (e sua thread) terminarem.

Mas e quando, afinal, devo usar [weak self] ?

O uso de weak self só é realmente necessário se todas as três condições existirem:

  1. A closure é escaping (ou seja, ela pode ser armazenada para ser executada só no futuro e também pode ser passada como parâmetro para outras closures).
  2. A closure é armazenada em uma variável ou é enviada como parâmetro para uma outra closure
  3. O objeto dentro da closure (ex: self) mantém uma referência forte com a mesma closure (ou para a closure para a qual ele é enviado)

No artigo You don't need to use [weak self] regularly encontramos um fluxograma bastante útil:

E para exemplificar ainda mais, deixo o código abaixo no qual as três condições acima são encontradas.

import Foundation

class ConditionsMet {

var name: String = "Empty Name"
var surname: String = "Empty Surname"

var myClosure: () -> Void = { }

init() {
myClosure = { // [weak self] in will fix the memory leak
DispatchQueue.global().async {
print("waiting 5 seconds")
Thread.sleep(forTimeInterval: 5)
print("\(self.name) \(self.surname)")
}
}
}

func run() {
myClosure()
}

deinit {
print("A deinit - \(name) \(surname)")
}
}

var conditionsMet: ConditionsMet? = ConditionsMet()
conditionsMet?.run()
conditionsMet = nil

print("Last line of main.swift 🎉")
RunLoop.main.run()

Conseguimos comprovar o vazamento de memória de duas formas:
1. deinit nunca é executado
2. pelo Leak profiler do Xcode:

print screen da tela do Leak profiler do XCode comprovando que uma instância da classe ConditionsMet está presa em memória

Conclusões

Sobre capture list:

  • Conhecer e usar de forma consciente a capture list nos dá maior controle sobre a execução da closure e do gerenciamento de memória do nosso app.
  • Variáveis do tipo "value type" (ex: structs, strings, ints) quando são adicionadas na capture list geram novas cópias independentes das mesmas. É como uma "foto independente" das variáveis originais tirada no momento em que a closure foi enviada como parâmetro.
  • Por outro lado, variáveis do tipo “reference type” (ex: classes) são capturadas implicitamente estando ou não na capture list. A exceção é com o uso de weak ou unowned. Weak e unowned previnem que a closure mantenha essa instância em memória e cause algum possível vazamento. Por outro lado, isso pode trazer algum comportamento indesejado já que quando a closure executar, a instância poderá já ter sido removida da memória.
  • Atenção: quando vc inclui na capture list uma variável optional ela será tratada sempre como um value type mesmo que guarde (em .some) uma variável “reference type”, por exemplo uma instância de classe. Ou seja, isso fará com que seja criada uma nova variável independente (seja uma cópia de um value type ou uma nova referência para um reference type).

Sobre o uso de [weak self] na capture list:

  • Não são todas as closures que necessitam de [weak self]. Apenas aquelas que obedecem a três critérios:
    1. São escaping
    2. São armazenadas em uma variável ou são enviadas como parâmetro para outra closure
    3. Possuem pelo menos um objeto com referência forte para a closure (ex: self quando a closure é uma variável da classe)

Referências

--

--

Fernando Luiz Goulart
Fernando Luiz Goulart

Written by Fernando Luiz Goulart

iOS developer. Brave enough to work with I am really passionated.

No responses yet