Skip to content

Latest commit

 

History

History
1034 lines (796 loc) · 30.8 KB

11_Memory_Management_Method_Dispatch.md

File metadata and controls

1034 lines (796 loc) · 30.8 KB

Memory Management & Method Dispatch в Swift/iOS

📋 Содержание

  1. ARC (Automatic Reference Counting)
  2. Retain Cycles
  3. weak vs unowned
  4. Closures и Memory Management
  5. Практические примеры
  6. Method Dispatch
  7. Оптимизация производительности
  8. Q&A Карточки

ARC (Automatic Reference Counting)

Что это?

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 Types vs Value Types

// 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 Cycles

Что такое Retain Cycle?

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
                  
Никто извне не держит ссылки, но объекты держат друг друга!

Решение: weak

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 vs unowned

Сравнение

Характеристика weak unowned
Тип Опциональный (var tenant: Person?) Не опциональный (var owner: Person)
Поведение при удалении Автоматически становится nil НЕ становится nil → crash
Использование Когда ссылка может стать nil Когда объект ВСЕГДА существует
Безопасность ✅ Безопаснее ⚠️ Может привести к crash
Производительность Немного медленнее (проверка на nil) Немного быстрее

Пример: weak

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 удалён

Пример: unowned

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 удалена

⚠️ Опасность unowned

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

Closures и Memory Management

Проблема: Closure Capture

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, захватывается по умолчанию)

Решение 1: [weak self]

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 удалён

Решение 2: [unowned self]

class ViewController {
    var name = "MainVC"
    
    func setupAnimation() {
        UIView.animate(withDuration: 1.0) { [unowned self] in
            // ✅ OK: анимация гарантированно завершится до удаления VC
            self.view.alpha = 0
        }
    }
}

Capture Lists: Полный синтаксис

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)
        }
    }
}

Когда НЕ нужен [weak self]?

// ✅ Не нужен: 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()
        }
    }
}

Практические примеры

Пример 1: URLSession

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 не завершится

Пример 2: Timer

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, поэтому:

  1. Всегда используй [weak self]
  2. Всегда вызывай timer?.invalidate() в deinit

Пример 3: Delegates

// ✅ Правильный паттерн делегата
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

Пример 4: NotificationCenter

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 удалён")
    }
}

Пример 5: DispatchQueue

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

Что это?

Method Dispatch – механизм, который определяет, какой именно метод будет вызван во время выполнения программы.

Типы диспетчеризации

1. Static 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 код (вставить прямо на место вызова)
  • Оптимизация на этапе компиляции

2. Dynamic Dispatch (Динамическая через vtable)

  • Метод определяется во время выполнения (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 через vtable

Virtual 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()

3. Message Dispatch (Objective-C runtime)

  • Самый медленный
  • Используется в 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, динамическое добавление методов

Оптимизация: final

// ❌ Медленно: 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 Dispatch

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, но для протоколов
  • Каждый тип имеет свою таблицу соответствия протоколу

Extension Methods

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.


Оптимизация производительности

1. Используй final для классов

// ❌ Медленно
class SlowCalculator {
    func calculate() -> Int {
        return 42
    }
}

// ✅ Быстро
final class FastCalculator {
    func calculate() -> Int {
        return 42
    }
}

2. Предпочитай struct вместо class

// ✅ Быстро (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
    }
}

3. Избегай @objc dynamic если не нужен

// ❌ Медленно
class SlowClass: NSObject {
    @objc dynamic func method() { }
}

// ✅ Быстро
class FastClass {
    func method() { }
}

4. Whole Module Optimization (WMO)

В Build Settings:

Optimization Level = -O
Whole Module Optimization = Yes

Компилятор может применять более агрессивные оптимизации (например, автоматически делать internal методы final).


Q&A Карточки

Memory Management

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).

Method Dispatch

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.


Checklist для собеседования

Memory Management

  • Понимаю как работает ARC (reference counting)
  • Знаю что такое retain cycle и как его избежать
  • Понимаю разницу между weak и unowned
  • Знаю когда использовать [weak self] в closures
  • Понимаю почему делегаты обычно weak
  • Знаю про проблемы с Timer и NSNotificationCenter
  • Понимаю разницу между reference types и value types

Method Dispatch

  • Знаю 3 типа dispatch (static, dynamic, message)
  • Понимаю как работает vtable
  • Знаю когда используется static vs dynamic dispatch
  • Понимаю зачем нужен final
  • Знаю про особенности методов в extensions
  • Понимаю Protocol Witness Table (PWT)

Резюме

Memory Management

  1. ARC работает только для классов (reference types)
  2. Retain cycle = два объекта держат друг друга → memory leak
  3. weak – безопаснее (становится nil), используй по умолчанию
  4. unowned – быстрее, но опасно (crash если объект удалён)
  5. [weak self] в closures когда closure хранится или может пережить объект
  6. Delegates всегда weak + протокол : AnyObject

Method Dispatch

  1. Static (⚡️⚡️⚡️) – struct, enum, final class
  2. Dynamic (⚡️⚡️) – class методы (vtable)
  3. Message (⚡️) – @objc dynamic (Obj-C runtime)
  4. Используй final для оптимизации
  5. Методы в extensions всегда static dispatch