Как избежать проблем с производительностью с помощью пресета Core Animation, что использовать для трассировки участков кода и с помощью каких функций сократить долю вычислительных операций в приложении с 26% до 0.6% — читай во второй части статьи по материалам доклада Люка Пархэма на прошлогодней конференции MBLT DEV. Первая часть статьи доступна здесь.
Под катом не только полезные советы, но и последние early bird билеты на MBLT DEV 2018 — купить их можно только сегодня.
Core Animation
Core Animation (CA) — пресет в профилировщике, который используется измерения FPS (кадры в секудну), чтобы увидеть, лагают анимации, или нет. Зачастую, даже если найдены проблемные места приложения, трудности с производительностью остаются. Причина в том, что при работе с UI-фреймворками, используется UIView, но под капотом создается экземпляр CATransaction (или система делает это сама), и все эти инструкции передаются серверу на обработку. Сервер для рендеринга отвечает за создание анимации. Если анимация выполняется с помощью UIView, например, классового метода animate(withDuration:animations:)
, она обрабатывается the render server, который считается отдельным потоком и работает со всеми анимациями в приложении.
Можно заставить the render server работать медленно, чтобы он не появлялся в Time Profiler, но он всё равно будет тормозить работу приложения. Вот как это выглядит:
Вверху расположен датчик частоты кадров. Внизу — самая важная часть — параметры отладки. Есть два ключевых и при этом лёгких в настройке параметра. Первый — color blended layers
. Исправить его довольно просто. На самом деле, проблемы возникают даже в iMessage, всеми любимом приложении от Apple.
Красным цветом обозначены участки с белым фоном. Они накладываются на другой белый фон, и по каким-то причинам отображаются прозрачными. Получается, что программа смешивает эти цвета друг с другом — белый с белым, и в результате снова получается белый цвет. В итоге для каждого пикселя, который обозначен красным, проводятся лишние вычисления, которые не приносят никакой пользы — получается белый фон.
Правило #3
Делайте слои непрозрачными, когда это возможно — при условии, что у них одинаковые цвета, которые накладываются друг на друга. У слоя есть свойство opacity
, которому и надо выставить единицу. Всегда проверяйте, что фоновый цвет задан и он непрозрачный.
Offscreen rendering
Следующий параметр — внеэкранный рендеринг. Если включить эту функцию, то участки будут выделены жёлтым цветом.
Удобство Core Animation заключается в возможности просматривать другие приложения. Можно включить параметры, запустить приложение и увидеть, что происходит не так. На экране в Instagram вверху есть небольшие жёлтые кружочки, в которых показываются сториз пользователей.
Например, на iPhone 6s они загружаются довольно медленно. А если посмотреть на них на iPhone 5 или на старой модели iPod, загрузка будет идти ещё медленнее. Это происходит из-за того, что внутрисистемный рендеринг гораздо хуже alpha blending. Он нагружает графический процессор. В итоге устройству приходится постоянно выполнять дополнительные вычисления между графическим и центральным процессором, поэтому возникают дополнительные задержки, которые в большинстве случаев можно избежать.
Правило #4
Не используйте параметр cornerRadius
. Применение viewLayer.cornerRadius
приводит к offscreen rendering. Вместо этого можно использовать класс UIBezierPath
, а также что-то похожее на работу с CGBitMap, как было ранее с декодированием JPEG. В этом случае используется UIGraphics context
.
Это ещё один метод экземпляра класса UIImage. Здесь можно задавать размер и делать закруглённые углы. Bezierpath
применяется для выделения области изображения. Затем фрагмент возвращается из UIImageContext. Таким образом, получаем готовое изображение с закруглёнными углами вместо того, чтобы закруглять рамки, в которые будет вставляться изображение.
На гифке — страница Твиттера. Изображение показано в режиме реального времени. Предполагается, что страница откроется и выдаст информацию, но текст и прочие элементы экрана прошли через внеэкранный рендеринг, поэтому анимации двигаются очень медленно.
Правило #5
Свойство класса shouldRasterize
из набора СALayer позволяет кэшировать текстуры, которые прошли рендеринг. Лучше его избегать. Если shouldRasterize
не использовался определённое количество миллисекунд, он оставит кэш и будет предлагать рендеринг каждого кадра. Так что это создаёт больше проблем, чем пользы.
Ускоряйтесь
- Избегайте внеэкранного рендеринга и смешения прозрачных слоёв.
- Используйте их только тогда, когда без них не обойтись. Внеэкранный рендеринг появляется из-за наличия теней, закругления углов и так далее.
- Делайте изображения непрозрачными.
- Не используйте cornerRadius, используйте кривые Безье.
- При работе с текстом не используйте свойство layer.shadow, замените на NSShadow.
Activity trace
Трассировка активности похожа на то, что делает Time Profiler, но в меньшем масштабе. Она позволяет рассмотреть потоки и их взаимодействие между собой.
Правило #6
Используйте System Trace для отслеживания периода времени для конкретных событий. Можно придумать способ для трассировки ивентов или участков кода и увидеть, сколько времени они занимают в реальной работе приложения. System Trace даёт информацию о том, что происходит в системе:
- “Sing Post” сигнализирует о том, что происходит что-то важное.
- Отметки — это единичные события, за которыми стоит следить, например — появление анимации.
- По интервалу события можно отследить, сколько времени занимает декодирование.
Итак, программа показывает, как код взаимодействует с остальными элементами системы.
На скрине — образец создания шаблона трассировки системы:
- 1 — загрузка изображения
- 2 — декодирование изображения
- 3 — наклон анимаций.
Нужно добавить несколько опций, чтобы понять, какой цвет получится. Как правило, им присваиваются цифры, например 1 или 2, и они становятся красными или зелёными, в зависимости от настройки. На Objective-C нужно прописать команду #import
для kdebug_signpost
. В Swift они уже доступны.
Затем нужно вызвать эту функцию, прописав kdebug_signpost
или kdebug_signpost_start
и kdebug_signpost_end
.
Указываем 3 события вместе с числами, которые написаны в коде, и ключевой элемент для каждого конкретного события. Последнее число обозначает цвет. Например, 2 — красный.
Далее идут важные события, особые объекты. Упрощенная схема описана в тестовом проекте Люка на Swift.
На скрине видно, как это будет выглядеть при запуске трассировки. Сначала, когда приложение будет запущено, программа не будет выдавать информацию, но как только приложение даст сбой, мы увидим расчёты.
Загрузка изображений занимает порядка 200 миллисекунд. Затем идёт декодинг, который занимает около 40 миллисекунд. Полезно отслеживать эти данные, если в приложении происходит множество операций. Можно перечислить в программе события, а затем наблюдать и получать информацию о том, сколько времени требуется на их выполнение, и как они взаимодействуют между собой.
Дополнительные инструменты
На скрине — AR-проект по замедленной съёмке смартфона. Приложение похоже на Snapchat. В нём использован эффект ретуширования фото, и для каждого кадра на него тратилось 26,4% от всех вычислительных операций. Из-за этого камера снимала медленно, около 10 кадров в секунду. При изучении видим, что самая верхняя строчка выполняла основную часть работы. Она отвечала за активное отправление данных. Если исследовать причины проседания производительности, можно заметить, что дело в интенсивном использовании NSDispatchData
.
Этот подкласс NSData
всего лишь получает последовательность байт с помощью метода getBytes
в определённом интервале и передаёт её в другое место. Кажется, что это так просто, однако всё, что этот метод делает внутри, занимает 18% из 26% вычислений.
Правило #1
Используйте
NSData
и getBytes
. Это несложная операция на Objective-C, но если она является причиной проблем в системе, следует переключиться на более низкоуровневую функцию на plain C. Так как проблема связана с получением плавающих значений, можно воспользоваться функцией копирования памяти. С её помощью можно переместить фрагмент данных в другое место. Это сильно экономит вычислительные мощности устройства.В NSData происходит много операций. В примере исходный код выделен красным. С помощью функции
getBytes
можно перевести данные в числа с плавающей точкой. Метод с копированием памяти — практически то же самое. Он довольно простой и выполняет на порядок меньше операций.
Если поменять подход и запустить приложение снова, видно, что доля вычислительных операций, потраченных на изменение фото, снизилась с 26% до 0,6%. И это благодаря изменению лишь одной строки кода о копировании памяти. Частота кадров при этом значительно увеличилась.
Правило #2
Избегайте наложения пикселей друг на друга при создании приложения, в котором есть рендеринг.
В большинстве случаев это будет происходить с частотой выше 60 кадров в секунду. Применяя CADisplayLink
, можно замедлить обновление UI. Существует параметр preferredFramesPerSecond
. Он применяется только для iOS 10 и более поздних версий. Для старых систем придётся проделать это вручную. При работе в новых версиях iOS можно установить желаемое количество кадров в секунду. В большинстве случаев — 15 кадров в секунду или около того, чтобы не тратить вычислительную мощность впустую и не добавлять приложению лишней работы.
Правило #3
При работе с Objective-C полезно применять кэширование IMP pointers (указателей на имплементацию методов). Когда в Objective-C вызывается метод
under the hood
, происходит вызов функции objc_msgSend()
. Если при трассировке видно, что вызов занимает большой отрезок времени, можно от него избавиться. По сути, это хранилище кэша с указателями на функцию, им можно дать названия некоторых методов. Поэтому вместо того, чтобы каждый раз производить этот поиск, стоит делать кэширование следующим образом: поместить указатели функций в кэш и вызывать их напрямую. Как правило, это происходит в два раза быстрее.
Если нет указателя, то метод вызывается при помощи команды methodForSelector. Для вызова этого метода помощи обычной функции вызова нужно внести название объекта в селектор, который выдаёт результаты поиска. Возможно, это не самый удобный, но зато быстрый способ.
Правило #4
Не применяйте ARC (automatic reference counting). ARC добавляет много лишней работы.
Компилятор при включенном ARC сам разбрасывает retain
/release
в нужных местах. Однако, если есть места, где retain
и release
занимают слишком много времени, присмотритесь к отказу от ARC. Делайте это в том случае, если оптимизация необходима, так как это займёт много времени.
Иногда стоит отказаться даже от Swift, особенно если производительность приложения чувствительна к изменениям. У Swift есть несколько действительно классных особенностей, но при этом для выполнения даже небольших задач он требует прописывать много строчек кода, чтобы добиться высокого уровня функциональности.
Полезные материалы
Первая часть статьи доступна здесь. Для дальнейшего погружения в тему Люк Пархэм рекомендует почитать книгу “iOS and MacOS: Performance Tuning” и посмотреть его туториалы.
Видеозапись доклада Люка на MBLT DEV 2017 теперь в открытом доступе:
Ещё больше знаний и советов на MBLT DEV 2018
Первые спикеры объявлены:
- John C. Fox из Netflix расскажет о высококачественной локализации, работе с агрессивными сетевыми условиями и A/B-тестировании.
- Krzysztof Zablocki, The New York Times, готовит доклад о паттернах в iOS.
- Laura Morinigo, Google Developer Expert, расскажет о лучших Firebase-практиках.
Последние early bird билеты улетят сегодня.
a_boy
в моменте про cornerRadius напрямую вызывается метод draw(), так делать не рекомендуется.