Хабрахабр

Производительность в iOS или как разгрузить main thread. Часть 1

Рассказываем, как разгрузить main thread и какие инструменты лучше подходят для отслеживания стека вызовов в нём. Есть разные приёмы и хитрости, которые помогают оптимизировать работу iOS-приложений, когда одна задача должна выполняться за 16,67 миллисекунд.

Умножив это на 5 миллионов пользователей, ежедневно у нас будет 50 миллионов секунд. «Ребята, давайте представим, что вы сможете сократить время запуска на 10 секунд. Поэтому, если вы сделаете первичную загрузку на 10 секунд быстрее, вы спасёте несколько десятков жизней. За год это составит порядка десяти человеческих жизней. Это действительно стоит того, не правда ли?»

Стив Джобс о производительности (времени запуска компьютера Apple II).

Статья основана на докладе iOS-разрабочика из Fyusion Люка Пархема, с которым он выступил на Международной конференции мобильных разработчиков MBLT DEV в прошлом году.

Билеты дешевле всего сейчас. MBLT DEV 2018 состоится в Москве 28 сентября. Воспользуйтесь этой возможностью сейчас. По доброй традиции, пока Программный комитет отбирает доклады, вы можете купить early bird билеты на конфу. С 29 июня билеты будут стоит дороже.

Потеря кадров

Он же рендерит экран. На main thread выполняется код, который отвечает за ивенты типа касания и работу с UI. Это значит, что задачи должны выполняться за 16,67 миллисекунд (1000 миллисекунд/ 60 кадров). В большинстве современных смартфонов рендеринг происходит с частотой 60 кадров в секунду. Поэтому ускорение работы в Main Thread — важно.

На некоторых устройствах рендеринг происходит ещё быстрее, например, на iPad Pro 2017 частота обновления экрана составляет 120 Гц, поэтому на выполнение операций за один кадр есть всего 8 миллисекунд. Если какая-то операция занимает больше 16,67 миллисекунд, автоматически происходит потеря кадров, и пользователи приложения заметят это при воспроизведении анимаций.

Правило #1

Вертикальная синхронизация гарантирует, что на рендеринг кадра отводится не более 16,67 миллисекунд. CADisplayLink — специальный таймер, который запускается во время вертикальной синхронизации (Vsync). Можно отследить длительность работы приложения и узнать, сколько времени прошло с момента последнего запуска этой функции. В качестве проверки в AppDelegate можно зарегистрировать CADisplayLink в main run loop, и тогда у вас появится дополнительная функция, которая будет выполнять вычисления.

.

Если выполнялось множество различных операций, которые перегружали main thread, то эта функция запускается с задержкой в 100 миллисекунд. Запуск происходит, когда появляется необходимость в рендере. Это значит, что выполнялось слишком много работы, и в этот момент произошла потеря кадров.

Во время загрузки картинок приложение начинает тормозить. Перед вами приложение Catstagram. Похоже, что-то занимает слишком много времени. Мы видим, что частота кадров снизилась в определённый момент, и время загрузки длилось порядка 200 миллисекунд.

.

Пользователи не будут в восторге от подобного, особенно если приложение запускается на более старых устройствах, например iPhone 5 или устаревших моделях iPod и т.д.

Time Profiler

Другие инструменты также полезны, но в конечном итоге в компании Fyusion в 90% случаев мы используем Time Profiler. Полезный инструмент для отслеживания таких проблем — Time Profiler. Обычно проблемы в приложении связаны со ScrollView, участками с текстом и изображениями.

Мы декодируем JPEG-формат с помощью UIImage. Изображения важны. Это происходит не сразу после установки изображения в UIImageView, но можно увидеть этот момент через трассировку в Time Profiler. Они делают это медленно, и мы не можем отследить их производительность напрямую.

Оно имеет значение, когда в приложении большое количество «сложного» текста, например на японском или китайском языках. Форматирование текста – ещё один важный момент. Может потребоваться много времени, чтобы вычислить правильные размеры для строчек с текстом.

Особенно это касается инструмента AutoLayout. Разметка интерфейса также замедляет рендеринг в приложении. Приходится идти на уступки. AutoLayout удобен в использовании, однако он сильно замедляет работу приложения в сравнении с ручной разметкой. Если AutoLayout замедляет работу приложения, возможно, настало время отказаться от него и попробовать другие виды разметок.

Образец трассировки

Можно поменять вид трассировки, взглянуть на неё с точки зрения потоков, процессоров.
В данном примере дерева вызовов можно увидеть, какую работу выполняет CPU. Обычно самое интересное — разделение трассировки на потоки и наблюдение за главным потоком.

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

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

Дерево вызовов

В нём находится основная функция, которая вызывает несколько других функций. Предположим, у нас есть очень простое приложение. Спустя еще одну миллисекунду он делает снимок трассировки. Суть работы Time Profiler заключается в том, что он делает снимки текущего состояния трассировки стека с периодичностью в одну миллисекунду (по умолчанию). Первичная трассировка стека изображена на скриншоте ниже. На ней вызывается основная функция, которая вызывает функцию “foo”, которая вызвала функцию “bar”. Напротив каждой функции указывается число: 1, 1, 1. Эти данные собираются воедино.

Затем, через одну миллисекунду мы получаем ещё один снимок стека.
Это значит, что каждая из этих функций вызывалась один раз. В этот раз он выглядит точно также, поэтому все цифры увеличиваются на 1, и мы получаем 2, 2, 2.

Основная функция вызывает “bar” напрямую.
Во время третьей миллисекунды наш стек вызовов выглядит немного иначе. Далее происходит разделение. Поэтому к главной функции и функции “bar” добавляется ещё по единице, и их значение становится 3. Такое происходило один раз. Иногда главная функция напрямую вызывает “foo”, иногда “bar” вызывается напрямую. Одна функция вызывалась через другую.

Мы видим, что функция “baz” вызывалась дважды. Далее, одна функция вызывала другую, которая вызывала третью функцию. Но эта функция настолько незначительная, что она вызывается быстрее одной миллисекунды.

Он не отображает точное время выполнения той или иной функции. При использовании Time Profiler важно помнить, что он не показывает конкретных временных интервалов. Так как некоторые процессы происходят достаточно быстро, они никогда не отображаются на снимках. Он только сообщает, как часто она появляется на снимках, что дает лишь приблизительное значение длительности выполнения каждой функции.

В примере несколько раз происходила потеря кадров и выполнялись различные процессы.
При переключении вызовов на консольный режим можно увидеть и сравнить все моменты снижения частоты кадров.

Они будут упорядочены по величине выполняемой работы.
Если в macOS нажать alt-click, то это развернет секцию и вложенные секции, а не только выбранную. В 90% случаев на первом месте будет CFRunLoopRun, а после — функции обратного вызова.

Существует бесконечно повторяющийся цикл, и на каждой его итерации запускаются коллбэки. Это приложение целиком основывается на едином цикле исполнения задач Run Loop. Если посмотреть на эти коллбэки, то можно выделить топ узких мест.

Это могут быть renders, image provider, IO. Взглянув на эти вызовы подробнее, вы, скорее всего, не поймёте, что они делают.

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

Если мы посмотрим на данный пример, то увидим здесь значение — 34%. Есть измерители, которые в процентном соотношении показывают, какой объём работы выполняет конкретная функция или операция. После изучения становится понятно, что декодинг JPEG-изображений происходит в main thread, и в большинстве случаев это является причиной потери кадров. Это процесс Apple jpeg_decode_image_all.

Правило #2

Большинство сторонних библиотек (AsyncDisplayKit, SDWebImage и т.д.) могут делать это по умолчанию. В общем случае, декодирование jpeg-изображений стоит делать в фоне. Для этого вы можете написать расширение над UIImage, в котором создадите контекст и вручную отрисуете изображение. Если вы не хотите использовать фреймворки, то можно сделать декодирование вручную.

Она всегда будет возвращать декодированное изображение.
При выполнении этой операции можно вызвать функцию decodeImage не из main thread. Но если вы кешируете данные правильным образом, в системе не возникнет ненужных процессов. Не существует способа проверить, прошло ли декодинг конкретное UIImage-изображение, поэтому всегда придётся пропускать их через этот метод.

Применение класса UIImageView представляется оптимизированным и эффективным. С технической точки зрения это менее эффективно. При таком методе ваши изображения будут декодироваться медленнее. Но он также выполняет аппаратное декодирование, поэтому здесь тоже есть свои минусы. Но есть и хорошая новость — можно декодировать изображение вышеописанным способом не на main thread, а затем вернуться на main thread и настроить интерфейс.

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

Предупреждения о нехватке памяти

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

Если бы я провёл декодинг всех своих JPEG-изображений на стороннем потоке, то в ряде случаев, например, на более старых моделях телефонов, эта система моментально бы сломала приложение. Такая проблема произошла в приложении Fyuse. Происходит следующая ситуация: сначала вы размещаете все эти изображения на сторонних потоках, а затем приложение постоянно даёт сбой. Это случилось бы из-за того, что сторонние потоки задач не реагируют на предупреждение о нехватке памяти от системы, вроде «Эй, удалите ненужные данные!». Если сторонние потоки посылают сигналы main thread о происходящем в системе, то такая проблема не возникнет.

Работа без сбоев

Когда происходит работа со сторонними потоками, в Objective-C можно прописать команду performSelectorOnMainThread:withObject:waitUntilDone:.
По сути, main thread — это очередь, состоящая из процессов. Поэтому, если main thread занят обработкой уведомлений о нехватке памяти, вызов этой команды позволит дождаться момента, когда все уведомления будут обработаны, и только затем начать выполнение сложного процесса загрузки и размещения данных. Благодаря ей, задачи встанут в конец очереди на main thread. DispatchQueue.main.sync освобождает место на main thread. В Swift это выглядит несколько проще.

Мы освободили память и проводим декодинг изображений на сторонних потоках. Вот ещё один пример. У нас всё ещё происходит потеря кадров из-за того, что мы тестируем iPod 5g. Визуально пролистывание ленты стало гораздо лучше. Это одна из самых худших моделей для тестирования из тех, которые до сих пор поддерживают версии iOS 10 и 11.

Однако, остаются процессы, которые продолжают создавать потерю кадров.
Если у вас происходит подобная потеря кадров, ленту все ещё можно просматривать. Есть и другие способы заставить приложение работать быстрее.

Но если у вас есть задачи, которые выполняются сравнительно долго, следует выносить их в фоновые потоки. Конечно, не всегда можно с лёгкостью оптимизировать работу приложения. Убедитесь, что эти задачи не связаны с UI, так как многие классы UIKit не потокобезопасны, то есть вы не можете создавать их в бэкгрануде.

Не скрывайте отображение системных библиотек. Используйте Core Graphics, если вам нужно обрабатывать изображения на стороннем потоке. Помните о предупреждениях о нехватке памяти.

Приглашаем на MBLT DEV 2018

Первые спикеры уже на сайте, а последние early bird ещё в продаже. Приходи 28 сентября на 5-ю Международную конференцию мобильных разработчиков MBLT DEV 2018 в Москве. Купи билеты сейчас по самой низкой цене. Цена на билеты вырастет 29 июня.

Читай о реализации пользовательского интерфейса в iOS, применении кривых Безье и других полезных инструментах во второй части статьи, которую мы опубликуем 28 июня.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»