Несмотря на то, что много статей и видео с конференций WWDC посвящены производительности UIKit, эта тема до сих пор непонятна многим IOS разработчикам. По этой причине мы решили собрать наиболее интересующие вопросы и проблемы, от которых в первую очередь зависит скорость и плавность работы UI приложения.
Первая проблема, на которую стоит обратить внимание — это смешивание цветов.
В статье специально использовались оригинальные картинки и код. Что бы каждый кому тема интересная смог разобраться… и провести эксперименты в новом Xcode и Instruments.
Смешивание — это операция кадровой визуализации, которая определяет конечный цвет пикселя. Каждый UIView (честно говоря, CALayer) влияет на цвет конечного пикселя, на пример, в случае объединения набора таких свойств, как alpha, backgroundColor, opaque всех вышележащих вью.
Начнем с наиболее используемых свойств UIView, таких как UIView.alpha, UIView.opaque и UIView.backgroundColor.
UIView.opaque — это подсказка для визуализатора, что позволяет рассмотреть изображения в качестве полностью непрозрачной поверхности, тем самым улучшая качество отрисовки. Непрозрачность означает: "Ничего не рисуй под поверхностью». UIView.opaque позволяет пропускать отрисовку нижних слоев изображения и тем самым смешивание цветов не происходит. Будет использоваться самый верхний цвет для вью.
Если значение alpha меньше 1, то значение opaque будет игнорироваться, даже если оно равно YES.
Несмотря на то, что значение непрозрачности по умолчанию — YES, в результате мы получаем смешивание цветов, поскольку мы сделали наше изображение прозрачным, установив значение Alpha меньше 1.
Примечание: Если вы хотите получить точную информацию о реальной производительности, вам необходимо протестировать приложение на реальном устройстве, а не на симуляторе. CPU устройства работает медленнее, нежели процессор вашего Mac устройства, что сильно их отличает.
В меню отладки IOS симулятора вы можете найти пункт «Color Blended Layers”. Отладчик может показать смешанные слои изображения, где несколько полупрозрачных слоев накладываются друг на друга. Несколько слоев изображения, которые нанесены друг на друга с включенной поддержкой смешивания, выделены красным цветом, в то время как несколько слоев изображения, которые отображаются без смешивания, выделены зеленым цветом.
Чтобы использовать инструмент Core Animation, необходимо подключить реальное устройство.
Вы можете найти ‘Color Blended Layers’ тут.
Та же проблема возникает, когда мы пытаемся понять, как изменение альфа-канала может повлиять на прозрачность UIImageView (так же рассмотрим влияние alpha свойства). Давайте использовать категорию для UIImage, чтобы получить другое изображение с настраиваемым alpha -каналом:
Рассмотрим 4 случая:
Смешанные слои изображения отображаются симулятором. Поэтому даже когда свойство alpha для UIImageView имеет значение по умолчанию 1.0, а изображение имеет преобразованный alpha канал, то мы получим смешанный слой.
Официальная документация Apple поощряет разработчиков уделять больше внимания смешиванию цветов:
“Чтобы значительно повысить производительность вашего приложения, уменьшайте количество красного при смешивании цветов. Использование смешивания цветов часто замедляет прокрутку.”
Чтобы создать прозрачный слой, необходимо провести дополнительные вычисления. Система должна смешать верхний и нижний слои, чтобы определить цвет и нарисовать его.
Закадровая визуализация — это прорисовка изображения, которая не может быть выполнена с помощью аппаратного ускорения GPU, вместо него следует использовать процессор CPU.
На низком уровне это выглядит следующим образом: во время прорисовки слоя, которому необходима закадровая визуализация, GPU останавливает процесс визуализации и передает управление CPU. В свою очередь, CPU выполняет все необходимые операции (например, запихивает ваши фантазии в DrawRect :) и возвращает управление GPU с уже прорисованным слоем. GPU визуализирует его и процесс прорисовки продолжается.
Кроме того, закадровая визуализация требует выделения дополнительной памяти, для так называемого резервного хранилища. В то же время, она не нужна для прорисовки слоев, где используется аппаратное ускорение.
Какие же эффекты / настройки приводят к закадровой визуализации? Давайте рассмотрим их:
пользовательские DrawRect: (любые, даже если вы просто зальете фон цветом)
Мы можем легко обнаружить закадровую визуализацию с помощью инструмента Core Animation в Instruments, если включить опцию Color Offscreen-Rendered Yellow. Места, где происходит закадровая визуализация, будут обозначены желтым слоем.
Рассмотрим несколько случаев и протестируем качество работы. Мы постараемся найти оптимальное решение, которое улучшит качество работы и, в то же время, реализует ваше видение хорошего дизайна.
Создадим простую Tableview с нашей кастомной ячейкой и добавим UIImageView и UILabel в нашу ячейку. Помните старые добрые времена, когда кнопки были круглыми? Для достижения этого фантастического эффекта в Tableview нам нужно установить значение YES для CALayer.cornerRadius и CALayer.masksToBounds.
Несмотря на то, что мы достигли желаемого эффекта, даже без Instruments очевидно, что исполнение очень далеко от рекомендованного 60 FPS. Но мы не будем смотреть в хрустальный шар, чтобы найти непроверенные числовые ответы. Вместо этого мы просто проверим производительность с помощью Instruments.
Прежде всего, включите опцию Color Offscreen-Rendered Yellow. Каждая ячейка UIImageView покрыта желтым слоем.
Также стоит проверить работу с Animation and OpenGL ES Driver в Instruments.
Говоря об OpenGL ES Driver tool, что же это нам дает? Чтобы понять как он работает, посмотрим на GPU изнутри. GPU состоит из двух компонентов — Renderer и Tiler. Обязанность рендерера заключается в том, чтобы нарисовать данные, хотя порядок и состав определяются компонентом Tiler. Таким образом, работа Tiler состоит в том, чтобы разделить кадр на пиксели и определить их видимость. Только тогда видимые пиксели передаются рендереру (т.е. процесс визуализации замедляется).
Если значение Renderer Utilization выше ~50%, то это значит, что процесс анимации может быть ограничен скоростью заполнения. Если же Tiler Utilization выше 50%, то это говорит о том, что анимация может быть ограничена геометрически, то есть, на экране, скорее всего, слишком много слоев.
Теперь понятно, что мы должны искать другой подход для достижения желаемого эффекта и, в то же время, повысить производительность. Используйте категорию для UIImage, чтобы закруглить углы, не используя свойства cornerRadius:
И теперь мы изменим реализацию dataSource метода cellForRowAtIndexPath.
Код отрисовки вызывается только один раз, когда объект впервые отображается на экране. Объект кешируется CALayer в и в последствии отображается без дополнительной прорисовки. Не зависимо от того, что он работает медленнее, чем методы Core Animation, этот подход позволяет преобразовать покадровую отрисовку разово.
Прежде чем мы вернемся к измерению производительности, давайте проверим закадровую визуализацию еще раз.
57 – 60 FPS! Нам удалось оптимальным путем увеличить производительность в два раза и снизить Tiler Utilization и Renderer Utilization.
Имейте в виду, что метод -drawRect приводит к закадровой визуализации, даже когда вам просто нужно залить фон цветом.
Особенно, если вы хотите сделать свою собственную реализацию метода — DrawRect для таких простых операций, как установка цвета фона, вместо использования свойства UIView BackgroundColor.
Этот подход нерациональный по двум причинам.
Во-первых: системные UIViews могут осуществлять собственные методы прорисовки, для отображения их содержимого, и очевидно, что компания Apple старается оптимизировать эти процессы. Кроме того, мы должны помнить о резервном хранилище — новое резервное изображение, с размерами пикселов, равным размеру изображения, умноженного на contentsScale, который будет кэшироваться, до следующего обновления изображения.
Во-вторых, если мы избегаем злоупотребления метода — DrawRect, нам не нужно выделять дополнительную память для резервного хранилища и обнулять ее каждый раз, когда мы выполняем новый цикл отрисовки.
Еще одним способом ускорить производительность для закадровой визуализации является использование свойства CALayer.shouldRasterize. Слой визуализируется один раз и кэшируется, до момента, когда нужно этот слой отрисовать сново.
Тем не менее, несмотря на потенциальное повышение производительности, если слой необходимо перерисовывать слишком часто, то дополнительные затраты на кэширования делают его бесполезным, поскольку система будет растрировать слой после каждой прорисовки.
В конце концов, использование CALayer.shouldRasterize зависит от конкретного сценария использования и Instruments.
С помощью теней возможно сделать пользовательский интерфейс более красивым. В iOS очень просто добавить эффект тени:
С включенной „Offscreen Rendering“ мы можем увидеть, что тень добавляет закадровую визуализацию из-за чего CoreAnimation вычисляет прорисовку теней в режиме реального времени, что снижает FPS.
»Позволить Core Animation определять форму тени может повлиять на производительность вашего приложения. Вместо этого, определите форму тени, используя свойство shadowPath для CALayer. При использовании shadowPath, Core Animation использует указанную форму для отрисовки и кэширования теневого эффекта. Для слоев, состояние которых не меняется или меняется редко, это значительно повышает производительность за счет сокращения количества визуализаций выполненных Core Animation".
Поэтому мы должны обеспечить кэширование теней (CGPath) для CoreAnimation, что довольно легко сделать:
Одной строкой кода мы избежали закадровой визуализации и сильно повысили производительность.
Так что, как вы видите, много проблем связанных с производительностью UI можно решить достаточно легко. Одно маленькое замечание – не забудьте измерить производительность до и после оптимизации :)
WWDC 2011 Video: Understanding UIKit rendering
WWDC 2012 Video: iOS App performance: Graphics and Animations
Книга: iOS Core animation. Advanced Techniques by Nick Lockwood
iOS image caching. Libraries benchmark
WWDC 2014 Video: Advanced Graphics and Animations for iOS Apps
Первая проблема, на которую стоит обратить внимание — это смешивание цветов.
От автора перевода
В статье специально использовались оригинальные картинки и код. Что бы каждый кому тема интересная смог разобраться… и провести эксперименты в новом Xcode и Instruments.
Смешивание цветов
Смешивание — это операция кадровой визуализации, которая определяет конечный цвет пикселя. Каждый UIView (честно говоря, CALayer) влияет на цвет конечного пикселя, на пример, в случае объединения набора таких свойств, как alpha, backgroundColor, opaque всех вышележащих вью.
Начнем с наиболее используемых свойств UIView, таких как UIView.alpha, UIView.opaque и UIView.backgroundColor.
Непрозрачность vs Прозрачность
UIView.opaque — это подсказка для визуализатора, что позволяет рассмотреть изображения в качестве полностью непрозрачной поверхности, тем самым улучшая качество отрисовки. Непрозрачность означает: "Ничего не рисуй под поверхностью». UIView.opaque позволяет пропускать отрисовку нижних слоев изображения и тем самым смешивание цветов не происходит. Будет использоваться самый верхний цвет для вью.
Alpha
Если значение alpha меньше 1, то значение opaque будет игнорироваться, даже если оно равно YES.
- (void)viewDidLoad {
[super viewDidLoad];
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(35.f, 35.f, 200.f, 200.f)];
view.backgroundColor = [UIColor purpleColor];
view.alpha = 0.5f;
[self.view addSubview:view];
}
Несмотря на то, что значение непрозрачности по умолчанию — YES, в результате мы получаем смешивание цветов, поскольку мы сделали наше изображение прозрачным, установив значение Alpha меньше 1.
Как проверить?
Примечание: Если вы хотите получить точную информацию о реальной производительности, вам необходимо протестировать приложение на реальном устройстве, а не на симуляторе. CPU устройства работает медленнее, нежели процессор вашего Mac устройства, что сильно их отличает.
В меню отладки IOS симулятора вы можете найти пункт «Color Blended Layers”. Отладчик может показать смешанные слои изображения, где несколько полупрозрачных слоев накладываются друг на друга. Несколько слоев изображения, которые нанесены друг на друга с включенной поддержкой смешивания, выделены красным цветом, в то время как несколько слоев изображения, которые отображаются без смешивания, выделены зеленым цветом.
Чтобы использовать инструмент Core Animation, необходимо подключить реальное устройство.
Вы можете найти ‘Color Blended Layers’ тут.
Alpha канал изображения
Та же проблема возникает, когда мы пытаемся понять, как изменение альфа-канала может повлиять на прозрачность UIImageView (так же рассмотрим влияние alpha свойства). Давайте использовать категорию для UIImage, чтобы получить другое изображение с настраиваемым alpha -каналом:
- (UIImage *)imageByApplyingAlpha:(CGFloat) alpha {
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0f);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect area = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, 0, -area.size.height);
CGContextSetBlendMode(ctx, kCGBlendModeMultiply);
CGContextSetAlpha(ctx, alpha);
CGContextDrawImage(ctx, area, self.CGImage);
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
Рассмотрим 4 случая:
- UIImageView имеет стандартное значение свойства alpha (1.0) и изображение не имеет alpha канала.
- UIImageView имеет стандартное значение свойства alpha (1.0) и у изображении есть alpha канал равный 0.5.
- UIImageView имеет изменяемое значение свойства alpha, но изображение не имеет alpha канала.
- UIImageView имеет изменяемое значение свойства alpha и изображение имеет alpha канал, равный 0.5.
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *image = [UIImage imageNamed:@"flower.jpg"];
UIImage *imageWithAlpha = [image imageByApplyingAlpha:0.5f];
//1st case
[self.imageViewWithImageHasDefaulAlphaChannel setImage:image];
//The 2nd case
[self.imageViewWihImageHaveCustomAlphaChannel setImage:imageWithAlpha];
//The 3d case
self.imageViewHasChangedAlphaWithImageHasDefaultAlpha.alpha = 0.5f;
[self.imageViewHasChangedAlphaWithImageHasDefaultAlpha setImage:image];
//The 4th case
self.imageViewHasChangedAlphaWithImageHasCustomAlpha.alpha = 0.5f;
[self.imageViewHasChangedAlphaWithImageHasCustomAlpha setImage:imageWithAlpha];
}
Смешанные слои изображения отображаются симулятором. Поэтому даже когда свойство alpha для UIImageView имеет значение по умолчанию 1.0, а изображение имеет преобразованный alpha канал, то мы получим смешанный слой.
Официальная документация Apple поощряет разработчиков уделять больше внимания смешиванию цветов:
“Чтобы значительно повысить производительность вашего приложения, уменьшайте количество красного при смешивании цветов. Использование смешивания цветов часто замедляет прокрутку.”
Чтобы создать прозрачный слой, необходимо провести дополнительные вычисления. Система должна смешать верхний и нижний слои, чтобы определить цвет и нарисовать его.
Закадровая визуализация
Закадровая визуализация — это прорисовка изображения, которая не может быть выполнена с помощью аппаратного ускорения GPU, вместо него следует использовать процессор CPU.
На низком уровне это выглядит следующим образом: во время прорисовки слоя, которому необходима закадровая визуализация, GPU останавливает процесс визуализации и передает управление CPU. В свою очередь, CPU выполняет все необходимые операции (например, запихивает ваши фантазии в DrawRect :) и возвращает управление GPU с уже прорисованным слоем. GPU визуализирует его и процесс прорисовки продолжается.
Кроме того, закадровая визуализация требует выделения дополнительной памяти, для так называемого резервного хранилища. В то же время, она не нужна для прорисовки слоев, где используется аппаратное ускорение.
Экранная визуализация
Закадровая визуализация
Какие же эффекты / настройки приводят к закадровой визуализации? Давайте рассмотрим их:
пользовательские DrawRect: (любые, даже если вы просто зальете фон цветом)
- радиус закругления для CALayer
- тень для CALayer
- маска для CALayer
- любой пользовательский рисунок с использованием CGContext
Мы можем легко обнаружить закадровую визуализацию с помощью инструмента Core Animation в Instruments, если включить опцию Color Offscreen-Rendered Yellow. Места, где происходит закадровая визуализация, будут обозначены желтым слоем.
Рассмотрим несколько случаев и протестируем качество работы. Мы постараемся найти оптимальное решение, которое улучшит качество работы и, в то же время, реализует ваше видение хорошего дизайна.
Среда для тестирования:
- Устройство: iPhone 4 with iOS 7.1.1 (11D201)
- Xcode: 5.1.1 (5B1008)
- MacBook Pro 15 Retina (ME294) with OS X 10.9.3 (13D65)
Закругление углов
Создадим простую Tableview с нашей кастомной ячейкой и добавим UIImageView и UILabel в нашу ячейку. Помните старые добрые времена, когда кнопки были круглыми? Для достижения этого фантастического эффекта в Tableview нам нужно установить значение YES для CALayer.cornerRadius и CALayer.masksToBounds.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = NSStringFromClass(CRTTableViewCell.class);
CRTTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:identifier];
cell.imageView.layer.cornerRadius = 30;
cell.imageView.layer.masksToBounds = YES;
cell.imageView.image = [UIImage imageNamed:@"flower.jpg"];
cell.textLabel.text = [NSString stringWithFormat:@"Cell:%ld",(long)indexPath.row];
return cell;
}
Несмотря на то, что мы достигли желаемого эффекта, даже без Instruments очевидно, что исполнение очень далеко от рекомендованного 60 FPS. Но мы не будем смотреть в хрустальный шар, чтобы найти непроверенные числовые ответы. Вместо этого мы просто проверим производительность с помощью Instruments.
Прежде всего, включите опцию Color Offscreen-Rendered Yellow. Каждая ячейка UIImageView покрыта желтым слоем.
Также стоит проверить работу с Animation and OpenGL ES Driver в Instruments.
Говоря об OpenGL ES Driver tool, что же это нам дает? Чтобы понять как он работает, посмотрим на GPU изнутри. GPU состоит из двух компонентов — Renderer и Tiler. Обязанность рендерера заключается в том, чтобы нарисовать данные, хотя порядок и состав определяются компонентом Tiler. Таким образом, работа Tiler состоит в том, чтобы разделить кадр на пиксели и определить их видимость. Только тогда видимые пиксели передаются рендереру (т.е. процесс визуализации замедляется).
Если значение Renderer Utilization выше ~50%, то это значит, что процесс анимации может быть ограничен скоростью заполнения. Если же Tiler Utilization выше 50%, то это говорит о том, что анимация может быть ограничена геометрически, то есть, на экране, скорее всего, слишком много слоев.
Теперь понятно, что мы должны искать другой подход для достижения желаемого эффекта и, в то же время, повысить производительность. Используйте категорию для UIImage, чтобы закруглить углы, не используя свойства cornerRadius:
@implementation UIImage (YALExtension)
- (UIImage *)yal_imageWithRoundedCornersAndSize:(CGSize)sizeToFit {
CGRect rect = (CGRect){0.f, 0.f, sizeToFit};
UIGraphicsBeginImageContextWithOptions(sizeToFit, NO, UIScreen.mainScreen.scale);
CGContextAddPath(UIGraphicsGetCurrentContext(),
[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:sizeToFit.width].CGPath);
CGContextClip(UIGraphicsGetCurrentContext());
[self drawInRect:rect];
UIImage *output = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return output;
}
@end
И теперь мы изменим реализацию dataSource метода cellForRowAtIndexPath.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = NSStringFromClass(CRTTableViewCell.class);
CRTTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:identifier];
cell.myTextLabel.text = [NSString stringWithFormat:@"Cell:%ld",(long)indexPath.row];
UIImage *image = [UIImage imageNamed:@"flower.jpg"];
cell.imageViewForPhoto.image = [image yal_imageWithRoundedCornersAndSize:cell.imageViewForPhoto.bounds.size];
return cell;
}
Код отрисовки вызывается только один раз, когда объект впервые отображается на экране. Объект кешируется CALayer в и в последствии отображается без дополнительной прорисовки. Не зависимо от того, что он работает медленнее, чем методы Core Animation, этот подход позволяет преобразовать покадровую отрисовку разово.
Прежде чем мы вернемся к измерению производительности, давайте проверим закадровую визуализацию еще раз.
57 – 60 FPS! Нам удалось оптимальным путем увеличить производительность в два раза и снизить Tiler Utilization и Renderer Utilization.
Избегайте злоупотребления преопределения метода drawRect
Имейте в виду, что метод -drawRect приводит к закадровой визуализации, даже когда вам просто нужно залить фон цветом.
Особенно, если вы хотите сделать свою собственную реализацию метода — DrawRect для таких простых операций, как установка цвета фона, вместо использования свойства UIView BackgroundColor.
Этот подход нерациональный по двум причинам.
Во-первых: системные UIViews могут осуществлять собственные методы прорисовки, для отображения их содержимого, и очевидно, что компания Apple старается оптимизировать эти процессы. Кроме того, мы должны помнить о резервном хранилище — новое резервное изображение, с размерами пикселов, равным размеру изображения, умноженного на contentsScale, который будет кэшироваться, до следующего обновления изображения.
Во-вторых, если мы избегаем злоупотребления метода — DrawRect, нам не нужно выделять дополнительную память для резервного хранилища и обнулять ее каждый раз, когда мы выполняем новый цикл отрисовки.
CALayer.shouldRasterize
Еще одним способом ускорить производительность для закадровой визуализации является использование свойства CALayer.shouldRasterize. Слой визуализируется один раз и кэшируется, до момента, когда нужно этот слой отрисовать сново.
Тем не менее, несмотря на потенциальное повышение производительности, если слой необходимо перерисовывать слишком часто, то дополнительные затраты на кэширования делают его бесполезным, поскольку система будет растрировать слой после каждой прорисовки.
В конце концов, использование CALayer.shouldRasterize зависит от конкретного сценария использования и Instruments.
Тени & shadowPath
С помощью теней возможно сделать пользовательский интерфейс более красивым. В iOS очень просто добавить эффект тени:
cell.imageView.layer.shadowRadius = 30;
cell.imageView.layer.shadowOpacity = 0.5f;
С включенной „Offscreen Rendering“ мы можем увидеть, что тень добавляет закадровую визуализацию из-за чего CoreAnimation вычисляет прорисовку теней в режиме реального времени, что снижает FPS.
Что говорит Apple?
»Позволить Core Animation определять форму тени может повлиять на производительность вашего приложения. Вместо этого, определите форму тени, используя свойство shadowPath для CALayer. При использовании shadowPath, Core Animation использует указанную форму для отрисовки и кэширования теневого эффекта. Для слоев, состояние которых не меняется или меняется редко, это значительно повышает производительность за счет сокращения количества визуализаций выполненных Core Animation".
Поэтому мы должны обеспечить кэширование теней (CGPath) для CoreAnimation, что довольно легко сделать:
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:cell.imageView.bounds];
[cell.imageView.layer setShadowPath:shadowPath.CGPath];
Одной строкой кода мы избежали закадровой визуализации и сильно повысили производительность.
Так что, как вы видите, много проблем связанных с производительностью UI можно решить достаточно легко. Одно маленькое замечание – не забудьте измерить производительность до и после оптимизации :)
Полезные ссылки и ресурсы
WWDC 2011 Video: Understanding UIKit rendering
WWDC 2012 Video: iOS App performance: Graphics and Animations
Книга: iOS Core animation. Advanced Techniques by Nick Lockwood
iOS image caching. Libraries benchmark
WWDC 2014 Video: Advanced Graphics and Animations for iOS Apps