Назад к вопросам
Middle
93
questionbank

Что такое циклы удержания (retain cycles) в iOS?

Sobes Copilot

Получайте ответы в реальном времени

Ответ от нейросети

sobes.tech AI

Циклы удержания возникают, когда два или более объекта имеют сильные ссылки друг на друга, создавая "замкнутый круг". Вследствие этого, счетчик ссылок каждого объекта никогда не достигает нуля, и он не может быть освобожден ARC (Automatic Reference Counting). Это приводит к утечкам памяти, так как объекты остаются в памяти даже после того, как они перестают использоваться.

Пример:

class Person {
    let name: String
    var apartment: Apartment?

    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?

    init(unit: String) {
        self.unit = unit
        print("Apartment \(unit) is being initialized")
    }

    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

В этом примере, если Person имеет сильную ссылку на Apartment, а Apartment имеет сильную ссылку на Person, то при попытке освободить эти объекты они не будут удалены, так как их счетчики ссылок не достигнут нуля.

Как решить проблему циклов удержания:

Используйте слабые (weak) или бесхозные (unowned) ссылки.

  • weak: Используется, когда возможно, что связанный объект станет nil в течение жизненного цикла другого объекта. Ссылка становится nil автоматически, когда объект, на который она ссылается, освобождается. Является опционалом.
  • unowned: Используется, когда известно, что связанный объект имеет тот же или более длительный жизненный цикл, чем объект, имеющий на него ссылку. Не может быть nil.

Исправленный пример с использованием weak:

Изменим ссылку tenant в классе Apartment на слабую:

class Apartment {
    let unit: String
    weak var tenant: Person? // Используем weak

    init(unit: String) {
        self.unit = unit
        print("Apartment \(unit) is being initialized")
    }

    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

Теперь, когда Person объект освобождается, ссылка tenant в Apartment автоматически становится nil, разрывая цикл удержания и позволяя Apartment также быть освобожденным.

Таблица сравнения weak и unowned:

Характеристика weak unowned
Тип ссылки Опциональный Неопциональный
Может быть nil? Да, при освобождении объекта Нет
Когда использовать Когда объект может быть nil Когда объект не может быть nil
Производительность Немного медленнее из-за проверки на nil Слегка быстрее

Также циклы удержания могут возникать в замыканиях.

Когда замыкание захватывает instance properties или self, оно может создать сильную ссылку на объект. Если этот объект также имеет сильную ссылку на замыкание (например, через свойство-замыкание или делегата), возникает цикл удержания.

Как решить проблему циклов удержания в замыканиях:

Используйте список захвата (capture list).

class SomeClass {
    var value = 0

    func doSomethingDelayed() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in // Список захвата с weak self
            guard let self = self else { return } // Проверка на nil для weak self
            self.value += 1
            print("Value is now \(self.value)")
        }
    }
}

В данном примере [weak self] создает слабую ссылку на self внутри замыкания, предотвращая цикл удержания.