- ARC (Automatic Reference Counting)
- Retain Cycles
- weak vs unowned
- Closures и Memory Management
- Практические примеры
- Method Dispatch
- Оптимизация производительности
- Q&A Карточки
ARC – автоматическое управление памятью в Swift. Он:
- Отслеживает количество ссылок на объект
- Увеличивает счётчик при создании новой ссылки
- Уменьшает счётчик при удалении ссылки
- Освобождает память, когда счётчик = 0
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) создан в памяти")
}
deinit {
print("\(name) удалён из памяти")
}
}
// Пример работы ARC
var person1: Person? = Person(name: "Alex") // Reference Count (RC) = 1
var person2 = person1 // RC = 2
var person3 = person1 // RC = 3
print("До удаления ссылок")
person1 = nil // RC = 2
person2 = nil // RC = 1
person3 = nil // RC = 0 → deinit вызывается автоматически
// Вывод:
// Alex создан в памяти
// До удаления ссылок
// Alex удалён из памяти// Reference Type (class) - ARC работает
class PersonClass {
var name: String
init(name: String) { self.name = name }
}
var p1 = PersonClass(name: "Alex")
var p2 = p1 // Обе переменные ссылаются на ОДИН объект
p2.name = "Bob"
print(p1.name) // "Bob" - изменился и p1!
// Value Type (struct) - ARC НЕ работает (копируется)
struct PersonStruct {
var name: String
}
var s1 = PersonStruct(name: "Alex")
var s2 = s1 // s2 - это КОПИЯ s1
s2.name = "Bob"
print(s1.name) // "Alex" - s1 не изменилсяВажно: ARC работает ТОЛЬКО для классов (reference types). Для struct, enum используется копирование (value semantics).
Retain Cycle (цикл сильных ссылок) – когда два объекта держат друг друга через strong references, что приводит к memory leak (утечке памяти).
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
print("\(name) создан")
}
deinit {
print("\(name) удалён")
}
}
class Apartment {
let number: Int
var tenant: Person? // ❌ Strong reference
init(number: Int) {
self.number = number
print("Квартира \(number) создана")
}
deinit {
print("Квартира \(number) удалена")
}
}
// Создаём retain cycle
var person: Person? = Person(name: "Alex")
var apartment: Apartment? = Apartment(number: 101)
person?.apartment = apartment // Person → Apartment (strong)
apartment?.tenant = person // Apartment → Person (strong)
// Пытаемся удалить
person = nil // ❌ Person НЕ удаляется (RC = 1, apartment держит)
apartment = nil // ❌ Apartment НЕ удаляется (RC = 1, person держит)
// MEMORY LEAK! Оба объекта остались в памяти навсегда
// Вывод:
// Alex создан
// Квартира 101 создана
// (deinit НЕ вызывается)┌──────────────┐ strong ┌────────────────┐
│ Person │────────────→│ Apartment │
│ (RC = 1) │ │ (RC = 1) │
└──────────────┘ ←───────────└────────────────┘
strong
Никто извне не держит ссылки, но объекты держат друг друга!
class Apartment {
let number: Int
weak var tenant: Person? // ✅ Weak reference (не увеличивает RC)
init(number: Int) {
self.number = number
print("Квартира \(number) создана")
}
deinit {
print("Квартира \(number) удалена")
}
}
// Теперь всё работает
var person: Person? = Person(name: "Alex")
var apartment: Apartment? = Apartment(number: 101)
person?.apartment = apartment // Person → Apartment (strong)
apartment?.tenant = person // Apartment → Person (weak) ✅
person = nil // ✅ Person удаляется (RC = 0)
// apartment.tenant автоматически становится nil
apartment = nil // ✅ Apartment удаляется
// Вывод:
// Alex создан
// Квартира 101 создана
// Alex удалён
// Квартира 101 удалена| Характеристика | weak |
unowned |
|---|---|---|
| Тип | Опциональный (var tenant: Person?) |
Не опциональный (var owner: Person) |
| Поведение при удалении | Автоматически становится nil |
НЕ становится nil → crash |
| Использование | Когда ссылка может стать nil |
Когда объект ВСЕГДА существует |
| Безопасность | ✅ Безопаснее | |
| Производительность | Немного медленнее (проверка на nil) |
Немного быстрее |
class Parent {
var name: String
var child: Child?
init(name: String) {
self.name = name
}
deinit {
print("\(name) удалён")
}
}
class Child {
var name: String
weak var parent: Parent? // ✅ Может быть nil (ребёнок может остаться без родителя)
init(name: String) {
self.name = name
}
deinit {
print("\(name) удалён")
}
}
var parent: Parent? = Parent(name: "Alice")
var child: Child? = Child(name: "Bob")
parent?.child = child
child?.parent = parent
print("Parent: \(child?.parent?.name ?? "nil")") // "Alice"
parent = nil // Parent удаляется
print("Parent после удаления: \(child?.parent?.name ?? "nil")") // "nil"
child = nil
// Вывод:
// Parent: Alice
// Alice удалён
// Parent после удаления: nil
// Bob удалёнclass Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
print("\(name) создан")
}
deinit {
print("\(name) удалён")
}
}
class CreditCard {
let number: String
unowned let customer: Customer // ✅ Карта ВСЕГДА принадлежит клиенту
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
print("Карта \(number) создана для \(customer.name)")
}
deinit {
print("Карта \(number) удалена")
}
}
var customer: Customer? = Customer(name: "Alex")
customer?.card = CreditCard(number: "1234-5678", customer: customer!)
print("Владелец карты: \(customer?.card?.customer.name ?? "nil")")
customer = nil // ✅ Оба объекта удаляются
// Вывод:
// Alex создан
// Карта 1234-5678 создана для Alex
// Владелец карты: Alex
// Alex удалён
// Карта 1234-5678 удаленаclass Customer {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) удалён") }
}
class CreditCard {
let number: String
unowned let customer: Customer
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
func printOwner() {
print("Владелец: \(customer.name)") // ⚠️ Может быть crash!
}
}
var customer: Customer? = Customer(name: "Alex")
let card = CreditCard(number: "1234", customer: customer!)
customer = nil // Customer удалён, но card.customer ещё существует!
card.printOwner() // 💥 CRASH! customer уже удалёнweak:
- Когда ссылка может стать
nilв течение жизни объекта - Delegates
- Closures (в большинстве случаев)
- Parent-child relationships где child может пережить parent
unowned:
- Когда объект ГАРАНТИРОВАННО существует на протяжении всей жизни ссылки
- Например: карта всегда принадлежит клиенту, и удаляется вместе с ним
- Когда хотите избежать optional unwrapping
class ViewController {
var name = "MainVC"
var closure: (() -> Void)?
func setupClosure() {
closure = {
print(self.name) // ❌ Strong reference на self
}
}
deinit {
print("\(name) удалён")
}
}
var vc: ViewController? = ViewController()
vc?.setupClosure()
vc = nil // ❌ НЕ удаляется! Memory leak
// Схема retain cycle:
// ViewController → closure (strong, хранится в свойстве)
// closure → self (strong, захватывается по умолчанию)class ViewController {
var name = "MainVC"
var closure: (() -> Void)?
func setupClosure() {
closure = { [weak self] in // ✅ Weak capture
guard let self = self else { return }
print(self.name)
}
}
deinit {
print("\(name) удалён")
}
}
var vc: ViewController? = ViewController()
vc?.setupClosure()
vc = nil // ✅ Удаляется
// Вывод:
// MainVC удалёнclass ViewController {
var name = "MainVC"
func setupAnimation() {
UIView.animate(withDuration: 1.0) { [unowned self] in
// ✅ OK: анимация гарантированно завершится до удаления VC
self.view.alpha = 0
}
}
}class Example {
var value = 10
func demo() {
let externalValue = 5
// 1. Strong capture (по умолчанию)
let closure1 = {
print(self.value) // Strong reference на self
}
// 2. Weak capture
let closure2 = { [weak self] in
print(self?.value ?? 0) // Weak reference, может быть nil
}
// 3. Unowned capture
let closure3 = { [unowned self] in
print(self.value) // Unowned reference
}
// 4. Захват конкретного значения (копия в момент создания)
let closure4 = { [value] in
print(value) // value = 10 (копия)
}
self.value = 20
closure4() // Выведет: 10 (не 20!)
// 5. Множественный захват
let closure5 = { [weak self, externalValue] in
guard let self = self else { return }
print(self.value + externalValue)
}
// 6. Захват с переименованием
let closure6 = { [weak viewController = self] in
print(viewController?.value ?? 0)
}
}
}// ✅ Не нужен: closure не хранится
func fetchData(completion: @escaping (Data) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
completion(data!)
}.resume()
}
// ❌ Нужен: closure хранится в свойстве
class MyClass {
var onComplete: (() -> Void)?
func setup() {
onComplete = { [weak self] in // ✅ Правильно
self?.doSomething()
}
}
}class DataLoader {
var data: Data?
var onComplete: ((Data?) -> Void)?
func loadData() {
// ❌ Плохо: retain cycle
URLSession.shared.dataTask(with: URL(string: "https://example.com")!) { data, _, _ in
self.data = data // Strong reference на self
self.onComplete?(data)
}.resume()
}
func loadDataCorrect() {
// ✅ Хорошо: weak self
URLSession.shared.dataTask(with: URL(string: "https://example.com")!) { [weak self] data, _, _ in
self?.data = data
self?.onComplete?(data)
}.resume()
}
}Почему нужен [weak self]?
URLSession.sharedглобальный синглтон- Task может продолжаться после удаления
DataLoader - Без
[weak self]→DataLoaderникогда не удалится, пока task не завершится
class TimerViewController: UIViewController {
var timer: Timer?
var counter = 0
override func viewDidLoad() {
super.viewDidLoad()
setupTimer()
}
func setupTimer() {
// ❌ Плохо: retain cycle
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.counter += 1 // Strong reference
print(self.counter)
}
}
func setupTimerCorrect() {
// ✅ Хорошо: weak self
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.counter += 1
print(self.counter)
}
}
deinit {
timer?.invalidate() // ⚠️ ВАЖНО! Всегда invalidate в deinit
print("TimerVC удалён")
}
}Важно: Timer держит strong reference на target, поэтому:
- Всегда используй
[weak self] - Всегда вызывай
timer?.invalidate()вdeinit
// ✅ Правильный паттерн делегата
protocol DataLoaderDelegate: AnyObject { // ⚠️ ВАЖНО: AnyObject (только классы)
func didLoadData(_ data: Data)
func didFailWithError(_ error: Error)
}
class DataLoader {
weak var delegate: DataLoaderDelegate? // ✅ Weak
func load() {
URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
if let error = error {
self?.delegate?.didFailWithError(error)
} else if let data = data {
self?.delegate?.didLoadData(data)
}
}.resume()
}
}
class ViewController: UIViewController, DataLoaderDelegate {
let loader = DataLoader()
override func viewDidLoad() {
super.viewDidLoad()
loader.delegate = self // ✅ Нет retain cycle
loader.load()
}
func didLoadData(_ data: Data) {
print("Данные загружены: \(data.count) bytes")
}
func didFailWithError(_ error: Error) {
print("Ошибка: \(error)")
}
deinit {
print("ViewController удалён")
}
}Схема без retain cycle:
ViewController → DataLoader (strong)
DataLoader → delegate (weak) → ViewController
Когда ViewController удаляется → delegate автоматически nil
class MyViewController: UIViewController {
var observer: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
// ❌ Плохо: retain cycle
observer = NotificationCenter.default.addObserver(
forName: .didReceiveData,
object: nil,
queue: .main
) { _ in
self.handleNotification() // Strong reference
}
}
func setupCorrect() {
// ✅ Хорошо: weak self
observer = NotificationCenter.default.addObserver(
forName: .didReceiveData,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleNotification()
}
}
deinit {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
print("MyViewController удалён")
}
}class DataProcessor {
var data: [String] = []
func processData() {
// ✅ Обычно weak self НЕ нужен для простых операций
DispatchQueue.global().async {
let result = self.heavyComputation()
DispatchQueue.main.async { [weak self] in // ✅ Но лучше добавить
self?.data = result
}
}
}
func heavyComputation() -> [String] {
// Долгая операция
return ["result"]
}
}Method Dispatch – механизм, который определяет, какой именно метод будет вызван во время выполнения программы.
- Компилятор знает, какой метод вызвать во время компиляции
- Самый быстрый (может inline optimization)
- Используется для:
struct,enum,final class,staticметоды, методы в extensions
struct Point {
var x: Int
var y: Int
func distance() -> Double { // ⚡️ Static dispatch
return sqrt(Double(x * x + y * y))
}
}
let point = Point(x: 3, y: 4)
point.distance() // Компилятор точно знает, какой метод вызватьПреимущества:
- ⚡️ Самая высокая производительность
- Компилятор может inline код (вставить прямо на место вызова)
- Оптимизация на этапе компиляции
- Метод определяется во время выполнения (runtime)
- Используется Virtual Method Table (vtable)
- Немного медленнее (нужен поиск в таблице)
- Используется для:
classметоды (по умолчанию)
class Animal {
func makeSound() { // ⚡️⚡️ Dynamic dispatch (vtable)
print("Some sound")
}
}
class Dog: Animal {
override func makeSound() {
print("Woof")
}
}
class Cat: Animal {
override func makeSound() {
print("Meow")
}
}
let animal: Animal = Dog()
animal.makeSound() // "Woof" - определяется в runtime через vtableVirtual Method Table (vtable):
Animal vtable:
[0] makeSound → Animal.makeSound()
Dog vtable:
[0] makeSound → Dog.makeSound() // Перезаписан
[1] bark → Dog.bark() // Новый метод
Runtime:
animal.makeSound() → Смотрим в vtable объекта → вызываем Dog.makeSound()
- Самый медленный
- Используется в Objective-C runtime
- В Swift: методы с
@objc dynamic - Позволяет method swizzling, KVO
class MyClass: NSObject {
@objc dynamic func method() { // ⚡️ Message dispatch
print("Hello")
}
}
// Можно использовать Objective-C features
let selector = #selector(MyClass.method)| Тип | Скорость | Используется | Возможности |
|---|---|---|---|
| Static | ⚡️⚡️⚡️ (самый быстрый) | struct, enum, final class |
Inline, compile-time optimization |
| Dynamic (vtable) | ⚡️⚡️ (быстрый) | class методы |
Полиморфизм, наследование |
| Message (Obj-C) | ⚡️ (медленный) | @objc dynamic |
KVO, method swizzling, динамическое добавление методов |
// ❌ Медленно: dynamic dispatch
class Vehicle {
func move() {
print("Moving")
}
}
// ✅ Быстро: static dispatch (весь класс)
final class Car {
func move() {
print("Driving")
}
}
// ✅ Быстро: static dispatch (конкретный метод)
class Bike {
final func move() { // Этот метод не может быть переопределён
print("Cycling")
}
func stop() { // Dynamic dispatch (можно переопределить)
print("Stopping")
}
}protocol Drawable {
func draw()
}
struct Circle: Drawable {
func draw() {
print("Drawing circle")
}
}
struct Square: Drawable {
func draw() {
print("Drawing square")
}
}
// Protocol Witness Table (PWT)
let shapes: [Drawable] = [Circle(), Square()]
for shape in shapes {
shape.draw() // Использует PWT для поиска метода
}Protocol Witness Table:
- Похоже на vtable, но для протоколов
- Каждый тип имеет свою таблицу соответствия протоколу
class Animal {
func eat() {
print("Eating")
}
}
extension Animal {
func sleep() { // ⚡️⚡️⚡️ Static dispatch!
print("Sleeping")
}
}
class Dog: Animal {
override func eat() {
print("Dog eating")
}
// ❌ НЕЛЬЗЯ override метод из extension!
// override func sleep() { }
}
let dog: Animal = Dog()
dog.eat() // "Dog eating" (dynamic dispatch)
dog.sleep() // "Sleeping" (static dispatch, вызывается Animal.sleep)Важно: Методы в extensions НЕ могут быть переопределены → всегда static dispatch.
// ❌ Медленно
class SlowCalculator {
func calculate() -> Int {
return 42
}
}
// ✅ Быстро
final class FastCalculator {
func calculate() -> Int {
return 42
}
}// ✅ Быстро (static dispatch, stack allocation)
struct Point {
var x: Int
var y: Int
}
// ❌ Медленнее (dynamic dispatch, heap allocation)
class PointClass {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}// ❌ Медленно
class SlowClass: NSObject {
@objc dynamic func method() { }
}
// ✅ Быстро
class FastClass {
func method() { }
}В Build Settings:
Optimization Level = -O
Whole Module Optimization = Yes
Компилятор может применять более агрессивные оптимизации (например, автоматически делать internal методы final).
Q1: Что такое ARC?
A: Automatic Reference Counting – автоматическое управление памятью. Отслеживает количество ссылок на объект и освобождает память, когда счётчик становится 0.
Q2: Что такое retain cycle и как он возникает?
A: Цикл сильных ссылок, когда два объекта держат друг друга через strong references. Оба объекта имеют RC ≥ 1 и никогда не удаляются → memory leak.
Q3: В чём разница между weak и unowned?
A:
weak– опциональный, автоматически становитсяnilпри удалении объектаunowned– не опциональный, crash если объект удалён
Используйweakкогда ссылка может статьnil,unownedкогда объект всегда существует.
Q4: Когда нужен [weak self] в closures?
A: Когда closure хранится в свойстве объекта или может пережить объект. Предотвращает retain cycle: объект → closure → self.
Q5: Почему делегаты обычно weak?
A: Чтобы избежать retain cycle. Владелец держит strong ссылку на объект, объект держит weak ссылку на делегата (владельца).
Q6: Что будет, если забыть invalidate() для Timer?
A: Memory leak! Timer (в RunLoop) держит strong reference на target, поэтому объект не удалится, пока timer не будет invalidate.
Q7: Для каких типов работает ARC?
A: Только для reference types (class). Для value types (struct, enum) используется копирование, ARC не нужен.
Q8: Что такое deinit?
A: Деинициализатор – метод, который вызывается автоматически перед удалением объекта из памяти. Используется для очистки ресурсов (invalidate timers, remove observers).
Q9: Что такое method dispatch?
A: Механизм определения, какой метод будет вызван во время выполнения программы.
Q10: Какие типы dispatch существуют?
A:
- Static (самый быстрый) – компилятор знает метод заранее
- Dynamic (vtable) – поиск метода в runtime
- Message (Obj-C) – самый медленный, через Objective-C runtime
Q11: Когда используется static dispatch?
A: Для struct, enum, final class, static методов, методов в extensions (не переопределяемых).
Q12: Как оптимизировать производительность с final?
A: Пометить класс или метод как final – компилятор использует static dispatch вместо dynamic (нельзя переопределить).
Q13: Что такое vtable?
A: Virtual Method Table – таблица виртуальных методов класса. Используется для dynamic dispatch: runtime смотрит в vtable объекта и вызывает нужный метод.
Q14: Почему методы в extensions не могут быть переопределены?
A: Методы в extensions всегда используют static dispatch (не добавляются в vtable), поэтому нельзя override.
Q15: Зачем нужен @objc dynamic?
A: Для использования Objective-C runtime features: KVO, method swizzling, dynamic method resolution. Но это самый медленный тип dispatch.
- Понимаю как работает ARC (reference counting)
- Знаю что такое retain cycle и как его избежать
- Понимаю разницу между
weakиunowned - Знаю когда использовать
[weak self]в closures - Понимаю почему делегаты обычно
weak - Знаю про проблемы с Timer и NSNotificationCenter
- Понимаю разницу между reference types и value types
- Знаю 3 типа dispatch (static, dynamic, message)
- Понимаю как работает vtable
- Знаю когда используется static vs dynamic dispatch
- Понимаю зачем нужен
final - Знаю про особенности методов в extensions
- Понимаю Protocol Witness Table (PWT)
- ARC работает только для классов (reference types)
- Retain cycle = два объекта держат друг друга → memory leak
weak– безопаснее (становитсяnil), используй по умолчаниюunowned– быстрее, но опасно (crash если объект удалён)[weak self]в closures когда closure хранится или может пережить объект- Delegates всегда
weak+ протокол: AnyObject
- Static (⚡️⚡️⚡️) –
struct,enum,final class - Dynamic (⚡️⚡️) –
classметоды (vtable) - Message (⚡️) –
@objc dynamic(Obj-C runtime) - Используй
finalдля оптимизации - Методы в extensions всегда static dispatch