Для хранения объектов Swift использует две структуры данных: стек и кучу. Управление распределением памяти подразумевает выделение памяти под объект (аллокацию) и ее последующее высвобождение (деаллокацию).

В iOS существуют две модели управления памятью:

  1. MRC: manual reference counting (ручной подсчет ссылок)

  2. ARC: automatic reference counting (автоматический подсчет ссылок)

Что такое MRC?

Изначально мы использовали не-ARC подход (т.е. MRC), в рамках которого мы были вынуждены сохранять и высвобождать объекты вручную.

В этой модели мы полагались на ключевые слова retain и release для сохранения объекта в памяти и его последующего высвобождения.

Что такое ARC?

Теперь же в Swift используется ARC подход, который выделяет и высвобождения память автоматически. В рамках этого метода вам не нужно использовать release и retain.

Основная концепция ARC очень проста: когда объект сохраняется в памяти, то счетчик ссылок инкрементируется, а высвобождается, когда происходит декремент этого счетчика.

В swift с ARC мы в основном используем strong (сильные), weak (слабые) и unowned ссылки.

Если счетчик ссылок станет равным нулю, то объект будет удален из памяти.

Что из себя представляет жизненный цикл объекта?

  1. Выделение памяти (аллокация): берет память из стека или кучи.

  2. Инициализация: выполняется init-код 

  3. Эксплуатация: объект используется

  4. Деинициализация: выполняется deinit-код

  5. Высвобождение памяти (деаллокация): память возвращается стеку или куче обратно.

Какие могут возникать проблемы в рамках управления памятью?

  1. Освобождение или перезапись данных, когда объект еще используется. Это вызывает сбой или повреждение данных.

  2. Объект не высвобожден, когда он уже не используется. Это приводит к утечке памяти.

  3. Сбои в приложении.

Что такое утечка памяти (memory leak)?

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

Утечки памяти в приложении приводят к снижению производительности системы из-за увеличения объема потребляемой памяти.

Каковы основные правила управления памятью?

  1. Когда вы создаете/получаете объект, не забывайте высвобождать память впоследствии, когда он больше не используется.

  2. Задействуйте счетчик ссылок (retain count) в процессе сохранения и высвобождения объекта в памяти

  3. Не высвобождайте объект, если вы не владеете им.

Как объект сохраняется в памяти, и как она высвобождается?

Сильные ссылки увеличивают счетчик ссылок. Когда счетчик ссылок равен единице или больше, это означает, что под объект выделена память. Когда счетчик ссылок достигает нуля, то этот объект будет высвобожден.

Рассмотрим приведенный выше пример. Мы создали два класса: Person (человек) и Department (отдел). Объект Person создается и присваивается переменной p1 а объект Department создается и присваивается переменной department соответственно.

Когда объект person высвобождается из памяти, автоматически высвобождается и его подобъект. Т.е. счетчик ссылок department уменьшится до 1, когда объект person будет высвобожден.

Когда объект Person высвобождается из памяти, вызывается его метод deinit, но метод deinit объекта Department не вызывается, поскольку его счетчик ссылок не равен нулю. После того, как department присваивается значение nil, его счетчк становится равным нулю и он высвобождается из памяти.

В приведенном выше примере мы заставили person хранить ссылку на свой department, а также department ссылаться на своего person.

Когда объекту person присваивается значение nil, его счетчик ссылок становится равным единице, а когда объекту department присваивается nil, то его счетчик ссылок становится равным единице.

В этой ситуации оба объекта сохраняют ненулевой счетчик ссылок, так как оба объекта ссылаются друг на друга, что делает невозможным их деаллокацию. Это приводит к утечке памяти. Такая ситуация называется циклом сохранения (retain cycle).

Когда контроллер представления удаляется, все объекты контроллера представления будут высвобождены из памяти. Все подчиненные объекты любого из этих объектов будут высвобождены в свою очередь, когда те будут высвобождены.

Примечание: если другие классы имеют сильную ссылку на объект класса, то весь класс не будет высвобожден из памяти. Таким образом, рекомендуется использовать слабые или unowned свойства.

Что такое цикл сохранения (retain cycle)?

Цикл сохранения возникает, когда два объекта ссылаются друг на друга, что делает невозможным их высвобождение из памяти, потому что оба их счетчика ссылок (retain count) всегда будут равны единице или больше.

Как исправить цикл сохранения?

Сделайте одну из ссылок weak или unowned.

Слабая ссылка: не увеличивает счетчик ссылок. Слабые ссылки всегда объявляются как необязательные (optional) типы. Когда счетчик ссылок становится равным нулю, объект автоматически будет деаллоцирован.

Unowned ссылки: тут точно так же, как и со слабыми ссылками. Она не увеличивает счетчик ссылок. Основное отличие в том, что это не необязательный тип. Если вы попытаетесь получить доступ к unowned свойству, которое ссылается на деинициализированный объект, вы получите ошибку времени выполнения, сравнимую с принудительной распаковкой необязательного типа с nil.

В приведенном выше примере вы можете разрешить цикл сохранения, изменив сильной тип ссылки на слабый или unowned в классе department.

Цикл сохранения сильных ссылок в замыканиях:

Когда вы используете замыкания (closures) внутри экземпляра класса, они потенциально могут захватить self. Если self, в свою очередь, сохраняет это замыкание, у вас будет взаимный цикл сохранения сильных ссылок между замыканием и экземпляром класса.

Чтобы избежать этого, вы должны использовать те же самые ключевые слова weak и unowned в списке захвата (capture list) замыкания.

Чтобы узнать больше о списке захвата и замыканиях, переходите сюда

Как определять утечки памяти?

Xcode имеет встроенный отладчик графа памяти. Он позволяет вам увидеть, сколько ссылок имеется на объект и какие объекты существуют в настоящее время.

Кучи и стеки

Swift автоматически выделяет память либо в куче, либо в стеке.

Стек:

  • Статическое выделение памяти, которое происходит только во время компиляции.

  • Стек имеет структуру данных LIFO (последний вошел, первый вышел).

  • Очень быстрый доступ.

  • Когда функция вызывается, все локальные экземпляры этой функции будут помещены в текущий стек. И как только функция совершит возврат, все экземпляры будут удалены из стека.

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

  • Каждая “область видимости” (scope) в вашем приложении (например, внутреннее содержимое метода) предоставит необходимый объем памяти.

  • Стек не используется с объектами, которые имеют переменный размер.

  • Каждый поток имеет свой собственный стек

  • В стеках хранятся такие типы значений, как структуры и перечисления.

  • Если размер вашего типа значения может быть определен во время компиляции или если ваш тип значения не содержит рекурсивно/не содержится в ссылочном типе, тогда он будет требовать аллокации в стеке.

  • Тип-значение не увеличивает счетчик ссылок. Но если ваш тип-значение содержит внутренние ссылки, его копирование потребует увеличения счетчика ссылок его дочерних элементов.

Куча:

  • Динамическое выделение памяти, которое происходит во время выполнения.

  • К значениям можно обращаться в любое время по адресу в памяти.

  • Нет ограничений на размер памяти.

  • Более медленный доступ.

  • Когда процесс запрашивает определенный объем памяти, куча ищет адрес памяти, который удовлетворяет этому запросу, и возвращает его процессу.

  • Когда память больше не используется, процесс должен указать динамической памяти о высвобождении этого раздела памяти.

  • Требует потокобезопасности.

  • Куча совместно используется всем, чем угодно.

  • Если размер вашего типа значения не может быть определен во время компиляции (из-за протокола/общего требования), или если ваш тип значения рекурсивно содержит/содержится ссылочным типом (помните, что замыкания также являются ссылочными типами), то потребуется выделение в куче.

  • Класс хранится в куче.

Аллокация в куче медленнее, чем аллокация в стеке не только из-за более сложной структуры данных — она также требует потокобезопасности. Каждый поток имеет свой собственный стек, но куча используется совместно всем, что требует синхронизации.

Несколько каверзных вопросов с собеседований

1. Что происходит, когда мы выполняем приведенный ниже код?

Ответ: Объект будет немедленно аллоцирован и деаллоцирован, и вы не сможете ссылаться на этот объект снова. Вот простой пример:

2. Почему IBOutlets слабые. Что произойдет, если вы используете сильные ссылки?

Если вы объявите IBOutlets сильным или слабым, ваше приложение не даст сбой. Каждый контроллер представления хранит ссылку на представление, которым он управляет. Эта ссылка сильная. Представление не должно высвобождаться, пока жив контроллер представления.

Представление этого контроллера представления всегда содержит сильную ссылку на подпредставления, которыми оно управляет. Это имеет смысл, потому что подпредставления по-прежнему живы и видны, даже если мы не объявляем outlet для подпредставлений в классе ViewController.

В соответствии с ARC: когда контроллер представления высвобождается, представление, которым он управляет, также высвобождается. Это также означает, что любые подпредставления, которыми управляет представление, также высвобождаются.

Иерархия представлений уже имеет сильную ссылку, поэтому назначение слабой ссылки для IBoutlets может помочь избежать циклов сохранения при уничтожении контроллера представления.

По состоянию на 2015 год рекомендуемая Apple передовая практика заключалась в том, чтобы IBOutlets был сильным, если только не требуется слабая ссылка специально, чтобы избежать цикла сохранения.

Материал подготовлен для будущих студентов специализации "iOS Developer". Скоро пройдет открытый урок «Классы», на котором изучим объект class, свойства, методы, инициализаторы и создадим экземпляры класса. Если интересно — записывайтесь.

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


  1. Gargo
    02.02.2022 21:46
    +1

    1. MRC: manual reference counting (ручной подсчет ссылок)

    нету такой аббревиатуры. Есть ARC и есть MRR (manual retain release)


    1. ws233
      03.02.2022 10:17
      +1

      Вот тут из 8 вхождений 7 -- это manual reference counting, и только одно - manual retain release. Правда, в этой же статье есть ссылка и сюда. И вот тут уже дается определение MRR, но тоже всего в нескольких упоминаниях. Имхо, не стоит так категорично. Кажется, что даже сами Apple вполне себе используют аббревиатуру MRC. Да и в русском сообществе она очень популярна. Ну, и так ли важно, как называется технология, или все же важнее понимают ли разработчики, как она работает?



  1. ws233
    03.02.2022 10:20

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

    И еще один. Какую из ссылок в графе ретейн цикла по теории можно сделать слабой для устранения проблемы? А какую нужно, чтобы не создать себе других проблем? Как определять ту единственную связь, которую в графе безопасно ослаблять?


    1. Agranatmark
      03.02.2022 21:41
      +1

      Многие на таком вопросе сыпятся:
      - Сделай retain cycle из одного объекта.


      1. kpanic666
        04.02.2022 17:47

        class UniqApple {

            static var shared = UniqApple()

            private init() { print("initialized") }

            deinit { print("deinitialized") }

        }

        var d: UniqApple? = UniqApple.shared

        d = nil


        1. Agranatmark
          04.02.2022 18:05

          В целом да, я больше про:

          class SelfRef {
          
              var selfRef: SelfRef?
          
          }
          
          let ref = SelfRef()
          
          ref.selfRef = ref


  1. faiwer
    03.02.2022 13:21

    Стек. Статическое выделение памяти, которое происходит только во время компиляции.

    А что тут имеется ввиду под "во время компиляции"? Как можно выделить память во время компиляции? И где она тогда выделяется. Какого рода "компиляция" имеется ввиду? Память в стеке ведь выделяется при создании очередного stack frame-а, т.е. в runtime.


    upd. или имеется ввиду что на весь стек резервируется память при запуске приложения и больше её размер не меняется? ну тогда тоже непонятно, причём тут компиляция