УПРАВЛЕНИЕ ПАМЯТЬЮ В SWIFT

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

Справочник

Содержание

Описание

В процессе запуска приложения операционная система выделяет ему виртуальное адресное пространство, где в дальнейшем будут храниться данные. Пространство в памяти выделяется страницами (page) и всегда кратно 16 КБ (размеру одной страницы).

Отмечу, что приложение работает именно с виртуальным пространством, а не физическим. Виртуальные адреса, используемые для доступа к страницам памяти, переводятся в адреса физической оперативной памяти с помощью таблицы page table.

Перерасход памяти

Если приложение используется слишком много памяти, то iOS отправит ему соответствующее предупреждение. При этом могут быть выполнены следующие действия:

  • Для приложений в фоне read-only объекты, которые уже хранятся на диске, могут быть удалены из памяти, так как без проблем могут быть загружены в дальнейшем (кодовые страницы, файлы, различные ресурсы и т.д.).
  • Система вызывает метод applicationDidReceiveMemoryWarning делегата приложения, в котором должны освобождаться как можно больше ресурсов из памяти (например удаляться кэши). Если приложение не реагирует на данный метод, то оно может быть незамедлительно закрыто.
  • Приложение закрывается, чтобы освободить используемую им память.

Структура памяти

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

  1. Инструкции, код программы.
  • Глобальные данные.
  • Куча (Heap).
  • Пустое пространство, куда при необходимости расширяются куча и стек.
  • Стек (Stack).

Экземпляры всех объектов, которые мы создаем в процессе разработки приложения (классы, структуры, функции, перечисления, кортежи и т.д.), хранятся в Куче и Стеке. Это две принципиально разные структуры, со своими «законами» и «правилами».

Стек

Стек в виртуальной памяти является реализацией структуры данных Stack, работающей на основе принципа LIFO (Last in, First Out — Последний пришел, первый ушел). Все объекты, помещенные в стек, удаляются в обратно порядке. Определение размера выделяемой в стеке памяти происходит еще во время компиляции программного кода.

В случае многопоточной работы приложения каждый поток (thread) имеет свой собственный стек.

Стек предназначен для хранения значимых типов (value type) и ссылок на ссылочные типы (reference type).

Фреймы

Как только в программе появляется новая область видимости (scope), например вызывается метод или выполняется иттерация цикла, в стеке создается новый фрейм (frame), то есть выделяется память, необходимая данной области видимости для хранения данных. Для выполнения операции выделения требуется всего 1 процессорная инструкция, перемещающая указатель на указанный адрес. Именно по этой причине работа со стеком является очень дешевой с точки зрения производительности операцией.

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

Куча

Куча предназначен для хранения ссылочных типов (reference type). Память в ней выделяется динамически прямо во время исполнения программы. Каждый экземпляр, хранящийся в куче, является независимым от других. Доступ к экземплярам производится по ссылке.

В случае многопоточной работы приложения куча является общей для всех потоков (threads).

Подсчет ссылок

Для того, чтобы эффективно ввыделять и удалять пространство в куче необходимо производить подсчет количества ссылок на объекты. При разработке под iOS используются две системы подсчета количества: MRC и ARC.

Manual Reference Counting (MRC)

При разработке iOS приложений на языке Objective-C используется MRC, то есть система ручного подсчета ссылок. При создании объектов с помощью методов new, copy (и др.) счетчик ссылок на объект увеличивается на 1. При вызове метода release счетчик ссылок уменьшался на 1. Как только счетчик ссылок на объект становится равным нулю, он удаляется из кучи.

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

Automatic Reference Counting (ARC)

Swift, в отличии от Objective-C, использует систему автоматического подсчета ссылок (ARC). В процессе компиляции компилятор автоматически расставляет вызоыве методов, увеличивающих и уменьшающих количество ссылок. А в процессе функционироввания приложения ARC постоянно мониторит количество ссылок. Как и в случае с MRC, как только счетчик ссылок становится равным нулю — объект удаляется.

Autorelese Pool

Autorelease pool — это хранилище объектов ссылочного типа, которые необходимо уничтожить не прямо сейчас, а несколько позже. В некоторых случаях требуется отложить уменьшение счетчика, например при возвращении объекта из функции. Так, если в конце функции вызвать release у возвращаемого объекта, то он будет уничтожен еще до того, как функция завершит работу. Но нам требуется, чтобы объект продолжал жить даже после завершения работы функции. Для таких ситуаций (когда нужно отложить уменьшение счетчика ссылок) существует метод autorelease. При его использовании вместо release данный объект помечается, как требующий уменьшения счетчика ссылок, но не сейчас, а несколько позже. Помеченный с помощью autorelease объект помещается в специальный autorelease pool.

Autorelease создается в начале каждой иттерации RunLoop и обрабатывает все хранящиеся в нем объекты (уменьшает их счетчик ссылок) в конце этой иттерации. Для очищения autorelease pool вызывается его метод drain.

Данная система активно использовалась во времена использования MRC. Сегодня существует ARC и autorelease pool напрямую может использоваться только для ObjC-объектов, или объектов, помеченных с помощью аттрибута @autorelease (например UIImage). В большинстве случаев вам не стоит думать о нем, Swift все делает автоматически.

Пример использования autorelease

autoreleasepool {
// ...
}

// пример
// все параметры image всех иттераций цикла будут храниться до конца жизни потока
// хотя в этом нет необходимости
// В результате. вприложении будет огромный перерасход памяти
for i in 1...1000 {
let image = UIImage(named: "\(i).jpg")
// ... сохранение файла в БД
// по сути image больше не нужен
}

// с помощью autoreleasepool значение параметр image будет удаляться 
// в конце каждой иттерации цикла
for i in 1...1000 {
autoreleasepool {
    let image = UIImage(named: "\(i).jpg")
    // ... сохранение файла в БД
}
}

Выделение памяти под объекты

Value type и Reference type

К значимым типам относятся структуры (struct), перечисления (enum) (но не indirect enum, это ссылочный тип) и кортежи (tuple). По умолчанию экземпляры value type хранятся в стеке, а память для них выделяется при инициализации значения (при присвоении нового или копировании старого значения). Такой подход называется copy-on-assignment.

Но существуют исключения, например:

  • массив может хранить значение, как в стеке, так и в куче (в зависимости от того, что является элементом массива. Так же с целью оптимизации массив поддерживает копирование при записи (copy-on-write), вместо copy-on-assignment.
  • значение value type входит в состав ссылочного типа.

К ссылочными типами относятся классы (class), замыкания (closure) и indirect enum. Значения ссылочных типов всегда хранятся в куче. При копировании такого значения в новый параметр инициализируется ссылка на уже существующее значение.

Copy-on-assignment и copy-on-write

Copy-on-assignment и Copy-on-write — это два важнейших механизма, работающих внутри функции копирования экземпляров. Они позволяют вам наиболее эффективно использовать оперативную память устройства.

Copy-on-assignment (COA)

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

var source = 3
// Int - структура
// копируем экземпляр структуры
var copy = source

// выводим адреса в памяти
withUnsafePointer(to: &source) {
print($0) // текущий адрес, например 0x00000001155e5090
}

withUnsafePointer(to: &copy) {
print($0) // другое значение, например 0x00000001155e5098
}

Copy-on-write (COW)

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

COW работе прозрачно и скрытно от разработчика.

Copy-on-write в кастномном типе

Существует вариант интеграции COW в собственный тип данных. В этом случае создается некий класс-враппер, являющийся оболочкой для хранимого значения.

Показанная реализация COW представлена в документации к исходному коду Swift на GitHub-аккаунте Apple

final class Ref<T> {
    var val: T
    init( _ v: T) {
        val = v
    }
}

struct Box<T> {
    var ref: Ref<T>
    init(_ x: T) {
        ref = Ref(x)
    }
    var value: T {
        get { return ref.val }
        set {
            if (!isUniquelyReferenceNonObjC(&ref)) {
                ref = Ref(newValue)
                return
            }
            ref.val = newValue
        }
    }
}

Дополнительная информация

1 Comment

  1. algebra.1993:

    А есть какой-нибудь наглядный пример этого утверждения: «В случае многопоточной работы приложения каждый поток (thread) имеет свой собственный стек». В оф доках я не нашел внутренней реализации, чтобы подтвердить это.

Добавить комментарий