[Разработка под iOS, Разработка мобильных приложений, Swift] Память в Swift от 0 до 1
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В этой статье мы рассмотрим принципы работы памяти в Swift и разберемся, как Swift располагает байты в памяти, как управляет памятью, и что из себя представляет жизненный цикл объектов.Содержание
- Что такое память?
- Memory Layout
- Deep dive
- Управление памятью
- Object Lifecycle
- Используемые материалы
Что такое память?Основной единицей информации является бит, который равен 1 или 0. Традиционно мы организовываем биты в группы по восемь, называемые байтами. Память — это просто длинная последовательность байтов, один за другим уходящих в даль. Но они расположены в определенном порядке. Каждый байт получает число, называемое его адресом.
1 байт = 8 битМы рассматриваем память, организованную по словам (word), а не по байтам. Слово — это расплывчатый термин в информатике, но обычно он означает единицу размером с указатель. На современных устройствах для 64-битного процессора (который и стоит на iPhone) одно слово (word) равняется 8 байтам (64 бита = 8 байтам). Именно столько мы можем получить байт за одно обращение к памяти.
1 слово = 8 байт (на 64 битных процессорах)Но как наши данные хранятся в памяти? Как они располагаются и почему? Давайте заглянем под капот и, быть может, там найдем для себя что-то интересное.Memory LayoutSizeИтак, первое, что нам интересно узнать — сколько потребуется выделить памяти для хранения структуры FullResume.
struct FullResume {
let id: String
let age: Int
let hasVehicle: Bool
}
Проверить ответ нам поможет MemoryLayout, который позволяет узнать информацию о размере структуры. Через статические свойства мы можем получить информацию о размере, выравнивании и шаге.
MemoryLayout<FullResume>.size // 25
Размер вычисляется достаточно просто — это сумма всех его полей. Как мы можем увидеть, String занимает 16 байт, Int — 8 байт, а Bool — 1 байт.
MemoryLayout<String>.size // 16
+ MemoryLayout<Int>.size // 8
+ MemoryLayout<Bool>.size // 1
Ради интереса попробуем переставить наше Bool свойство на первое место, остальные поля просто сместим ниже. Сколько теперь? Кажется, 25. Или нет? По логике размер не должен измениться, ведь он считается по сумме всех полей. Проверяем!
struct FullResume {
let hasVehicle: Bool
let id: String
let age: Int
}
MemoryLayout<FullResume>.size // 32 ???
MemoryLayout<Bool>.size // 1
+ MemoryLayout<String>.size // 16
+ MemoryLayout<Int>.size // 8
Немного неожиданный результат: перестановкой мы заняли только больше памяти. Что ж, давайте разбираться дальше.Посмотрим, как наша структура разместилась в памяти, в этом нам поможет метод withUnsafeBytes(_:), который возвращает нам UnsafeRawBufferPointer, позволяющий итерироваться по каждому байту. Получаем следующую картину — Bool занял все 8 байтов, String как и ожидалось свои 16 байтов и Int занял 8 байтов.
Расположение байтов для FullResumeЧтобы разобраться с этим, нам понадобится понять еще два термина — stride (шаг) и alignment (выравнивание). Для начала познакомимся со stride.StrideЯ выделю простую структуру ShortResume. Простую в том плане, что она имеет меньший размер (Int32 — 4 байта, Bool — 1 байт) и будет проще восприниматься на изображениях.
struct ShortResume {
let age: Int32
let hasVehicle: Bool
}
Шаг между двумя ShortResumeИтак, как видно на изображении выше, шаг определяет промежуток между элементами, который всегда будет больше или равен размеру объекта. Благодаря шагу мы знаем, на сколько байтов нужно двигать указатель, чтобы добраться до следующего объекта. Можно заметить, что между первым и вторым резюме остается свободные 3 байта. Но зачем и для чего нужны эти 3 пустые байта? Вопросов становится больше, чем ответов, но мы уже близки к разгадке.AlignmentПервое, что нам хочется понять — для чего нужно выравнивание? В начале статьи выделялся такой термин как word, который на примере ShortReume выглядит так:
Выравнивание между двумя ShortResumeСуть выравнивания в том, чтобы сделать как можно меньше обращений к памяти для получения данных — это позволит работать программе максимально быстро.Для ясности рассмотрим пример не выровненных (смещенных) данных. Это грозит тем, что для получения значения Int32 нужно сделать два обращения к памяти — сначала прочитать слово 0, затем слово 1, соединить два прочитанных массива байтов, и только затем получить окончательное значение. На всё это накладывается непонятки: откуда читать данные у слова 0 и до куда у слова 1.Дабы избежать таких ситуаций, у нас и есть такое значение — alignment (выравнивание).У всех простых типов в Swift есть свое выравнивание. Простой Int или String должен выравниваться по 8 байт, Int32 и Int16 требуют меньше выравнивания — 4 и 2 байта соответственно, а для Bool достаточно одного. Как можно заметить, для простых типов выравнивание равно размеру. Но давайте рассмотрим, как эти числа влияют на memory layout структуры.
MemoryLayout<Int>.alignment // 8
MemoryLayout<Int32>.alignment // 4
MemoryLayout<Int16>.alignment // 2
MemoryLayout<Bool>.alignment // 1
Возвращаясь к нашему FullResume (из которого был убран только String), можно заметить следующее: размер — 9, выравнивание — 8, шаг — 16. Каждое свойство выровнено, мы можем получить любое значение свойства из резюме за один цикл чтения памяти.
Выравнивание для FullResumeИ немного иная картина получится если переставить Bool на первое место. Из-за того что Int имеет выравнивание равное 8, он должен начинаться с байта, кратный 8, поэтому и образовывается пустое место между Bool и Int, что за собой влечет увеличения размера структуры, которая становится 16 вместо 9.
Выравнивание для FullResume c Bool на первом местеВыравнивание всей структуры рассчитывается достаточно просто — это наибольшее выравнивание из всех свойств. Если мы заменим Int на Int16, у которого выравнивание равно 2, то и вся структура будет иметь выравнивание 2.Шаг считается также просто, но немного хитрее — это размер округленный в большую сторону, кратный выравниванию. Именно поэтому при размере структуры равному 9 байт следующим числом, кратным 8, будет 16.Проверь себя
struct Test {
let firstBool: Bool
let array: [Bool]
let secondBool: Bool
let smallInt: Int32
}
Нужно определить размер, выравнивание и шаг. В уме это будет немного сложновато, поэтому можно просто набросать на бумажке, какое свойство сколько памяти займет с учетом выравнивания.Ответ
MemoryLayout<Test>.size // 24
MemoryLayout<Test>.alignment // 8
MemoryLayout<Test>.stride // 24
Если распечатать байты, то получим следующую схему:
ClassMemoryLayout работает и для классов. Попробуем вывести для них всё то же самое.
class PaidService {
let id: String
let name: String
let isActive: Bool
let expiresAt: Date?
}
MemoryLayout<PaidService>.size // 8
MemoryLayout<PaidService>.alignment // 8
MemoryLayout<PaidService>.stride // 8
Что ж, везде будет 8, потому что классы — ссылочный тип, а все ссылки равны 8 байтам (на 64-битной машине).Чтобы узнать реальный размер, занимаемый в куче, нужно воспользоваться Objective-C runtime функцией — class_getInstanceSize(_:). В этом случае получится: 16 * 2 String + 8 Bool (1 + 7 alignment) + 8 Date + 8 Optional (1 + 7 alignment) + 16 metadata (isa ptr + ref count)
Расположение в памяти для класса PaidServiceDeep diveХочется погрузиться немного глубже и найти какой-нибудь способ, чтобы обследовать содержимое памяти напрямую. Не просто распечатывать байты, как мы делали до этого, а посмотреть на указатели: кто где живет и как размещается в памяти. Для всего этого нам нужно:
- Дампить память
- Найти указатели
- Визуализировать
Поначалу были попытки написать такую программу самостоятельно, пока не нашелся интересный доклад от Mike Ash.
Доклад от Mike AshСуть в том, что Mike Ash написал программу на Swift, которая может прыгать по указателям и уходить в глубину, учитывая то, что в какой-то момент указатель может стать конечным. Для того, чтобы не словить краш при обращении к указателю, которого нет, он использует вспомогательные функции из языка C. Исходный код открыт, и с ним можно ознакомиться.
Слайды о программе MemoryDumper из докладаВспомним наш FullResume и попробуем прогнать его через dumper.Самая прелесть заключается в том, что этот dumper строит граф памяти, и имеется возможность оценить всё визуально.
FullResume через dumperПолучилось довольно изящно. Здесь отображается как адрес памяти самого объекта, так и адреса его внутренностей.Попробуем изменить структуру на класс и снова прогоним через наш Memory Dumper.
class FullResume {
let id: String
let age: Int
let hasVehicle: Bool
}
class FullResume через dumperНемного сложней, чем структура, да.Это и логично, классы в Swift сами по себе сложнее, так как связаны с Objective-C, хранятся в куче, имеют свои метаданные для указателей, счетчики ссылок и так далее.Теперь нам удалось очень наглядно разглядеть эту разницу.Управление памятьюМы немного покопались в памяти, посмотрели как располагаются наши данные, узнали, что порядок свойств объявленных в классе или структуре напрямую влияет на выделяемый размер.Но есть еще интересные моменты, связанные с тем, как и сколько живут классы в памяти. Давайте посмотрим, как счетчики ссылок влияют на управление памятью.Reference CountersВсего в Swift три счетчика ссылок:
- Strong
- Weak
- Unowned
Попробуем разобраться зачем так много, как они все работают вместе и где хранятся.До Swift 4Прежде как перейти к текущей реализации счетчиков ссылок, хочется упомянуть старую, чтобы прояснить, для чего сделали новые механизмы и какие проблемы решили.До Swift 4, счетчики ссылок располагались до свойств класса прямо в объекте. Класс имел только два счетчика — weak и strong.
На объект начинает ссылаться два внешних объекта — один сильно, другой слабо, счетчики прибавляются по одному.
В один момент времени объект с сильной ссылкой удаляется из памяти, и теперь у нас осталась только одна слабая ссылка. Что происходит в этот момент?
Данные объекта уничтожаются, но память не освобождается, так как счетчик еще требуется хранить. В памяти остается так называемый «зомби объект», на который ссылается слабая ссылка. Только при обращении по слабой ссылке в runtime будет выполнена проверка: «зомби» (NSZombie) этот объект или нет. Если да, счетчик ссылок уменьшается.Xcode умеет находить такие объекты и сообщать о них, плюс имеет инструмент для этого.Данный подход достаточно прозрачный, но главный минус в том, что так объекты могут долго оставаться в памяти, занимая лишнее место, хотя не несут никакой пользы.Встречался еще один достаточно критичный баг: получение (загрузка) объекта по слабой ссылке было не потокобезопасным!
import Foundation
class Target {}
class WeakHolder {
weak var weak: Target?
}
for i in 0..<1000000 {
print(i)
let holder = WeakHolder()
holder.weak = Target()
dispatch_async(dispatch_get_global_queue(0, 0), {
let _ = holder.weak
})
dispatch_async(dispatch_get_global_queue(0, 0), {
let _ = holder.weak
})
}
Данный кусок кода может получить ошибку в Runtime. Суть именно в том механизме, который был рассмотрен ранее. Два потока могут одновременно обратиться к объекту по слабой ссылке. Перед тем, как получить объект, они проверяют, является ли проверяемый объект «зомби». И если оба потока получат ответ true, они отнимут счётчик и постараются освободить память. Один из них сделает это, а второй просто вызовет краш, так как попытается освободить уже освобожденный участок памяти.Такая реализация не очень хороша и с этим нужно что-то делать.Side TableВ новой реализации появляется такое понятие как Side Table или, если на русском — «Боковая Таблица».Боковая таблица — это область в памяти, содержащая некоторую дополнительную информацию об объекте, которую не нужно хранить в нем самом. В текущей реализации в боковой таблице хранятся счетчики ссылок, но в некоторых статьях мелькала мысль, что там можно было бы хранить associated objects. Сейчас они хранятся в глобальной таблице, доступ к которой замедлен из-за потокобезопасности.Стоит разобраться как сегодня Swift работает с боковой таблицей. Потому что в новой реализации объект должен как-то ссылаться на боковую таблицу и работать со счетчиками ссылок.Чтобы избежать дополнительных затрат в виде 8 байт на указатель боковой таблицы, Swift прибегает к изящной оптимизации.
До создания боковой таблицыПервоначально объект содержит pointer и имеет только два счетчика ссылок. Боковой таблицы нет, ибо объект в ней никак не нуждается. При увеличении счетчика сильных ссылок всё работает как обычно, и ничего особенного не происходит.
Объект с боковой таблицейВо втором поле резервируется один бит, по которому определяется, используется ли сейчас боковая таблица в этом поле или здесь хранится счетчик ссылок.Как только мы начинаем ссылаться на объект слабо (weak reference), то создается боковая таблица, и теперь объект вместо сильного счетчика ссылок хранит ссылку на боковую таблицу. Сама боковая таблица также имеет ссылку на объект.Еще боковая таблица может создаваться, когда происходит переполнение счетчика, и он уже не помещается в поле (счетчики ссылок будут маленькими на 32-битных машинах).
Ссылка на объект через weakС таким механизмом слабые ссылки ссылаются не напрямую на объект, а на боковую таблицу, которая указывает на объект. Это решает две предыдущие проблемы:
- Экономие памяти. Объект удаляется из памяти, если на него больше нет сильных ссылок.
- Это позволяет безопасно обнулять слабые ссылки, поскольку слабая ссылка теперь не указывает напрямую на объект и не является предметом race condition.
Object lifecycleТакой механизм немного усложняет понимание жизненного цикла объекта, но в самом исходном коде Swift в комментариях он расписан хорошо и представляет из себя конечную машину состояний.
Live состояниеИтак, машина заводится с самого первого состояния сразу, как только мы создали объект. Объект жив, его счетчики инициализируются со значениями strong — 1, unowned — 0, weak — 0 (weak появляется только с боковой таблицей). На данный момент нет боковой таблицы. Операции с unowned переменными работают нормально.На объект также может быть unowned ссылка, которая прибавляет +1 к unowned и +1 к strong.
+ 1 unowned ссылка на объектКогда strong RC достигает нуля, вызывается deinit(), и объект переходит в следующее состояние.
Deiniting состояниеЭто состояние Deiniting. На данном этапе операции со strong ссылками не действуют. Unowned отнимает -1 от strong, который был прибавлен на предыдущем шаге. При чтении через unowned ссылку будет срабатывать assertion failure. Но новые unowned ссылки еще могут добавляться. Если есть боковая таблица, то weak операции будут возвращать nil. Далее из этого состояния уже можно перейти в два других.
Deiniting без weak и unownedПервое: если нет боковой таблицы (то есть нет weak ссылок) и нет unowned ссылок, то объект переходит в Dead состояние и сразу удаляется из памяти.
Deinited состояниеВторое: если у нас есть unowned или weak ссылки, объект переходит в состояние Deinited. В этом состоянии функция deinit() завершена. Сохранение и чтение сильных или слабых ссылок невозможно. Как и сохранение новых unowned ссылок. При попытке чтения unowned ссылки вызывается assertion failure. Из этого состояния также возможно два исхода.
Deinited без weak ссылокВ том случае, если нет слабых ссылок, объект переходит непосредственно в состояние Dead, которое было описано выше.
Freed состояниеВ случае наличия weak ссылок, а значит и боковой таблицы, осуществляется переход в состояние Freed (Освобожден). Weak ссылки также добавляет +1 к unowned, и после освобождения отнимает обратно. В Freed состоянии объект уже полностью освобожден и не занимает места в памяти, но его боковая таблица остается жива.
Dead состояниеПосле того как счетчик слабых ссылок достигает нуля, боковая таблица также удаляется и освобождает память, и осуществляется переход в финальное состояние — Dead.В мертвом состоянии от объекта ничего не осталось, кроме указателя на него. Указатель на HeapObject освобождается из кучи, не оставляя следов объекта в памяти.Инварианты счетчиков ссылокВесь жизненный цикл сопровождается инвариантами счетчиков ссылок. Инвариантность — это выражение, определяющее непротиворечивое внутреннее состояние объекта.
- Если счетчик strong ссылок становится равен нулю, то объект всегда переходит в состояние deiniting. Unowned ссылки выкидывают ошибку в runtime, а чтение weak ссылок возвращает nil.
- Счетчик unowned ссылок добавляет +1 к strong, который впоследствие уменьшается после завершения deinit-a объекта.
- Счетчик weak ссылок добавляет +1 к счетчику unowned ссылок. Он уменьшается после освобождения (freed) объекта из памяти.
Используемые материалы
- Advanced iOS Memory Management with Swift: ARC, Strong, Weak and Unowned Explained
- RefCount.h at Swift repo
- Object life cycle
- Swift 4 Weak References
- Exploring Swift Memory Layout
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка мобильных приложений, Интерфейсы, Дизайн мобильных приложений, Графический дизайн, Дизайн] Адаптация таблиц под мобильные устройства
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android, Дизайн мобильных приложений, Дизайн] Исправляем Госуслуги малой кровью — добровольный редизайн мобильного приложения
- [Разработка мобильных приложений, Управление проектами, Монетизация мобильных приложений, Управление продуктом, IT-компании] Facebook перезапускает Instagram Lite для Android, чтобы занять развивающиеся рынки
- [Разработка мобильных приложений, Тестирование мобильных приложений, Аналитика мобильных приложений] Приложение Роскомнадзора: кому полезно и насколько хорошо защищает данные
- [Python, Работа с 3D-графикой, Разработка мобильных приложений] Кроссплатформенные OpenGL + Python при помощи Kivy
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android] Робопрактика в режиме онлайн для мобильных разработчиков в red_mad_robot
- [Разработка под iOS, Swift] DI в iOS: Complete guide
- [Разработка мобильных приложений, Разработка под Android, Локализация продуктов] CompositionLocal в Jetpack Compose. Что это и как с его помощью реализовать реактивную локализацию приложения
- [Разработка под iOS, Разработка мобильных приложений, Разработка игр, Unity] Запуск игры на Unity из приложения SwiftUI для iOS (перевод)
- [Совершенный код, Разработка мобильных приложений, Разработка под Android, Kotlin] Kotlin Best Practices
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_swift, #_ios, #_memory, #_swift, #_xcode, #_development, #_blog_kompanii_headhunter (
Блог компании HeadHunter
), #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_swift
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:37
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В этой статье мы рассмотрим принципы работы памяти в Swift и разберемся, как Swift располагает байты в памяти, как управляет памятью, и что из себя представляет жизненный цикл объектов.Содержание
1 байт = 8 битМы рассматриваем память, организованную по словам (word), а не по байтам. Слово — это расплывчатый термин в информатике, но обычно он означает единицу размером с указатель. На современных устройствах для 64-битного процессора (который и стоит на iPhone) одно слово (word) равняется 8 байтам (64 бита = 8 байтам). Именно столько мы можем получить байт за одно обращение к памяти. 1 слово = 8 байт (на 64 битных процессорах)Но как наши данные хранятся в памяти? Как они располагаются и почему? Давайте заглянем под капот и, быть может, там найдем для себя что-то интересное.Memory LayoutSizeИтак, первое, что нам интересно узнать — сколько потребуется выделить памяти для хранения структуры FullResume. struct FullResume {
let id: String let age: Int let hasVehicle: Bool } MemoryLayout<FullResume>.size // 25
MemoryLayout<String>.size // 16
+ MemoryLayout<Int>.size // 8 + MemoryLayout<Bool>.size // 1 struct FullResume {
let hasVehicle: Bool let id: String let age: Int } MemoryLayout<FullResume>.size // 32 ??? MemoryLayout<Bool>.size // 1 + MemoryLayout<String>.size // 16 + MemoryLayout<Int>.size // 8 Расположение байтов для FullResumeЧтобы разобраться с этим, нам понадобится понять еще два термина — stride (шаг) и alignment (выравнивание). Для начала познакомимся со stride.StrideЯ выделю простую структуру ShortResume. Простую в том плане, что она имеет меньший размер (Int32 — 4 байта, Bool — 1 байт) и будет проще восприниматься на изображениях. struct ShortResume {
let age: Int32 let hasVehicle: Bool } Шаг между двумя ShortResumeИтак, как видно на изображении выше, шаг определяет промежуток между элементами, который всегда будет больше или равен размеру объекта. Благодаря шагу мы знаем, на сколько байтов нужно двигать указатель, чтобы добраться до следующего объекта. Можно заметить, что между первым и вторым резюме остается свободные 3 байта. Но зачем и для чего нужны эти 3 пустые байта? Вопросов становится больше, чем ответов, но мы уже близки к разгадке.AlignmentПервое, что нам хочется понять — для чего нужно выравнивание? В начале статьи выделялся такой термин как word, который на примере ShortReume выглядит так: Выравнивание между двумя ShortResumeСуть выравнивания в том, чтобы сделать как можно меньше обращений к памяти для получения данных — это позволит работать программе максимально быстро.Для ясности рассмотрим пример не выровненных (смещенных) данных. Это грозит тем, что для получения значения Int32 нужно сделать два обращения к памяти — сначала прочитать слово 0, затем слово 1, соединить два прочитанных массива байтов, и только затем получить окончательное значение. На всё это накладывается непонятки: откуда читать данные у слова 0 и до куда у слова 1.Дабы избежать таких ситуаций, у нас и есть такое значение — alignment (выравнивание).У всех простых типов в Swift есть свое выравнивание. Простой Int или String должен выравниваться по 8 байт, Int32 и Int16 требуют меньше выравнивания — 4 и 2 байта соответственно, а для Bool достаточно одного. Как можно заметить, для простых типов выравнивание равно размеру. Но давайте рассмотрим, как эти числа влияют на memory layout структуры. MemoryLayout<Int>.alignment // 8
MemoryLayout<Int32>.alignment // 4 MemoryLayout<Int16>.alignment // 2 MemoryLayout<Bool>.alignment // 1 Выравнивание для FullResumeИ немного иная картина получится если переставить Bool на первое место. Из-за того что Int имеет выравнивание равное 8, он должен начинаться с байта, кратный 8, поэтому и образовывается пустое место между Bool и Int, что за собой влечет увеличения размера структуры, которая становится 16 вместо 9. Выравнивание для FullResume c Bool на первом местеВыравнивание всей структуры рассчитывается достаточно просто — это наибольшее выравнивание из всех свойств. Если мы заменим Int на Int16, у которого выравнивание равно 2, то и вся структура будет иметь выравнивание 2.Шаг считается также просто, но немного хитрее — это размер округленный в большую сторону, кратный выравниванию. Именно поэтому при размере структуры равному 9 байт следующим числом, кратным 8, будет 16.Проверь себя struct Test {
let firstBool: Bool let array: [Bool] let secondBool: Bool let smallInt: Int32 } MemoryLayout<Test>.size // 24
MemoryLayout<Test>.alignment // 8 MemoryLayout<Test>.stride // 24 ClassMemoryLayout работает и для классов. Попробуем вывести для них всё то же самое. class PaidService {
let id: String let name: String let isActive: Bool let expiresAt: Date? } MemoryLayout<PaidService>.size // 8 MemoryLayout<PaidService>.alignment // 8 MemoryLayout<PaidService>.stride // 8 Расположение в памяти для класса PaidServiceDeep diveХочется погрузиться немного глубже и найти какой-нибудь способ, чтобы обследовать содержимое памяти напрямую. Не просто распечатывать байты, как мы делали до этого, а посмотреть на указатели: кто где живет и как размещается в памяти. Для всего этого нам нужно:
Доклад от Mike AshСуть в том, что Mike Ash написал программу на Swift, которая может прыгать по указателям и уходить в глубину, учитывая то, что в какой-то момент указатель может стать конечным. Для того, чтобы не словить краш при обращении к указателю, которого нет, он использует вспомогательные функции из языка C. Исходный код открыт, и с ним можно ознакомиться. Слайды о программе MemoryDumper из докладаВспомним наш FullResume и попробуем прогнать его через dumper.Самая прелесть заключается в том, что этот dumper строит граф памяти, и имеется возможность оценить всё визуально. FullResume через dumperПолучилось довольно изящно. Здесь отображается как адрес памяти самого объекта, так и адреса его внутренностей.Попробуем изменить структуру на класс и снова прогоним через наш Memory Dumper. class FullResume {
let id: String let age: Int let hasVehicle: Bool } class FullResume через dumperНемного сложней, чем структура, да.Это и логично, классы в Swift сами по себе сложнее, так как связаны с Objective-C, хранятся в куче, имеют свои метаданные для указателей, счетчики ссылок и так далее.Теперь нам удалось очень наглядно разглядеть эту разницу.Управление памятьюМы немного покопались в памяти, посмотрели как располагаются наши данные, узнали, что порядок свойств объявленных в классе или структуре напрямую влияет на выделяемый размер.Но есть еще интересные моменты, связанные с тем, как и сколько живут классы в памяти. Давайте посмотрим, как счетчики ссылок влияют на управление памятью.Reference CountersВсего в Swift три счетчика ссылок:
На объект начинает ссылаться два внешних объекта — один сильно, другой слабо, счетчики прибавляются по одному. В один момент времени объект с сильной ссылкой удаляется из памяти, и теперь у нас осталась только одна слабая ссылка. Что происходит в этот момент? Данные объекта уничтожаются, но память не освобождается, так как счетчик еще требуется хранить. В памяти остается так называемый «зомби объект», на который ссылается слабая ссылка. Только при обращении по слабой ссылке в runtime будет выполнена проверка: «зомби» (NSZombie) этот объект или нет. Если да, счетчик ссылок уменьшается.Xcode умеет находить такие объекты и сообщать о них, плюс имеет инструмент для этого.Данный подход достаточно прозрачный, но главный минус в том, что так объекты могут долго оставаться в памяти, занимая лишнее место, хотя не несут никакой пользы.Встречался еще один достаточно критичный баг: получение (загрузка) объекта по слабой ссылке было не потокобезопасным! import Foundation
class Target {} class WeakHolder { weak var weak: Target? } for i in 0..<1000000 { print(i) let holder = WeakHolder() holder.weak = Target() dispatch_async(dispatch_get_global_queue(0, 0), { let _ = holder.weak }) dispatch_async(dispatch_get_global_queue(0, 0), { let _ = holder.weak }) } До создания боковой таблицыПервоначально объект содержит pointer и имеет только два счетчика ссылок. Боковой таблицы нет, ибо объект в ней никак не нуждается. При увеличении счетчика сильных ссылок всё работает как обычно, и ничего особенного не происходит. Объект с боковой таблицейВо втором поле резервируется один бит, по которому определяется, используется ли сейчас боковая таблица в этом поле или здесь хранится счетчик ссылок.Как только мы начинаем ссылаться на объект слабо (weak reference), то создается боковая таблица, и теперь объект вместо сильного счетчика ссылок хранит ссылку на боковую таблицу. Сама боковая таблица также имеет ссылку на объект.Еще боковая таблица может создаваться, когда происходит переполнение счетчика, и он уже не помещается в поле (счетчики ссылок будут маленькими на 32-битных машинах). Ссылка на объект через weakС таким механизмом слабые ссылки ссылаются не напрямую на объект, а на боковую таблицу, которая указывает на объект. Это решает две предыдущие проблемы:
Live состояниеИтак, машина заводится с самого первого состояния сразу, как только мы создали объект. Объект жив, его счетчики инициализируются со значениями strong — 1, unowned — 0, weak — 0 (weak появляется только с боковой таблицей). На данный момент нет боковой таблицы. Операции с unowned переменными работают нормально.На объект также может быть unowned ссылка, которая прибавляет +1 к unowned и +1 к strong. + 1 unowned ссылка на объектКогда strong RC достигает нуля, вызывается deinit(), и объект переходит в следующее состояние. Deiniting состояниеЭто состояние Deiniting. На данном этапе операции со strong ссылками не действуют. Unowned отнимает -1 от strong, который был прибавлен на предыдущем шаге. При чтении через unowned ссылку будет срабатывать assertion failure. Но новые unowned ссылки еще могут добавляться. Если есть боковая таблица, то weak операции будут возвращать nil. Далее из этого состояния уже можно перейти в два других. Deiniting без weak и unownedПервое: если нет боковой таблицы (то есть нет weak ссылок) и нет unowned ссылок, то объект переходит в Dead состояние и сразу удаляется из памяти. Deinited состояниеВторое: если у нас есть unowned или weak ссылки, объект переходит в состояние Deinited. В этом состоянии функция deinit() завершена. Сохранение и чтение сильных или слабых ссылок невозможно. Как и сохранение новых unowned ссылок. При попытке чтения unowned ссылки вызывается assertion failure. Из этого состояния также возможно два исхода. Deinited без weak ссылокВ том случае, если нет слабых ссылок, объект переходит непосредственно в состояние Dead, которое было описано выше. Freed состояниеВ случае наличия weak ссылок, а значит и боковой таблицы, осуществляется переход в состояние Freed (Освобожден). Weak ссылки также добавляет +1 к unowned, и после освобождения отнимает обратно. В Freed состоянии объект уже полностью освобожден и не занимает места в памяти, но его боковая таблица остается жива. Dead состояниеПосле того как счетчик слабых ссылок достигает нуля, боковая таблица также удаляется и освобождает память, и осуществляется переход в финальное состояние — Dead.В мертвом состоянии от объекта ничего не осталось, кроме указателя на него. Указатель на HeapObject освобождается из кучи, не оставляя следов объекта в памяти.Инварианты счетчиков ссылокВесь жизненный цикл сопровождается инвариантами счетчиков ссылок. Инвариантность — это выражение, определяющее непротиворечивое внутреннее состояние объекта.
=========== Источник: habr.com =========== Похожие новости:
Блог компании HeadHunter ), #_razrabotka_pod_ios ( Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_swift |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:37
Часовой пояс: UTC + 5