
Привет, Хабр! Меня зовут Егор, и это моя первая статья на этой платформе. Я занимаюсь iOS-разработкой, и за время работы я прочитал множество статей и документаций. Для того чтобы не теряться в этом потоке информации, я стал делать для себя короткие шпаргалки — они помогали закрепить изученное и готовиться к собеседованиям. В этой статье я решил собрать часть таких заметок в один материал, посвящённый работе с памятью в Swift. Надеюсь, он поможет кому-то освежить знания или узнать что-то новое.
Итак, чтобы не растягивать вступление, поехали!
Константы и переменные
Первое, с чего стоит начать, — это понятие констант и переменных в Swift.
let a = 10 // Константа
var b = 10 // Переменная
Константа — это неизменяемое значение. После объявления мы не можем изменить то, что в ней хранится.
Переменная — это изменяемое значение, её можно переопределять и присваивать новые значения.
Здесь есть важная особенность при работе со ссылочными типами. К ней мы вернёмся в разделе Value и Reference types.
let a = 10 // Константа
a = 20 // Ошибка: a не может быть изменена
var b = 10 // Переменная
b = 20 // Всё хорошо, теперь в b хранится 20
Value и Reference типы
В Swift, как и во многих других языках, существует семантика типов — разделение на два лагеря: типы значения (value types) и ссылочные типы (reference types).
Reference-типы — это объекты, которые хранятся в куче (heap). Например:
сlass A {}
let a = A() <------ здесь лежит ссылка, сам объект лежит в куче
В константе a
хранится не сам объект класса, а ссылка на него. Сам объект размещается в куче.
Куча (heap) — область памяти для объектов. Поиск в ней занимает больше времени, чем в стеке (о нём поговорим ниже). Зато в куче удобно хранить большие и долговременные объекты, что делает код более гибким.
К reference-типам относятся: классы, функции и акторы.
Противоположностью ссылочных типов являются value-типы. Их главное отличие — они хранятся в стеке. К value-типам относятся: все базовые типы Swift (Int
, Double
, Bool
и др.), enum
, struct
, коллекции, кортежи и строки.
На практике это выглядит так:
struct A {}
let a = A() // <------ тут нет ссылки, объект лежит тут (на стеке)
Объект размещается непосредственно в переменной, и при присвоении другой переменной он копируется полностью.
Стек — структура данных по принципу «последний вошёл, первый вышел». Вставка и чтение работают за константное время, что делает операции очень быстрыми.
Основное различие
Теперь рассмотрим ключевое отличие value и reference-типов:
class A {}
let a = A()
let b = a
Здесь a
и b
— ссылки на один объект. Изменение поля через b
изменит его и у a
:
class A {
var num = 10
}
let a = A()
let b = a
b.num = 20
print(a.num) // 20
С value-типами ситуация другая:
struct A {
var num = 10
}
let a = A()
var b = a
b.num = 20
print(a.num) // 10
Значение num
в a
остаётся прежним.
Почему так?
Reference-типы: константа
let
хранит ссылку, а не сам объект. Пока мы не меняем ссылку, поля объекта можно менять.Value-типы: переменная хранит сам объект. Если структура объявлена через
let
, менять её поля нельзя — это равносильно созданию нового объекта.
В Swift изменение любого поля у структуры фактически создаёт новый объект. Это важно учитывать при работе со структурами.
Исключения и нюансы:
Хотя в большинстве случаев value-типы хранятся в стеке, есть ситуации, когда они попадают в кучу:
Являются полем класса.
Находятся в листе захвата замыкания (closure capture).
Размер больше трёх машинных слов и находится в экзистенциальном контейнере (existential container).
Содержат ссылочные типы внутри структуры.
Дженерики могут храниться в куче в зависимости от контекста.
Экзистенциальный контейнер — механизм Swift для реализации полиморфизма через протоколы. Например, позволяет хранить в одном массиве разные типы, реализующие один протокол. Подробнее — тема отдельной статьи.
Ссылочные типы тоже могут храниться на стеке, если:
Размер фиксирован.
Жизненный цикл можно предсказать (например, объект класса объявлен внутри скоупа и не выходит за его пределы).
Важный момент про стеки и кучи: стеков столько, сколько потоков, поэтому value-типы потокобезопасны, а куча — одна на всю программу и, соответственно, классы не потокобезопасны.
Чем отличаются value-типы и reference-типы?
Value-типы (struct, enum, String, массивы, словари и др.) хранятся в стеке и копируются при присвоении.
Reference-типы (class, actor, функции) хранятся в куче, переменная хранит только ссылку на объект.
Copy-On-Write (COW)
А теперь давайте представим ситуацию. У нас есть очень тяжёлая структура, например большая коллекция, и мы многократно её копируем:
let a = Array<Int>() // Представим, что это очень большая коллекция
let b = a
let c = a
let d = a
// ...
Неужели каждый раз Swift будет копировать весь массив целиком? Копирование — операция тяжёлая, и при больших объёмах данных она потребляет много времени и ресурсов.
Ответ — нет, и в этом помогает механизм COW (Copy-On-Write). В приведённой ситуации Swift не создаёт новую копию массива при каждом присвоении. Вместо этого он присваивает каждой переменной ссылку на общий массив, а копирование произойдёт только при изменении содержимого.
Тут стоит остановиться на моменте про «ссылку на массив». Ведь массив — это value-тип, так почему же мы говорим о ссылке? Всё верно, массивы в Swift устроены хитро: под капотом они хранят ссылку на внутреннее хранилище данных. Глубже в детали мы погружаться не будем — это тема для отдельной статьи.
Важно помнить: Swift из коробки реализует COW только для коллекций, строк и типа Data
. А что насчёт других структур? Если у вас есть своя тяжёлая структура, её каждый раз копировать не обязательно. Swift позволяет реализовать собственный COW с помощью функции isKnownUniquelyReferenced
.
Вот пример:
final class Ref<T> {
var val: T
init(v: T) {
val = v
}
}
struct Box<T> {
var ref: Ref<T>
init(x: T) {
ref = Ref(v: x)
}
var value: T {
get { ref.val }
set {
if !isKnownUniquelyReferenced(&ref) {
ref = Ref(v: newValue)
} else {
ref.val = newValue
}
}
}
}
let a = Box(x: 1)
var b = a
print(a.value) // 1
print(b.value) // 1
b.value = 2
print(a.value) // 1
print(b.value) // 2
Почему это работает?
isKnownUniquelyReferenced
проверяет количество сильных ссылок на объект Ref
. В нашем примере a
и b
изначально ссылаются на один и тот же объект. Когда мы пытаемся изменить структуру через b
, Swift проверяет: если ссылок больше одной, создаётся новая копия (copy
), иначе можно изменять объект in-place.
В примере для простоты используется
Int
, но можно попробовать и с более сложными структурами, чтобы убедиться, что механизм работает так же.
Итак, всё, о чём мы говорили выше, — это базовые понятия. Дальше мы перейдём к более сложным вещам и углублённым аспектам работы с памятью.
Когда value-тип может попасть в кучу?
Если является полем класса.
Если захвачен замыканием.
Если хранится в экзистенциальном контейнере (например, через протокол).
Если внутри содержит ссылочные типы.
При работе с дженериками (в зависимости от контекста).
Жизненный цикл объекта в Swift
У каждого объекта в Swift есть свой жизненный цикл, который состоит из пяти стадий: Live, Deiniting, Deinited, Freed и Dead. Этот цикл описывает, что происходит с объектом при работе со strong, weak или unowned ссылками. Apple хорошо задокументировала этот процесс, а здесь я приведу схему и краткое описание каждой стадии.
Схема жизненного цикла объекта:
+------------+
| Live |
+------------+
|
| [no strong refs]
v
+------------+
| Deiniting |
+------------+
| |
| | [no weak refs, no unowned refs]
| v
| +------------+
| | Dead |
| +------------+
|
| [has unowned refs]
v
+------------+
| Deinited |
+------------+
| |
| | [no weak refs]
| v
| +------------+
| | Dead |
| +------------+
|
| [has weak refs and side table]
v
+------------+
| Freed |
+------------+
|
| [no side table]
v
+------------+
| Dead |
+------------+
Давайте пройдемся по стадиям по порядку:
1. Live — объект находится в этом состоянии, пока у него есть хотя бы одна сильная ссылка.
2. Deiniting — состояние, когда счётчик сильных ссылок достигает нуля. На этом этапе возможны два пути:
Переход в Dead, если нет ни слабых, ни бесхозных ссылок.
Переход в Deinited, если есть хотя бы одна weak или unowned ссылка.
3. Deinited — здесь у объекта нет сильных ссылок, но могут быть слабые или бесхозные ссылки.
Если слабых ссылок нет, объект переходит в Dead.
Если есть weak ссылки, возможен переход в Freed.
4. Freed — объект фактически уже удалён, но остаётся боковая таблица (*side table*), которая хранит информацию о слабых ссылках.
5. Dead — объект полностью уничтожен: нет сильных, слабых или unowned ссылок, нет боковой таблицы. В памяти остаётся только указатель, который может быть перезаписан или удалён системой.
Здесь мы затронули тему strong, weak, unowned ссылок и боковой таблицы (side table). Подробно об этом я не буду рассказывать в этой статье — это тема для отдельного материала про ARC.
Мы поговорили про объекты, кучи и стеки, но в Swift есть еще одна сущность, называется она статические переменные. Они не хранятся ни в куче, ни в стеке, так где же они хранятся?
Что такое Copy-On-Write (COW)? Как можно сделать свой COW?
Это механизм оптимизации, при котором массивы, строки и Data
копируются только при изменении. До изменения все переменные указывают на одно и то же хранилище данных.
У пользовательских структур его нет, но его можно сделать при помощи isKnownUniquelyReferenced
и обертки
Сегменты памяти
В процессе выполнения программы есть несколько основных сегментов памяти: Data Segment, Text Segment и BSS Segment.
Data Segment
Data Segment — это область памяти, где хранятся статические переменные на протяжении всей жизни программы.
Этапы формирования сегмента данных
1. Компиляция
При компиляции Swift-кода компилятор анализирует, какие данные можно сохранить в сегменте данных. Обычно это константы, глобальные и статические переменные.
2. Линковка (Linking)
На этом этапе данные из объектных файлов объединяются в единый исполняемый файл. Глобальные и статические данные Swift помещаются в сегмент данных.
3. Загрузка программы
При запуске программы операционная система выделяет память для различных сегментов, включая сегмент данных.
В Swift константы и переменные из Data Segment инициализируются на этапе компиляции (константы) или загрузки (переменные) программы.
Text Segment и BSS Segment
Может возникнуть вопрос: а что такое Text Segment и BSS Segment? Давайте разберёмся.
Text Segment — это область памяти, где хранятся машинные инструкции, скомпилированные из исходного кода программы.
* Методы и функции: весь исполняемый код программы (кроме данных) хранится здесь.
* Константы в коде: иногда константы, известные на этапе компиляции, тоже могут размещаться в Text Segment.
BSS Segment — это область памяти для неинициализированных глобальных и статических переменных, которым присваивается значение по умолчанию (например, 0
или nil
).
Пример:
static var uninitializedVar: Int // Размещается в сегменте BSS
Переменные в этом сегменте не занимают места в исполняемом файле до того, как программа запустится, а ОС выделяет для них память при загрузке.
Какие стадии жизненного цикла объекта есть в Swift?т
Live — у объекта есть хотя бы одна сильная ссылка.
Deiniting — strong = 0.
Deinited — нет сильных ссылок, но есть weak/unowned.
Freed — объект удалён, но side table (для weak-ссылок) ещё хранится.
Dead — объект полностью уничтожен.
Выводы
Итак, я попытался раскрыть самые важные, на мой взгляд, аспекты памяти в Swift: от базовых понятий let и var, через различие value и reference-типов, механизм Copy-On-Write, жизненный цикл объектов, и до более сложных тем, таких как сегменты памяти. Надеюсь, материал был полезен и помог освежить знания или узнать что-то новое. Само собой, это моя перввая статья, поэтому конструктивная критика приветсвуется в комментариях.
Где хранятся статические переменные?
Data Segment — глобальные и статические переменные.
Text Segment — машинные инструкции.
BSS Segment — неинициализированные глобальные/статические переменные.
Полезные ссылки
Контакты для связи
Rorg
Спасибо, интересно было прочитать. Единственное, не совсем понятно расположение сворачиваемых блоков. Такое впечатление, что некоторые из них съехали в другие разделы.
Например, блок "Где хранятся статические переменные?" находится в конце раздела "Выводы", хотя выше есть раздел "Сегменты памяти", где все это и описано. Блок "Что такое Copy-On-Write (COW)? Как можно сделать свой COW?" находится в разделе "Жизненный цикл объекта в Swift", хотя выше есть раздел посвященный COW.
Grandschtien Автор
Привет! Спасибо за комментарий)
На самом деле это было сделано специально. Я подчеркнул этот подход из книги "Как научиться учиться: Навыки осознанного усвоения знаний" Ульриха Бозера. Там книга строится на том, что вопросы по главе могут появляться не сразу после нее, а позже, например через главу. Это позволяет читателю возвращаться и искать информацию тем самым помогает усвоению материала