Привет, Хабр! Меня зовут Егор, и это моя первая статья на этой платформе. Я занимаюсь 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-типы хранятся в стеке, есть ситуации, когда они попадают в кучу:

  1. Являются полем класса.

  2. Находятся в листе захвата замыкания (closure capture).

  3. Размер больше трёх машинных слов и находится в экзистенциальном контейнере (existential container).

  4. Содержат ссылочные типы внутри структуры.

  5. Дженерики могут храниться в куче в зависимости от контекста.

Экзистенциальный контейнер — механизм Swift для реализации полиморфизма через протоколы. Например, позволяет хранить в одном массиве разные типы, реализующие один протокол. Подробнее — тема отдельной статьи.

Ссылочные типы тоже могут храниться на стеке, если:

  1. Размер фиксирован.

  2. Жизненный цикл можно предсказать (например, объект класса объявлен внутри скоупа и не выходит за его пределы).

Важный момент про стеки и кучи: стеков столько, сколько потоков, поэтому 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 хорошо задокументировала этот процесс, а здесь я приведу схему и краткое описание каждой стадии.

Ссылка на документацию от 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 — неинициализированные глобальные/статические переменные.

Полезные ссылки

Контакты для связи

Комментарии (3)


  1. Rorg
    31.08.2025 10:16

    Спасибо, интересно было прочитать. Единственное, не совсем понятно расположение сворачиваемых блоков. Такое впечатление, что некоторые из них съехали в другие разделы.

    Например, блок "Где хранятся статические переменные?" находится в конце раздела "Выводы", хотя выше есть раздел "Сегменты памяти", где все это и описано. Блок "Что такое Copy-On-Write (COW)? Как можно сделать свой COW?" находится в разделе "Жизненный цикл объекта в Swift", хотя выше есть раздел посвященный COW.


    1. Grandschtien Автор
      31.08.2025 10:16

      Привет! Спасибо за комментарий)

      На самом деле это было сделано специально. Я подчеркнул этот подход из книги "Как научиться учиться: Навыки осознанного усвоения знаний" Ульриха Бозера. Там книга строится на том, что вопросы по главе могут появляться не сразу после нее, а позже, например через главу. Это позволяет читателю возвращаться и искать информацию тем самым помогает усвоению материала