Эта запись в блоге является продолжением Способы улучшить свое мышление в отношении цели C - Часть 1.

Перейти в пулы автозапуска

Объекты Objective-C, проходящие свою жизнь, подвергаются подсчету ссылок. Освобождение объекта означает, что его счетчик удержания либо немедленно уменьшается посредством вызова release, либо добавляется в пул автозапуска посредством вызова autorelease. При использовании ARC (автоматический подсчет ссылок, который сейчас используется по умолчанию) мы не беспокоимся о размещении этих сообщений, это выполняется компилятором автоматически. Пул с автоматическим выпуском на самом деле представляет собой набор объектов, которые будут выпущены в какой-то момент в будущем (либо в конце цикла выполнения потока, либо в конце области действия пула с автоматическим выпуском). Когда пул опорожняется, всем объектам в пуле отправляется сообщение release.

Синтаксис создания пула автозапуска довольно прост:

@autoreleasepool {
    //…
}

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

Давайте посмотрим на этот пример:

for (int i = 0; i < 100000; i++) {
    [self doMagicWith:i];
}

Если метод doMagicWith создает временные объекты, они обязательно будут помещены в пул автозапуска (пул из цикла выполнения текущего потока). Несмотря на то, что они не используются после завершения метода, эти объекты могут оставаться активными, потому что они находятся в пуле автозапуска, готовы к освобождению и впоследствии освобождены. Однако пул не будет очищен до следующего цикла обработки событий этого потока. Это означает, что по мере выполнения цикла for будет создаваться все больше и больше объектов, которые будут добавляться в пул автозапуска. В конце концов, как только он закончится, объекты будут освобождены. Но во время выполнения цикла объем памяти приложения будет увеличиваться (что может даже привести к тому, что ОС убьет наше приложение), а затем внезапно уменьшится, поскольку все временные объекты, наконец, будут освобождены.

Рассмотрите возможность чтения набора объектов из базы данных:

NSArray *databaseRecods = /*…*/;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecods) {
    SSLPerson *person = [[SSLPerson alloc] initWithRecord:record];
    [people addObject:person];
}

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

NSArray *databaseRecods = /*…*/;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecods) {
    @autoreleasepool {
        SSLPerson *person = [[SSLPerson alloc] initWithRecord:record];
        [people addObject:person];
    }
}

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

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

Подождите с группами отправки

Группы отправки - это функция GCD (Grand Central Dispatch), которая позволяет вам легко группировать (обычно не связанные) задачи и ждать их завершения, прежде чем вы сможете продолжить выполнение. Это может быть очень полезно, когда нам нужно выполнять несколько задач одновременно и знать, когда закончилась последняя. Представьте, что нам нужно выполнить несколько независимых сетевых запросов, и мы хотим, чтобы все они были выполнены, прежде чем мы продолжим. Конечно, мы могли бы каскадировать все вызовы в блоках завершения и вызывать наш последний блок завершения в конце, но это не только выглядит очень плохо, но и «сериализует» вызовы, вместо того, чтобы делать их параллельными друг другу. Мы не хотим звонить им в каком-то определенном порядке, нас просто интересует, когда все будет сделано. Эту проблему можно решить с помощью Dispatch Groups, и вот как вы ее создаете:

dispatch_group_t group = dispatch_group_create();

Теперь давайте добавим задачи в эту группу. Нам нужно сообщить группе, когда мы начинаем и заканчиваем выполнение задачи:

dispatch_group_enter(group);
[networkManager startFirstRequestWithCompletion:^(id result){
    //Handle result
    dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[networkManager startSecondRequestWithCompletion:^(id result){
    //Handle result
    dispatch_group_leave(group);
}];

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

Как мы узнаем, что группа задач завершилась? Мы можем заблокировать текущий поток, пока группа не будет завершена:

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//We won't get here until everything has finished

Или мы можем попросить группу асинхронно уведомить нас, когда это будет сделано:

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //This block will be called when everything has finished
});

Кэш с NSCache

Когда нам нужен кеш в памяти, мы обычно используем NSMutableDictionary. Однако по определению кеш - это то, что должно поддерживать освобождение памяти в случае необходимости. Здесь на помощь приходит NSCache. Когда системная память заполняется, этот кеш автоматически освобождается и, что еще лучше, в первую очередь избавляется от наименее используемых объектов. Еще одно преимущество NSCache по сравнению с NSMutableDictionary состоит в том, что это потокобезопасность. Это очень полезно, потому что мы можем захотеть прочитать из него в одном потоке, и если ключ не существует, загрузим данные в фоновом потоке.

В определенной степени мы можем контролировать, когда кеш освободит свое содержимое. Две метрики, которые мы можем настроить вместе с ресурсами системной памяти: ограничение на количество объектов в кеше и общая стоимость объектов. При добавлении в кэш каждому объекту может быть дополнительно присвоена стоимость. Когда общее количество объектов превышает предел количества или общая стоимость превышает предел стоимости, кэш может вытеснять объекты, так же, как это происходит, когда доступная системная память становится недостаточной. Однако важно отметить, что он может выселить, а не выселить. Это означает, что мы не можем ничего форсировать, мы просто предоставляем запрос и манипулируем метриками для принудительного выселения - плохая идея.

Метрику стоимости следует использовать только в том случае, если расчет стоимости невысокий. В противном случае использование кеша становится неоптимальным, поскольку нам нужно вычислять это каждый раз, когда объект кэшируется, а кеш должен помочь сделать приложение более отзывчивым. Например, получение доступа к диску для определения размера файла и определения стоимости - плохая идея. С другой стороны, если объекты NSData добавляются в кеш, мы можем использовать их размер в качестве стоимости. Допустим, у нас есть сетевой менеджер, который загружает данные со своим NSCache кешем. Мы можем настроить кеш так:

cache.countLimit = 100; //Cache a maximum of 100 objects
cache.totalCostLimit = 5 * 1024 * 1024; //The size in bytes of data
                                        //so this is a limit of 5MB

Теперь мы можем использовать кеш так:

NSData *cachedData = [cache objectForKey:url];
if (cachedData) {
    //Use data
} else {
    //Fetch data
    [cache setObject:fetchedData 
              forKey:url 
                cost:fetchedData.length];
    //Use data
}

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

Я должен отметить, что ограничение количества и общая стоимость не соблюдаются строго. То есть, когда кеш выходит за одно из своих пределов, некоторые из его объектов могут быть удалены немедленно, позже или никогда, все в зависимости от деталей реализации кеша. Если нам нужно быть более строгими с восстановлением памяти, мы можем использовать NSCache с NSPurgeableData вместо NSData. NSPurgeableData объекты «легче» отбрасываются при нехватке памяти. Давайте перепишем предыдущий пример, используя очищаемые данные:

NSPurgeableData *cachedData = [cache objectForKey:url];
if (cachedData) {
 //Prevent the data from being purged
 [cachedData beginContentAccess];
 
 //Use cached data
 
 //Mark that the data may be purged again
 [cachedData endContentAccess];
} else {
    //Fetch data
    //Create purgeable data
    NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
    //Cache data
    [cache setObject:purgeableData 
              forKey:url 
                cost:purgeableData.length];
    //Use data
    //Mark that the data may be purged now
    [purgeableData endContentAccess];
}

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