Анимация и настройка iOS для повышения эффективности

Опубликовано: 2022-03-11

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

Анимация и настройка iOS для повышения эффективности

В целях тестирования производительности я бы рекомендовал использовать реальные устройства. Если вы серьезно относитесь к созданию приложений и их оптимизации для плавной анимации iOS, симуляторы просто не подойдут. Моделирование иногда может не соответствовать реальности. Например, симулятор может работать на вашем Mac, что, вероятно, означает, что процессор (центральный процессор) намного мощнее, чем процессор вашего iPhone. И наоборот, GPU (графический процессор) настолько отличаются между вашим устройством и вашим Mac, что ваш Mac фактически эмулирует GPU устройства. В результате операции с привязкой к процессору, как правило, выполняются быстрее в симуляторе, а операции с привязкой к графическому процессору — медленнее.

Анимация со скоростью 60 кадров в секунду

Одним из ключевых аспектов воспринимаемой производительности является обеспечение того, чтобы ваши анимации работали со скоростью 60 кадров в секунду (кадров в секунду), что соответствует частоте обновления вашего экрана. Есть несколько анимаций, основанных на таймере, которые мы не будем здесь обсуждать. Вообще говоря, если вы работаете со скоростью выше 50 кадров в секунду, ваше приложение будет выглядеть плавно и производительно. Если ваши анимации застревают между 20 и 40 кадрами в секунду, будет заметное заикание, и пользователь обнаружит «шероховатость» в переходах. Все, что ниже 20 кадров в секунду, серьезно повлияет на удобство использования вашего приложения.

Прежде чем мы начнем, вероятно, стоит обсудить разницу между операциями, связанными с ЦП и операциями, связанными с графическим процессором. GPU — это специализированный чип, оптимизированный для отрисовки графики. Хотя процессор тоже может, он намного медленнее. Вот почему мы хотим перенести как можно большую часть рендеринга графики, процесса создания изображения из 2D- или 3D-модели, на GPU. Но нам нужно быть осторожными, так как когда у графического процессора заканчивается вычислительная мощность, производительность, связанная с графикой, снижается, даже если процессор относительно свободен.

Core Animation — это мощный фреймворк, который обрабатывает анимацию как внутри вашего приложения, так и вне его. Он разбивает процесс на 6 ключевых шагов:

  1. Макет: место, где вы упорядочиваете свои слои и устанавливаете их свойства, такие как цвет и их относительное положение.

  2. Отображение: здесь фоновые изображения рисуются в контексте. Любая подпрограмма, которую вы написали в drawRect: или drawLayer:inContext: доступна здесь.

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

  4. Commit: здесь Core Animation отправляет все эти данные на сервер рендеринга.

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

  6. Draw: визуализирует фигуры (фактически треугольники).

Вы могли догадаться, что процессы 1-4 — это операции процессора, а 5-6 — операции графического процессора. На самом деле у вас есть контроль только над первыми двумя шагами. Самый большой убийца графического процессора — это полупрозрачные слои, когда графическому процессору приходится заполнять один и тот же пиксель несколько раз за кадр. Также на производительность будет влиять любой рисунок вне экрана (несколько эффектов слоя, таких как тени, маски, закругленные углы или растеризация слоя заставят Core Animation рисовать вне экрана). Изображения, которые слишком велики для обработки графическим процессором, вместо этого будут обрабатываться гораздо более медленным процессором. Хотя тени можно легко получить, задав два свойства непосредственно для слоя, они могут легко снизить производительность, если на экране много объектов с тенями. Иногда стоит рассмотреть возможность добавления этих теней в качестве изображений.

Измерение производительности анимации iOS

Мы начнем с простого приложения с 5 изображениями PNG и табличным представлением. В этом приложении мы по существу загрузим 5 изображений, но повторим это для 10 000 строк. Мы добавим тени как к изображениям, так и к меткам рядом с изображениями:

 -(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; NSInteger index = (indexPath.row % [self.images count]); NSString *imageName = [self.images objectAtIndex:index]; NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; cell.customCellImageView.image = image; cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; return cell; }

Изображения просто перерабатываются, а метки всегда разные. Результат:

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

Вместо того, чтобы делать слепые предположения, давайте попробуем и измерим производительность. Настало время инструментов.

Чтобы использовать инструменты, вам нужно перейти с «Выполнить» на «Профиль». И вы также должны быть подключены к вашему реальному устройству, не все инструменты доступны на симуляторе (еще одна причина, по которой вам не следует оптимизировать производительность на симуляторе!). В первую очередь мы будем использовать шаблоны «Драйвер GPU», «Основная анимация» и «Профилировщик времени». Малоизвестный факт заключается в том, что вместо того, чтобы останавливаться и запускать другой инструмент, вы можете перетаскивать несколько инструментов и запускать их одновременно.

Теперь, когда мы настроили наши инструменты, давайте измерим. Сначала давайте посмотрим, действительно ли у нас есть проблема с нашим FPS.

Да, я думаю, что мы получаем здесь 18 кадров в секунду. Действительно ли загрузка изображений из пакета в основной поток такая дорогая и затратная? Обратите внимание, что использование нашего средства визуализации почти исчерпано. Как и наше использование плиточника. Оба выше 95%. И это никак не связано с загрузкой изображения из бандла в основной поток, так что давайте не будем искать здесь решения.

Настройка на эффективность

Существует свойство, называемое shouldRasterize, и люди, вероятно, порекомендуют вам использовать его здесь. Что именно должен делать Rasterize? Он кэширует ваш слой как сглаженное изображение. Все эти дорогостоящие рисунки слоев должны быть сделаны один раз. В случае, если ваш кадр часто меняется, нет смысла использовать кеш, так как его все равно придется каждый раз регенерировать.

Внося быструю поправку в наш код, получаем:

 -(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; NSInteger index = (indexPath.row % [self.images count]); NSString *imageName = [self.images objectAtIndex:index]; NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; cell.customCellImageView.image = image; cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; cell.layer.shouldRasterize = YES; cell.layer.rasterizationScale = [UIScreen mainScreen].scale; return cell; }

И снова измеряем:

Всего двумя строчками мы увеличили FPS в 2 раза. Сейчас у нас в среднем выше 40 FPS. Но поможет ли это, если мы переместим загрузку изображений в фоновый поток?

 -(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; NSInteger index = (indexPath.row % [self.images count]); NSString *imageName = [self.images objectAtIndex:index]; cell.tag = indexPath.row; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; dispatch_async(dispatch_get_main_queue(), ^{ if (indexPath.row == cell.tag) { cell.customCellImageView.image = image; } }); }); cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; // cell.layer.shouldRasterize = YES; // cell.layer.rasterizationScale = [UIScreen mainScreen].scale; return cell; }

После измерения мы видим, что производительность составляет в среднем около 18 кадров в секунду:

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

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

Проверяя «Слои со смешанным цветом» в инструменте основной анимации, мы видим:

«Слои со смешанным цветом» отображаются на экране, где ваш графический процессор выполняет большую часть рендеринга. Зеленый цвет указывает на наименьшую активность рендеринга, а красный — на максимальную. Но мы установили для параметра shouldRasterize значение YES . Стоит отметить, что «Слои со смешанным цветом» — это не то же самое, что «Цвет соответствует зеленому и пропускает красный». Последний в основном выделяет растеризованные слои красным цветом по мере регенерации кеша (хороший инструмент, чтобы увидеть, используете ли вы кеш неправильно). Установка для параметра shouldRasterize значения YES не влияет на первоначальный рендеринг непрозрачных слоев.

Это важный момент, и нам нужно сделать паузу, чтобы подумать. Независимо от того, установлено ли для shouldRasterize значение YES или нет, для рендеринга фреймворк должен проверять все представления и смешивать (или нет) в зависимости от того, являются ли подпредставления прозрачными или непрозрачными. Хотя для вашего UILabel может иметь смысл быть непрозрачным, он может быть бесполезен и убивать вашу производительность. Например, прозрачный UILabel на белом фоне, вероятно, бесполезен. Сделаем его непрозрачным:

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

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

  1. Подготовить
  2. Совершить

Может показаться, что это совершенно не в наших руках, но это не совсем так. Мы знаем, что для загрузки изображения его необходимо распаковать. Время декомпрессии меняется в зависимости от формата изображения. Для PNG декомпрессия происходит намного быстрее, чем для JPEG (хотя загрузка дольше, и это также зависит от размера изображения), поэтому мы были на правильном пути, чтобы использовать PNG, но мы ничего не делаем с процессом декомпрессии, и эта декомпрессия происходит в «точке рисования»! Это худшее место, где мы можем убить время, — основной поток.

Есть способ принудительной декомпрессии. Мы могли бы сразу установить его в свойство изображения UIImageView. Но это все еще распаковывает изображение в основном потоке. Есть ли лучший способ?

Есть один. Нарисуйте его в CGContext, где изображение должно быть распаковано, прежде чем его можно будет нарисовать. Мы можем сделать это (используя ЦП) в фоновом потоке и при необходимости задать его границы в зависимости от размера нашего представления изображения. Это оптимизирует наш процесс рисования изображения, выполняя его вне основного потока, и избавит нас от ненужной «подготовки» вычислений в основном потоке.

Пока мы на этом, почему бы не добавить тени, когда мы рисуем изображение? Затем мы можем захватить изображение (и кэшировать его) как одно статическое непрозрачное изображение. Код выглядит следующим образом:

 - (UIImage*)generateImageFromName:(NSString*)imageName { //define a boudns for drawing CGRect imgVwBounds = CGRectMake(0, 0, 48, 48); //get the image NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; //draw in the context UIGraphicsBeginImageContextWithOptions(imgVwBounds.size, NO, 0); { //get context CGContextRef context = UIGraphicsGetCurrentContext(); //shadow CGContextSetShadowWithColor(context, CGSizeMake(0, 3.0f), 3.0f, [UIColor blackColor].CGColor); CGContextBeginTransparencyLayer (context, NULL); [image drawInRect:imgVwBounds blendMode:kCGBlendModeNormal alpha:1.0f]; CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0); CGContextEndTransparencyLayer(context); } image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }

И наконец:

 -(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; // NSInteger index = (indexPath.row % [self.images count]); // NSString *imageName = [self.images objectAtIndex:index]; // // cell.tag = indexPath.row; // // dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; // UIImage *image = [UIImage imageWithContentsOfFile:filePath]; // // dispatch_async(dispatch_get_main_queue(), ^{ // if (indexPath.row == cell.tag) { // cell.customCellImageView.image = image; // } // }); // }); cell.customCellImageView.image = [self getImageByIndexPath:indexPath]; cell.customCellImageView.clipsToBounds = YES; // cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); // cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; cell.layer.shouldRasterize = YES; cell.layer.rasterizationScale = [UIScreen mainScreen].scale; return cell; }

И результаты таковы:

Теперь у нас в среднем выше 55 кадров в секунду, а использование рендеринга и тайлера составляет почти половину того, что было изначально.

Заворачивать

На случай, если вам интересно, что еще мы можем сделать, чтобы увеличить скорость на несколько кадров в секунду, не ищите дальше. UILabel использует WebKit HTML для отображения текста. Мы можем перейти непосредственно к CATextLayer и, возможно, там тоже поиграть с тенями.

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

Настройка на эффективность — это разница между приложением мирового уровня и любительским. Оптимизация производительности, особенно когда речь идет об анимации iOS, может оказаться непростой задачей. Но с помощью Instruments можно легко диагностировать узкие места в производительности анимации на iOS.