Управление памятью — большая тема в разработке Swift и iOS. Если есть много руководств, объясняющих, когда использовать слабое я с замыканием, вот короткая история, когда с ним все еще могут происходить утечки памяти.
Для целей этого поста предположим, что у нас есть следующий класс с двумя функциями. Каждая функция что-то выполняет и завершает выполнение выполнением замыкания.
ОБНОВЛЕНИЕ — 9 апреля 2022 г.: я пересмотрел примеры, чтобы выделить, когда увеличивается счетчик ссылок и когда это может вызвать утечку памяти.
class MyClass {
func doSomething(_ completion: (() -> Void)?) {
// do something
completion?()
}
func doSomethingElse(_ completion: (() -> Void)?) {
// do something else
completion?()
}
}
Теперь возникает новое требование: нам нужна новая функция doEverything, которая будет выполнять как doSomething, так и doSomethingElse в указанном порядке. Попутно мы меняем состояние класса, чтобы следить за прогрессом.
var didSomething: Bool = false
var didSomethingElse: Bool = false
func doEverything() {
self.doSomething {
self.didSomething = true // <- strong reference to self
print("did something")
self.doSomethingElse {
self.didSomethingElse = true // <- strong reference to self
print("did something else")
}
}
}
С самого начала мы видим, что self сильно захвачено в первом и втором замыканиях: замыкания сохраняют сильную ссылку на self, которая внутренне увеличивает счетчик ссылок и может предотвратить освобождение экземпляра во время выполнения doSomething.
Это означает, что если эти функции были асинхронными, и мы хотим освободить экземпляр до того, как он завершит выполнение, системе все равно придется ждать, чтобы завершить его, прежде чем освободить память.
Конечно, нам виднее, и мы настроили weak self для замыканий:
func doEverything() {
self.doSomething { [weak self] in
self?.didSomething = true
print("did something")
self?.doSomethingElse { [weak self] in
self?.didSomethingElse = true
print("did something else")
}
}
}
Подождите, нам действительно нужны оба [weak self] для каждого замыкания?
На самом деле, нет.
Когда у нас есть вложенные замыкания, как здесь, мы всегда должны устанавливать слабое я в первое, внешнее замыкание. Внутреннее замыкание, вложенное во внешнее, может повторно использовать одно и то же слабое «я».
func doEverything() {
self.doSomething { [weak self] in
self?.didSomething = true
print("did something")
self?.doSomethingElse { in
self?.didSomethingElse = true
print("did something else")
}
}
}
Однако, если бы мы поступили наоборот, имея слабое «я» только во вложенном замыкании, внешнее замыкание все равно сильно захватило бы «я» и увеличило счетчик ссылок. Так что будьте осторожны, когда вы устанавливаете это.
func doEverything() {
self.doSomething { in
self.didSomething = true // <- strong reference to self
print("did something")
self.doSomethingElse { [weak self] in
self?.didSomethingElse = true
print("did something else")
}
}
}
Все идет нормально.
Так как мы хотим изменить другие переменные по пути, давайте очистим код с помощью Guardlet, чтобы убедиться, что экземпляр все еще доступен.
func doEverything() {
self.doSomething { [weak self] in
guard let self = self else {
return
}
self.didSomething = true
print("did something")
self.doSomethingElse { in
self.didSomethingElse = true // <-- strong reference?
print("did something else")
}
}
}
Но теперь возникает вопрос: поскольку у нас есть сильная ссылка с именем self во внешнем замыкании, захватывает ли его сильно внутреннее замыкание? Как мы можем это проверить?
Это те вопросы, в которые стоит углубиться, и Xcode Playground идеально подходит для этого. Я включу несколько журналов, чтобы отслеживать шаги, а также регистрировать счетчик ссылок.
Давайте не будем усложнять первый пример, чтобы мы могли видеть, как счетчик ссылок увеличивается по пути.
class MyClass {
func doSomething(_ completion: (() -> Void)?) {
// do something
completion?()
}
func doSomethingElse(_ completion: (() -> Void)?) {
// do something else
completion?()
}
var didSomething: Bool = false
var didSomethingElse: Bool = false
deinit {
print("Deinit")
}
func printCounter() {
print(CFGetRetainCount(self))
}
func doEverything() {
print("start")
printCounter()
self.doSomething {
self.didSomething = true
print("did something")
self.printCounter()
self.doSomethingElse {
self.didSomethingElse = true
print("did something else")
self.printCounter()
}
}
printCounter()
}
}
do {
let model = MyClass()
model.doEverything()
}
Вот результат
# output
start
2
did something
4
did something else
6
2
Deinit
Только с сильными ссылками на себя мы можем видеть, как счетчик увеличивается до 6. Однако, как и ожидалось, как только обе функции выполняются, экземпляр освобождается.
Теперь давайте представим слабое «я» во внешнем замыкании.
func doEverything() {
print("start")
printCounter()
self.doSomething { [weak self] in
self?.didSomething = true
print("did something")
self?.printCounter()
self?.doSomethingElse {
self?.didSomethingElse = true
print("did something else")
self?.printCounter()
}
}
printCounter()
}
При первом слабом я экземпляр по-прежнему освобождается, и счетчик идет только до 4.
# output
start
2
did something
3
did something else
4
2
Deinit
Так что же происходит с guard let self?
func doEverything() {
print("start")
printCounter()
self.doSomething { [weak self] in
guard let self = self else { return }
self.didSomething = true
print("did something")
self.printCounter()
self.doSomethingElse {
self.didSomethingElse = true
print("did something else")
self.printCounter()
}
}
printCounter()
}
Вот результат
# output
start
2
did something
3
did something else
5
2
Deinit
Если экземпляр успешно деинициализирован, мы можем видеть, что счетчик на самом деле увеличивается с 4 до 5, когда мы выполняем doSomethingElse, что означает, что замыкание гнезда сильно захватывает наше временное «я».
Выглядит уже подозрительно, но давайте попробуем на другом примере. Что, если вместо функций doSomething и doSomethingElse являются замыкающими свойствами класса. Давайте адаптируем код для аналогичного исполнения.
class MyClass {
var doSomething: (() -> Void)?
var doSomethingElse: (() -> Void)?
var didSomething: Bool = false
var didSomethingElse: Bool = false
deinit {
print("Deinit")
}
func printCounter() {
print(CFGetRetainCount(self))
}
func doEverything() {
print("start")
printCounter()
doSomething = { [weak self] in
guard let self = self else { return }
self.didSomething = true
print("did something")
self.printCounter()
self.doSomethingElse = {
self.didSomethingElse = true
print("did something else")
self.printCounter()
}
self.doSomethingElse?()
}
doSomething?()
printCounter()
}
}
do {
let model = MyClass()
model.doEverything()
}
Вот результат
# output
start
2
did something
3
did something else
5
3
На этот раз класс даже не освобождается 🤯. Чтобы исправить это, мы должны сохранить слабый экземпляр.
func doEverything() {
print("start")
printCounter()
doSomething = { [weak self] in
self?.didSomething = true
print("did something")
self?.printCounter()
self?.doSomethingElse = {
self?.didSomethingElse = true
print("did something else")
self?.printCounter()
}
self?.doSomethingElse?()
}
doSomething?()
printCounter()
}
Вот результат
# output
start
2
did something
3
did something else
3
2
Deinit
Ура, на этот раз экземпляр правильно деалоцирован. Подтверждено, что внутренняя застежка делала сильную ссылку на защиту себя.
Итак, что это значит для моего кода?
Когда мы сталкиваемся с закрытием, мы склонны писать слабое я, за которым следует охранник, чтобы быстро обойти, не слишком задумываясь о выполнении дальше. Здесь нам все еще нужно быть осторожными. Такого рода утечки памяти легко пропустить, поэтому вот несколько выводов:
Во-первых, что касается формата, я лично использую в замыкании guard let strongSelf вместо guard let self. Причина в том, что во время просмотра кода может быть сложно понять, какое «я» мы имеем в виду дальше в коде.
Во-вторых, если есть вложенное замыкание, я бы предпочел сохранить ссылку на слабое (и необязательное) «я»? и никогда не указывать на strongSelf, поэтому у меня есть страховка, чтобы избежать любых сильных ссылок на него.
func doEverything() {
doSomething = { [weak self] in
guard let strongSelf = self else { return }
strongSelf.didSomething = true
print("did something")
strongSelf.doSomethingElse = {
self?.didSomethingElse = true
print("did something else")
}
strongSelf.doSomethingElse?()
}
doSomething?()
}
И последнее, но не менее важное: если у нас слишком много замыканий для дескрипторов, лучше всего провести их рефакторинг как отдельную функцию или использовать более новый API, чтобы избежать этих ошибок. Я думаю о функциональном реактивном программировании, таком как RxSwift или Combine, но также и о быстром Async.
Конечно, представленный сегодня код может быть немного надуманным и может не отражать ежедневное использование вами замыканий, но, на мой взгляд, все же важно помнить об управлении памятью и ссылках, которые мы делаем для наших экземпляров. Кроме того, этот вопрос попал прямо в середину экспертной оценки, поэтому мы никогда не были слишком осторожны;)
Надеюсь, вам понравился этот пост, удачного кодирования!
Вопрос? Обратная связь? Не стесняйтесь, пишите мне в Twitter.