Области тьмы: разбираем неочевидные моменты при использовании памяти в Swift

от автора

Привет, Хабр! Это Александр, iOS Developer из Clevertec. В статье хочу рассмотреть, казалось бы, набившую оскомину тему — управление памятью в Swift и системой подсчёта ссылок. Да, на Хабре уже есть пара довольно исчерпывающих статей. Но предлагаю копнуть с другой стороны и попытаться собрать недостающие детали пазла.

Тип для хранения адресов памяти

Вместе взглянем под капот на открытый репозиторий языка Swift, в частности, на файл RefCount.h (местами перевел на русский для удобства). Это часть исходного кода Swift, которая связана с подсчетом ссылок и управлением памятью. Рассмотрение файла поможет лучше понять, как Swift обрабатывает ссылки, особенно при работе с объектами, где применяется механизм ARC.

Предварительно рекомендую ознакомиться с замечательной статьей, она будет упоминаться далее по тексту.

Теперь к файлу. С самого начала встречаем объявление структуры:

typedef struct {    __swift_uintptr_t refCounts;  } InlineRefCountsPlaceholder;

Из названия понятно, эта структура — заглушка для компилятора Swift. Объект, представленный заглушкой, даёт информацию компилятору о занимаемом в памяти размере (size) и выравнивании (alighnment) объекта, содержащего непосредственно в себе внутренний счётчик ссылок. 

Внутренний — в том смысле, что он содержит именно счётчик, а не ссылку на другой объект, который сам содержит счётчик ссылок (например, side table). Из комментария “It provides size and alignment but cannot be manipulated safely there” становится ясно, что никакая работа с этим объектом не осуществляется компилятором на текущем уровне. 

Структура содержит единственное поле refCounts, тип данных которого  __swift_uintptr_t. Это тип данных, представляющий целое число без знака. Число имеет ту же разрядность, что и указатель в архитектуре, на которой исполняется программа. То есть — тип для хранения адресов памяти или других объектов с таким же размером, что и указатель. На современных 64-битных системах это будет 64-битное целое число, соответственно на 32-битных — 32-битное.

Что значит “концептуально имеет 3 счётчика ссылок”?

Дальше начинается самое интересное. Довольно большой блок комментариев, где подробно раскрывается номенклатура ссылок и их функционал, а также жизненный цикл объекта в Swift.

Блок комментариев встречает фразой, которая переводится примерно так: “Объект концептуально имеет 3 счётчика ссылок. Знать бы, что автор подразумевал под словом “концептуально”?

Эти счётчики хранятся или непосредственно внутри объекта в поле, следующем за isa, или в поле боковой таблицы, на которую указывает поле, следующее за isa”. Пояснять, что такое боковая таблица объекта в Swift, нет нужды, об этом есть тонны материалов в сети на разных языках и к тому же это один из самых популярных вопросов на собеседованиях на позицию разработчика на языке Swift.

Но хочу пояснить про загадочное слово “isa”. Благодаря статье о счётчиках ссылок узнал, что по сути это ссылка на метаданные объекта, описывающие структуру его класса. Говоря проще — просто ссылка на код класса, к которому принадлежит объект.

С какими значениями инициализируется счётчик ссылок?

Далее следует краткое описание каждого типа ссылок. Мы прекрасно знаем все эти типы ссылок, но в описаниях указана любопытная информация. Она касается начальных значений счётчиков ссылок. Давайте посмотрим.

Сильные ссылки: Счётчики сильных ссылок ведут подсчёт сильных ссылок на объект”. Несмотря на тавтологию, удачная формулировка, как по мне. Ничего необычного, смотрим дальше. “Когда счётчик сильных ссылок достигает нулевого значения, объект деинициализирован, unowned ссылки становятся (видимо, имеется в виду “указывают на”) ошибками, а обращение к слабым ссылкам возвращает nil”. На мой взгляд, не самая удачная формулировка, но тем не менее описание достаточно точное. Самая мякотка заключается в следующей фразе: “Счётчик сильных ссылок хранится как дополнительное значение: когда физическое поле равно 0, логическое значение равно 1.” И тут хочется пошутить…

В сети немного материалов по этому вопросу, вероятно, из-за того, что эти сведения практически неприменимы в повседневной практике разработчика. В самом деле, зачем ещё ко всему прочему задумываться, с какими значениями инициализируется счётчик ссылок? Внятного объяснения этому вопросу я не нашёл. Понятно, почему счётчик ссылок хранится как дополнительное значение — в самом деле, когда счетчик сильных ссылок на объект обнуляется, то объект необходимо уничтожить, но до этого момента сам счётчик требуется ещё хранить. Непонятно, почему это дополнительное значение связано с физическим значением поля 0. Как вы думаете?

Unowned ссылки

Счётчик unowned ссылок подсчитывает unowned ссылки на объект. Счётчик unowned ссылок имеет также дополнительное значение от сильных ссылок; эта +1 вычитается после завершения деинициализации объекта. Когда счётчик unowned ссылок достигает 0, высвобождается память, выделенная под объект.” Из этой формулировки становится понятно, что во-первых, счётчик unowned ссылок также имеет дополнительное значение +1 (от сильных ссылок, т. е. при создании объекта его счётчик unowned ссылок уже имеет значение +1). Во-вторых, высвобождение участка памяти, выделенной под объект, происходит при обнулении этого счётчика unowned ссылок. Я бы резюмировал так — счётчик unowned ссылок не только участвует в обеспечении механизма ARC, но и в высвобождении памяти, выделенной под объект.

Слабые ссылки

Я намеренно не буду уделять много внимания side table, материалов по ней достаточно в сети, но пару слов про слабые ссылки стоит сказать. Сначала про формулировку: “Счётчик слабых ссылок подсчитывает слабые ссылки на объект. Счётчик слабых ссылок также имеет дополнительное значение +1 от unowned ссылок; этот +1 декрементируется после того, как освобождена выделенная под объект память. Когда счётчик слабых ссылок обнуляется, память под side table высвобождается”. Резюме — счётчик слабых ссылок помимо участия в обеспечении механизма ARC, также участвует в высвобождении памяти, выделенной под side table объекта.

Мы взглянули на определение и краткое описание типов ссылок на объекты, с какими значениями они инициализируются (+1, +1, +1) и попробовали порассуждать, почему именно так. Далее описание side table пропустим. Нас ждёт описание жизненного цикла объекта, его рассматривать тоже не будем: вопрос уже довольно подробно разобран, в том числе в статьях про память в Swift, ссылки на которые я приводил. Но я бы хотел затронуть один нюанс.

Смотрим исходник: “Машина состояний жизненного цикла объекта.” Из этой короткой фразы становится понятно, что жизненный цикл объекта устроен по принципу машины состояний. И в этом кроется, скорее всего, ключевой ответ на вопрос с дополнительными +1 у счетчиков ссылок — такие значения нужны для корректной работы машины состояний. При нулевых значениях счетчиков машина состояний может активировать механизм перехода объекта в другое состояние, но объект при этом, например, ещё используется. 

Еще некоторые нюансы

enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true }; 

Это перечисление определяет место, в котором хранится счетчик ссылок: непосредственно в объекте или в side table.

enum PerformDeinit { DontPerformDeinit = false, DoPerformDeinit = true }; 

Функция _swift_release_dealloc(swift::HeapObject *object); через это перечисление гарантирует правильное высвобождение ресурсов перед удалением объекта.

Дальше идут шаблоны, которые описывают, как Swift организует хранение данных о количестве ссылок на объект в зависимости от архитектуры и расположения счётчиков (непосредственно в объекте или вовне). Далее блок кода, который описывает, как организованы биты внутри поля, отвечающего за подсчёт ссылок. Этот вопрос хорошо разобран здесь. Вот эти макросы создают маски и определяют, как эти маски накладывать на битовые поля и насколько их нужно сдвигать.

# define maskForField(name) (((uint64_t(1)<<name##BitCount)-1) << name##Shift) # define shiftAfterField(name) (name##Shift + name##BitCount) 

Остальные статические константы как раз и описывают биты в битовых полях счётчиков, их тоже отлично разобрали в приведенной выше статье.

Временно отключенный блок кода, описывающий различные static_assert, тоже не
будем рассматривать. Продолжим изучение. 

Механизм управления счётчиками ссылок

Класс RefCountBitsT. Он управляет битами счётчиков ссылок и флагами, описанными в статье, ссылка на которую приведена выше. Основная переменная bits в классе хранит биты счётчиков ссылок и флаги. Управление битами счётчиков ссылок и флагами в классе осуществляется с помощью соответствующих методов, а доступ к этим полям класс реализует с помощью макросов. Таким образом, класс RefCountBitsT реализует механизм управления счётчиками ссылок.

Дальше класс SideTableRefCountBits. Из названия ясно, что он аналогичен предыдущему, но только уже для объектов с side table и оперирует уже счётчиком слабых ссылок. Интересно, что метод hasSideTable(), в отличие от предыдущего класса, всегда возвращает false, что логично — у side table не может быть своей собственной side table.

Да, информация выше мало применяется разработчиками на языке Swift. Она не поможет стать более сильным программистом, но определенно расширяет кругозор и подталкивает к размышлениям [меня уж точно 🤣]. А что вам любопытно в области манипулирования языком Swift ссылками различных видов? Поделитесь мнением в комментариях.


ссылка на оригинал статьи https://habr.com/ru/articles/852718/