Аналитика — это неотъемлемая часть современного мобильного приложения. Аналитика позволяет собрать информацию о пользователе, чтобы развивать и совершенствовать продукт.
Часто сбор информации снижает производительность приложения. Процесс дополнительно нагружает CPU и память, а это высокая цена. Медленная работа приложения может стать причиной негативных отзывов пользователей, снизить рейтинг и привести к потере аудитории.
С такой проблемой столкнулась и наша команда Android-разработчиков во время работы над очередным проектом, который был связан с новостями. Нам нужно было регистрировать отображение каждой новости в списке.
Получив задание на сбор аналитики, команда достаточно быстро показала результат. Триггером для генерации события выбрали метод onViewAttachedToWindow. Все вроде хорошо, но при быстром скролле интерфейс заметно подвисал — что-то пошло не так. Проблему нужно было решать.
Каждый воспринимает подвисание по-разному, так что мы нуждались в фактах и доказательствах. Чтобы измерить подвисание, выбрали показатель FPS (Frames Per Second), а для измерения показателя?—?FPS-meter TinyDenser. После подключения утилиты команда получила объективное подтверждение проблемы — показатель падал, временами достаточно заметно: меньше 30 кадров в секунду, запись экрана с включенной утилитой показана на Рисунке 1.
Рисунок 1. Запись экрана до оптимизации
А если отложить отправку событий, пока пользователь скроллит список? «Хммм», — подумала команда, и решила создать очередь из событий и отправлять их после того, как скролл остановится. Тут все просто: добавляем OnScrollListener в RecyclerView и ждем, пока newState будет ровным SCROLL_STATE_IDLE?—?задача частично решена.
Следующий шаг — реализовать накопление событий и их отправку.
Для управления очередью создали класс, который отвечал за добавление событий в очередь и отправку аналитики в сервис. Для периодической отправки выбрали ScheduledExecutorService с одним потоком, который запускал Runnable каждую секунду, время подобрали эмпирическим способом.
Это сразу дало результаты, значительный прирост FPS. В принципе, задача была решена, на Рисунке 2 видим результат работы приложения после второй попытки. Но осталась одна задачка — события отправлялись в сервис, что приводило к частому генерированию объектов класса Intent, а это дополнительно нагружало GC и доставляло «приятности» в виде stop-the-world пауз.
Рисунок 2. Запись экрана после второй попытки
«Отправка не одного события за раз, а списка событий — еще одна чудесная идея», — подумала команда. После короткой доработки класса команда реализовала отправление событий списком и получила стабильные значения показателя на уровне 55–60 кадров в секунду (Рисунок 3).

Рисунок 3. Запись экрана после третий попытки
Тривиальная задача по сбору аналитики превратилась в интересный и познавательный процесс, в ходе которого команда прокачала свои навыки в исследовании проблем производительность приложений и поиска путей их решения. Надеюсь, наш опыт пригодится как начинающим так и опытным разработчикам.
Наша команда остановилась на третьем варианте, но это не единственное, что можно было применить.
В текущей реализации при срабатывании метода onViewAttachedToWindow происходит превращение новости в объект события аналитики, соответственно, создание нового объекта. Одно из возможных решений — отложить конвертацию до момента отправки: накапливать в очереди не события, а сами элементы списка. Тогда конвертация будет происходить, когда скролл будет в режиме SCROLL_STATE_IDLE. Также для событий можно было создать пул объектов. В комплексе эти действия могут дать дополнительный прирост производительности приложения.
Часто сбор информации снижает производительность приложения. Процесс дополнительно нагружает CPU и память, а это высокая цена. Медленная работа приложения может стать причиной негативных отзывов пользователей, снизить рейтинг и привести к потере аудитории.
С такой проблемой столкнулась и наша команда Android-разработчиков во время работы над очередным проектом, который был связан с новостями. Нам нужно было регистрировать отображение каждой новости в списке.
Попытка №1
Получив задание на сбор аналитики, команда достаточно быстро показала результат. Триггером для генерации события выбрали метод onViewAttachedToWindow. Все вроде хорошо, но при быстром скролле интерфейс заметно подвисал — что-то пошло не так. Проблему нужно было решать.
Каждый воспринимает подвисание по-разному, так что мы нуждались в фактах и доказательствах. Чтобы измерить подвисание, выбрали показатель FPS (Frames Per Second), а для измерения показателя?—?FPS-meter TinyDenser. После подключения утилиты команда получила объективное подтверждение проблемы — показатель падал, временами достаточно заметно: меньше 30 кадров в секунду, запись экрана с включенной утилитой показана на Рисунке 1.

Попытка №2
А если отложить отправку событий, пока пользователь скроллит список? «Хммм», — подумала команда, и решила создать очередь из событий и отправлять их после того, как скролл остановится. Тут все просто: добавляем OnScrollListener в RecyclerView и ждем, пока newState будет ровным SCROLL_STATE_IDLE?—?задача частично решена.
class IdleStateScrollListener(private val analyticsFacade: AnalyticsFacade)
: RecyclerView.OnScrollListener() {
fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
analyticsFacade.setPending(newState != RecyclerView.SCROLL_STATE_IDLE)
}
}
Следующий шаг — реализовать накопление событий и их отправку.
Для управления очередью создали класс, который отвечал за добавление событий в очередь и отправку аналитики в сервис. Для периодической отправки выбрали ScheduledExecutorService с одним потоком, который запускал Runnable каждую секунду, время подобрали эмпирическим способом.
Это сразу дало результаты, значительный прирост FPS. В принципе, задача была решена, на Рисунке 2 видим результат работы приложения после второй попытки. Но осталась одна задачка — события отправлялись в сервис, что приводило к частому генерированию объектов класса Intent, а это дополнительно нагружало GC и доставляло «приятности» в виде stop-the-world пауз.

Попытка №3
«Отправка не одного события за раз, а списка событий — еще одна чудесная идея», — подумала команда. После короткой доработки класса команда реализовала отправление событий списком и получила стабильные значения показателя на уровне 55–60 кадров в секунду (Рисунок 3).

Рисунок 3. Запись экрана после третий попытки
Выводы
Тривиальная задача по сбору аналитики превратилась в интересный и познавательный процесс, в ходе которого команда прокачала свои навыки в исследовании проблем производительность приложений и поиска путей их решения. Надеюсь, наш опыт пригодится как начинающим так и опытным разработчикам.
Можно ли было сделать еще что-то?
Наша команда остановилась на третьем варианте, но это не единственное, что можно было применить.
В текущей реализации при срабатывании метода onViewAttachedToWindow происходит превращение новости в объект события аналитики, соответственно, создание нового объекта. Одно из возможных решений — отложить конвертацию до момента отправки: накапливать в очереди не события, а сами элементы списка. Тогда конвертация будет происходить, когда скролл будет в режиме SCROLL_STATE_IDLE. Также для событий можно было создать пул объектов. В комплексе эти действия могут дать дополнительный прирост производительности приложения.
Комментарии (6)
sankarsana
23.06.2018 12:59Спасибо за статью! Познавательно!
Вы писали о частом создании intent.
Вопрос такой, на сколько затратна операция, создания интента?
funca
23.06.2018 17:08С такой проблемой столкнулась и наша команда Android-разработчиков во время работы над очередным проектом, который был связан с новостями. Нам нужно было регистрировать отображение каждой новости в списке.
Расскажите еще немного о бизнес-задаче. В чем практический смысл трекать появление новостей на экране при скроллинге со скоростью 55-60 fps?idg_chernyshov Автор
23.06.2018 20:30Задача стояла регистрировать факт отображения preview каждой новости на экране, не зависимо от скорости скрола ленты, как именно эта аналитика потом была бы использована я могу только предполагать, возможно потом применялись алгоритмы машинного обучения для анализа и дальнейшего формирования наиболее релевантной выдачи.
Почему 60 fps — мы стремились получить максимальную производительность для достижения планости работы приложения. Лично меня очень напрягает когда я хочу быстро пролистать контент, а в этот момент приложение тормозит, по этому я не хотел отдавать приложение так как есть.
Dwite
А каким сервисом аналитики пользуетесь? Просто большинство современных сервисов вроде Fabric делают все очень оптимизировано и из коробки уже очень давно)
idg_chernyshov Автор
Мы использовали несколько сервисов GoogleAnalytics и AppsFlyer. Сервисы самособой под капотом умеют агрегировать и отправлять ивенты порциями, у нас была проблема в том что еще до отправки генерация происходила слишком часто и нужно было отложыть саму отправку в сервис.