Как работает autorelease и почему он откладывает освобождение памяти?

Ответ

autorelease откладывает отправку сообщения release объекту до завершения текущего autorelease pool, обычно в конце run loop цикла.

Механизм работы:

  1. Объект помещается в текущий autorelease pool
  2. При вызове drain() (или автоматически в конце run loop) pool отправляет release всем объектам
  3. Если счетчик ссылок достигает нуля, вызывается dealloc

Зачем это нужно:

  • Возврат объектов из методов — позволяет безопасно вернуть вновь созданный объект без немедленного освобождения
  • Удобство — избавляет от необходимости точно отслеживать моменты release в ручном управлении памятью

Пример (ручное управление памятью):

// Без autorelease — объект освободится ДО возврата из функции
- (NSString *)badMethod {
    NSString *temp = [[NSString alloc] initWithString:@"test"];
    [temp release]; // Освобождаем сразу
    return temp;    // ОШИБКА: возвращаем освобожденный объект!
}

// С autorelease — объект будет освобожден позже
- (NSString *)goodMethod {
    NSString *temp = [[[NSString alloc] initWithString:@"test"] autorelease];
    return temp; // Безопасно, объект будет жить до конца run loop
}

В ARC-среде:

  • Явное использование autorelease почти не требуется
  • Компилятор автоматически вставляет оптимизированные вызовы
  • Autorelease pools создаются автоматически для каждого run loop
  • Можно создавать вложенные pools для контроля времени жизни:
    autoreleasepool {
    // Создание множества временных объектов
    for _ in 0..<10000 {
        _ = UIImage(contentsOfFile: path)
    }
    // Все autoreleased объекты освобождаются здесь
    }

Проблема: Большое количество autoreleased объектов может вызвать всплеск использования памяти.

Ответ 18+ 🔞

А, ну это же классика, про которую каждый второй джуниор в iOS так и норовит на собеседовании блевануть заученной фразой, не понимая, как оно там внутри скрипит, блядь.

Смотри, представь себе бар. Это наш autorelease pool. Ты заходишь туда, заказываешь объект — тебе его наливают в стакан. Ты его выпиваешь (используешь) и мог бы сразу стакан помыть (вызвать release), но бармен такой: «Да ладно, брось его в этот таз с грязной посудой, я потом всё разом в конце ночи в посудомойку закину». Этот таз — и есть autorelease pool, а «конец ночи» — это завершение run loop цикла, когда вызывается drain().

Как оно по косточкам разбирается:

  1. Ты создал объект и сказал ему autorelease. Он такой: «Окей, пошёл в этот самый общий таз-пул записываться».
  2. Жизнь идёт, твоё приложение крутится в цикле событий (run loop). В конце этого цикла система берёт таз и говорит: «Всё, тусовка закончена, вся посуда — в мойку!». И шлёт каждому объекту в тазе сообщение release.
  3. Если после этого счётчик ссылок у объекта стал ноль — ему пи**ец, вызывается dealloc, и память освобождается.

А нахуя этот цирк? Главная фишка, из-за которой его и придумали, — вернуть объект из метода, не угробив его на полпути. Без этого получалась дичь.

Смотри, как было в старые добрые времена ручного управления (MRC):

// Вот так делать НЕЛЬЗЯ, это прямой билет в кресты (EXC_BAD_ACCESS)
- (NSString *)methodForDumbass {
    NSString *temp = [[NSString alloc] initWithString:@"test"]; // retain count = 1
    [temp release]; // retain count = 0 -> объект УМЕР
    return temp;    // ОЙ. Возвращаем труп. Приложение крашнется, когда попробует с этим трупом что-то сделать.
}

// А вот так — правильно. Авторелиз спасает.
- (NSString *)methodForSmartass {
    // Создали (retain count = 1), сразу отправили в autorelease pool
    NSString *temp = [[[NSString alloc] initWithString:@"test"] autorelease];
    return temp; // Спокойно возвращаем. Объект жив и будет жить до конца run loop.
}

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

Но теперь-то у нас ARC! Сейчас компилятор такой хитрожопый, что сам всё расставляет. Явно писать autorelease уже не нужно. Пул создаётся автоматически на каждый run loop. Но понимать механизм всё равно надо, потому что иногда он вылезает боком.

Проблема в чём? А в том, что если ты в цикле наклепаешь овердохуища временных autoreleased объектов (например, 10 000 картинок UIImage), то они все будут валяться в том самом «тазе» и ждать конца run loop. Память будет расти, как на дрожжах, и можешь получить предупреждение от системы. Поэтому в ARC тоже есть инструмент:

// Создаём свой маленький "тазик" для грязной посуды
autoreleasepool {
    for _ in 0..<10000 {
        // Каждая итерация создаёт временный autoreleased объект
        _ = UIImage(contentsOfFile: path)
    }
    // ВСЁ! Здесь наш локальный пул дренируется, и вся временная память освобождается.
    // Не ждём конца основного run loop.
}
// А здесь уже чисто.

Короче, autorelease — это как отсрочка платежа по кредиту. Удобно, если знаешь, что скоро получишь зарплату (закончится run loop). Но если набрать таких кредитов дофига — к концу месяца охуеешь от долгов (высокое потребление памяти). Поэтому умные ребята иногда берут быстрые займы под закрытие (локальные autoreleasepool), чтобы не засирать общую кредитную историю.