Ответ на один из 10 технических вопросов недавнего собеседования по Swift и Objective C был очевиден, но нуждался в точном объяснении.
Вот этот вопрос:
«Какие ссылки на объект применяются в Swift для предотвращения цикла сохранения:
a) сильные; б) слабые; в) бесхозные?»
Оптимальный ответ: б) слабые.
Но почему?
Циклы сохранения
Если вкратце, для управления памятью в Swift используется автоматический подсчет ссылок.
Когда объект инстанцируется, в памяти сохраняется дополнительная информация о нем для автоматических операций сохранения и освобождения, таких как подсчет ссылок — в коде это retainCount
, — чтобы сохранить объект со строгими ссылками на его дочерние объекты, пока он во избежание высвобождения памяти еще «помечен» строгой ссылкой как необходимый.
«А как насчет значений-типов вроде структур?»
В отличие от ссылочных типов, объектов, которыми с помощью этих retainCount отслеживается число ссылок на объект, значениям-типам такая стратегия не требуется. Каждый экземпляр значения-типа — это собственная, независимая часть данных, и область его существования привязана ко внешней его области.
Копии этих значений создаются их передачей как аргументов или сохранением в других переменных. Например, при передаче в Swift в качестве аргумента функции значения-типа, такого как структура или перечисление, создается копия этого значения и таким образом выделяется новый кусок памяти. Это называется семантикой копирования.
Возьмем приложение с двумя классами:
class Manager: EmployeeDelegate {
let employee: Employee
init(employee: Employee) {
self.employee = employee
self.employee.delegate = self
}
func perform() {/* код */}
...
}
protocol EmployeeDelegate: AnyObject {
func perform()
}
class Employee {
public var delegate: EmployeeDelegate?
...
}
В Swift это типичный шаблон с классом, который «знает», когда выполнять операцию, но неизвестно какую. В этом случае шаблоном делегирования с protocol
гарантируется, что руководитель Manager определяет операцию и дает указания работнику Employee, который в нужный момент их выполняет.
Сильные ссылки
Проблема в том, что в этом примере очень четко генерируется цикл сохранения. Почему? Когда создается экземпляр Manager
, в него передается Employee
, который сохраняется, и создается сильная ссылка, так что память от объекта Employee не освобождается, пока существует его Manager.
Чтобы определить операции JobDelegate, для Employee нужен делегат. Здесь в методе инициализатора в качестве делегата задан объект Manager, так что в Employee тоже имеется сильная ссылка на Manager и память от экземпляра Manager не освобождается, пока существует его Employee.
То есть они не дадут друг другу освободить память в течение неопределенного времени:
Слабые и бесхозные ссылки
Теперь главный вопрос: почему для предотвращения цикла сохранения применяется слабая weak
ссылка, а не бесхозная unowned
, ведь обе они нейтральны по своему влиянию на retainCount
?
//Рассмотрим два класса:
class Manager: JobDelegate {
func operate() {
print("Operation executed.")
}
}
protocol JobDelegate: AnyObject { func operate() }
class Employee {
(unowned/weak) var delegate: JobDelegate?
//Этот модификатор будет единственной переменной для обоих запусков.
public func performOperation() {
delegate?.operate()
}
}
//ПРИМЕР
var employee = Employee()
var manager: Manager? = Manager() //выделяется память
employee.delegate = manager //в качестве делегата задается «Manager»
manager = nil //память освобождается
employee.performOperation()
unowned
применяется, когда «сильное» владение в ссылке не требуется, но в большинстве сценариев unowned не рекомендуется. Поскольку он не может быть Optional, всегда предполагается, что в нем содержится объект, пока используется ссылка.
Для этого нужна уверенность в том, что объект, на который ссылаются, «переживет» ссылку или обоими память высвободится одновременно. Доступ к ссылке unowned
после освобождения памяти чреват аварийным завершением работы:
/*
unowned var delegate: JobDelegate?
Обозначение здесь «delegate» как «Optional» никак не сказывается. Попытка получить доступ к «unowned» с «Optional», когда он освобожден, по-прежнему чревата аварийным завершением работы приложения.
Для использования «unowned» в этом случае потребовалось бы полное понимание жизненных циклов этих объектов и соответственных рисков.
*/
employee.performOperation() //Работа приложения аварийно завершилась.
/*
Лог:
ошибка: выполнение прервано, причина: сигнал «SIGABRT».
*/
С другой стороны, слабой ссылкой weak
не только избегается «сильное» владение в ссылке, она всегда Optional и автоматически становится nil
при высвобождении объекта.
Поэтому хороша для сценариев, где объект, на который ссылаются, выходит из области видимости или освобождается независимо от ссылки:
/*
weak var delegate: JobDelegate?
Здесь «delegate» в обязательном порядке помечается компилятором как «Optional».
*/
employee.performOperation() //Функция выполнена.
/*
Ничего не регистрируется.
*/
Заключение
Учитывая, что об отношении конкретного владения и жизненного цикла экземпляра на основе данных вопроса просто нечего сказать, в силу изложенных причин предпочтительным ссылочным типом становится weak.
Читайте также:
- Реализация параллакс-карусели из SwiftUI в Jetpack Compose
- Swift: 7 секретов оптимизации
- Map, CompactMap и FlatMap в Swift
Читайте нас в Telegram, VK и Дзен
Перевод статьи Matheus Quirino Cardoso: Swift | Reference types and Retain cycles (Weak vs. Unowned) | Memory #1