Оптимизация работы
с данными в мобильных
приложениях
Святослав Иванов, Артём Миронов
«Едадил»
• Агрегируем информацию о специальных предложениях
в магазинах
• Показываем самое выгодное поблизости –
можно использовать поиск, сортировки и сравнение цен
• Собираем все персональные предложения и купоны
• Даём инструменты для отслеживания интересного
• Помогаем составить универсальный список покупок
и поделиться им с семьёй и друзьями
Мобильное приложение
про выгодные повседневные покупки
Рост аудитории
Какие мобильные приложения нравятся?
• Понятный интерфейс
• Отзывчивость в работе
• Плавные переходы, анимация
Сначала обычно всё хорошо
Молодцы
Спасибо вам большое
Отличная, нужная программа!
Отлично
Приложение очень полезное!
Супер, отличная программа!
Красавы
Отличное приложение
Наращиваем функциональность…
После последнего обновления стал тормозить.
Каталоги не загружаются
Испортили после обновления
Перестала грузиться. Очень долго думает...
Жалею, что обновил
Старая версия была удобнее, похоже, сделали
обновление ради обновления. Придётся искать
предыдущие версии
Тормоза!!!
Бог ты мой, да отключите вы это
позиционирование, дистанцию до магазина
в метрах все и так знают. У меня телефон,
а не сервер Пентагона. Мамо мия.
И еще наращиваем…
С каждым обновлением приложение грузится всё дольше
и дольше. А теперь стало вылетать каждые 5 мин. Обидно.
Отвратительное приложение. Не загружается нормально
Фото не загружаются!!!
Верните как было! Приложение тормозит по-страшному!!!
Зря потратил время
Господи, как же оно тормозит! Даже желание пропало
знакомиться с предложенными возможностями! Тормозит
Приложение раньше летало, пользуюсь больше
года! Сейчас постоянно висит и вылетает. Удалила
Хватит это терпеть!
Как мы «разгоняли» Едадил
Видео девайса
с процессом
загрузки?
Проблемы:
• загрузка данных стала занимать вечность
• приложение стало слишком медлительным
Что делать:
• уменьшить время загрузки
• уменьшить лаги интерфейса
Рост контента
Устройства наших пользователей
73%
Android
Топ устройств
Galaxy S3 Neo 2,87%
Galaxy Grand Prime 2,56%
Galaxy A3 2,12%
Galaxy S4 Mini 1,91%
Galaxy A5 1,85%
Galaxy S4 1,60%
Galaxy S3 1,56%
Galaxy J1 1,30%
Galaxy S5 1,23%
Galaxy A5(2016) 1,19%
Топ версий Android
>= 5.0
57%
4.4.x
26%
< 4.4
17%
Структура API Едадила
1. получаем список id каталогов и магазинов поблизости
edapi.net/locationInfo?lat=55.75&lng=37.62
2. получаем список акций (содержимое каталогов)
edapi.net/catalogs?ids=121,122,123
Процесс загрузки данных
Время
locationInfo catalog 1 catalog 2 catalog 3 catalog n Update UI…
locationInfo
catalog 2
catalog 1
catalog 3
catalog n
Update UI Update UI
…


Оптимизация по этапам загрузки
Ожидание ответа сервера Десериализация Обработка данных
• Оптимизация бэкенда
• Кэширование на бэкенде
• Кэширование на клиенте
• Более быстрый формат
данных (Protobuf)
• Оптимизация кода
fun loadLocationInfo(loc: Location) {
api.getLocationInfo(lat, lon)
.subscribeOn(Schedulers.newThread())
.subscribe {
updateLocationInfo(it)
notifyUI()
loadCatalogs(it.catalogsIds)
}
}
Загрузка списка id каталогов поблизости
fun loadLocationInfo(loc: Location) {
api.getLocationInfo(round(lat, 2), round(lon, 2))
.subscribeOn(Schedulers.newThread())
.subscribe {
updateLocationInfo(it)
notifyUI()
loadCatalogs(it.catalogsIds)
}
}
Загрузка списка id каталогов поблизости
fun loadCatalogs(ids: List<Int>) {
val observables = ids.map {
api.getCatalogs(it)
}
Single.zip(observables, { it }).subscribeOn(Schedulers.newThread())
.subscribe {
updateCatalogs(it)
notifyUI()
}
}
Содержимое каталогов
Schedulers.newThread()
fun loadCatalogs(ids: List<Int>) {
val observables = ids.map {
api.getCatalogs(it).subscribeOn(Schedulers.newThread())
}
Single.zip(observables, { it })
.subscribe {
updateCatalogs(it)
notifyUI()
}
}
Schedulers.io()
fun loadCatalogs(ids: List<Int>) {
val observables = ids.map {
api.getCatalogs(it).subscribeOn(Schedulers.io())
}
Single.zip(observables, { it })
.subscribe {
updateCatalogs(it)
notifyUI()
}
}
Какой scheduler выбрать?
• Schedulers.newThread()
• Schedulers.io()
• Schedulers.from(Executors.newFixedThreadPool(5))
• Schedulers.from(
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1)
)
• Schedulers.from(
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
)
Schedulers.from(...)
fun loadCatalogs(ids: List<Int>) {
val observables = ids.map {
api.getCatalogs(it).subscribeOn(scheduler)
}
Single.zip(observables, { it })
.subscribe {
updateCatalogs(it)
notifyUI()
}
}
Форматы передачи данных
• Текстовые форматы — XML, JSON
+ универсальные, с ним умеет работать всё; подходят для многих применений
– сериализация-десериализация не самые быстрые
• Бинарные вариации на тему JSON —BJSON, MessagePack
+ компактнее, чем текстовые; быстрее десериализуются
• Protocol buffers
– есть ограничения по типам данных
Почему мы выбрали Protobuf
• быстрая десериализация
• человекочитаемость нам не нужна
• лучше «обфусцированность», чем у MessagePack
Сравнение форматов сериализации
https://github.com/eishay/jvm-serializers/wiki
• гарантия обратной совместимости
* А потом мы узнали про FlatBuffers
Бенчмарки
Samsung Galaxy S4 (2 cpus), 3G интернет, ~100 каталогов,
минимальная обработка данных, сервер отдает кэш
18
12
11
10
13
6
5
5
Последовательно
Параллельно Schedulers.newThread()
Параллельно Schedulers.io()
Параллельно newFixedThreadPool
JSON (gson) Protobuf (Wire)
Что, если объединить запросы?
edapi.net/catalogs?ids=121,122,123
10
8
9
5
4
6
1 каталог
10 каталогов
Все каталоги
JSON (gson) Protobuf (Wire)
HTTP/2
 OkHttp на Android 5.0+
Пишем полученное на локальную ФС
Время


catalog 1 catalog 2 catalog 3 catalog 4 catalog 5 catalog1.json
catalog2.json
catalog3.json
catalog4.json
catalog5.jsoncatalog 1
catalog 2
catalog 3
catalog 4
catalogs.protobuf
Как хранить полученное на устройстве
• Файлы с исходными данными
+ просто; один алгоритм обработки для локальных и удаленных данных
– только для малого объема; нужно подготавливать данные при каждом запуске
• Файлы с предварительно обработанными данными
+ обрабатываем один раз, потом просто используем
– всё равно приходится считывать в память весь объем данных
• Решения, специфичные для платформы (e.g. Core Data)
+ оперируем готовыми объектами; скорость работы на чтение
– не все гладко с производительностью при записи; в целом — pain in the ass
Как хранить полученное на устройстве
• SQLite
+ структурированное хранилище;
+ гибкий язык запросов;
+ не держим всё в памяти, достаём данные порциями по надобности;
+ предварительную обработку (сортировку, группировку, фильтрацию) осуществляет СУБД;
+ нативная реализация для большинства платформ;
– не всегда удобно работать с объектами — по-хорошему, не помешает ORM.
Как хранить полученное на устройстве
• NoSQL-базы и вариации на тему
+ как правило, быстрее, чем SQLite;
+ документоориентированная структура придает гибкости;
+ часто идут в комплекте с ORM;
+ можно передавать на вход JSON, который сразу ложится в базу;
+ есть решения с синхронизацией данных;
– бывают проблемы с многопоточностью;
– встречаются подводные камни, специфичные для реализации.
Эволюция хранилища Едадила на iOS
SQLite + FMDB
• структура БД аналогична таковой на сервере;
• самописное подобие ORM;
• стабильно и предсказуемо, но все обертки приходится писать самому.
Core Data +
MagicalRecord
• работать с сущностями стало удобнее;
• производительность при записи больших объемов проседает;
• в целом весьма капризное поведение.
Realm
• курсоры, уведомления;
• очень простые миграции;
• производительность радует;
Оптимизация кода - updateCatalogs
class Item {
val id = 1234
val categoryId = 12
val retailerId = 3
val dateEnd = "2016-12-31T01:00:00",
...
}
TimeUtils.compareDates(now, TimeUtils.strToCalendar(item.dateEnd)) >= 0
nowStr.compareTo(item.dateEnd.take(10)) >= 0
Оптимизация кода - updateCatalogs
class Item {
val id = 1234
val categoryId = 12
val retailerId = 3
val dateEnd = "2016-12-31T01:00:00",
...
}
class MyItem {
val id = 1234
val category: Category
val retailer: Retailer
val dateEnd = "2016-12-31T01:00:00",
...
}
class ItemData {
val category: Category
val retailer: Retailer
...
}
model.getItemData(item).category ?
Оптимизация кода — логи
android.util.Log.d("tag", "something " + arg)
Оптимизация кода — логи
Utils.log("tag", "something " + arg)
fun log(message: String) {
if (BuildConfig.DEBUG) {
android.util.Log.d("tag", message)
}
}
Оптимизация кода — логи
Utils.log("tag", "something %s", arg)
fun log(message: String, vararg args: Any?) {
if (BuildConfig.DEBUG) {
android.util.Log.d("tag", message.format(*args))
}
}
Выводы
• Думайте о будущем приложения с самого начала
Выпустить сначала простейшую версию — это правильно.
Но если полетело, то лучше не затягивать с переработкой потенциально слабых мест.
• Если все же затянули с доработками
Обязательно запланируйте отдельный технический релиз (или серию).
Пользователи не всегда жаждут новые фичи, а вот ожившее приложение точно оценят.
• Учитесь на чужих ошибках
Спасибо за внимание!
Какие ещё бывают проблемные места
• Длинные списки — таблицы, коллекции
• Формирование данных для списка
• Конструкция ячейки
• Работа с графикой
• Асинхронная загрузка изображений
• Используем кэш на устройстве
• Целевая подготовка изображений
• Аналитика
• Разумное количество событий, их параметров и логики
Решения по загружаемым данным
• Структурирование передаваемых через API данных
• Не стремимся повторить структуру серверного хранилища
• Не проецируем структуру данных на интерфейс (и наоборот)
• Количество запросов
• Зависимость одних запросов от результата выполнения других
• Объем передаваемых данных
• Приложение-терминал или приложение-СУБД?
• Работа в оффлайне
• Для мобильных приложений — отдельный API
Что используют в Android
• http-клиент (OkHttp + Retrofit)
• JSON (GSON, Jackson, Moshi)
• Protocol Buffers (Wire)
• Другие форматы и библиотеки (MessagePack, BJSON)
• Изображения (Picasso, GLide, UIL, Fresco)
• RxJava

Оптимизация работы с данными в мобильных приложениях / Святослав Иванов, Артём Миронов (Едадил)

  • 1.
    Оптимизация работы с даннымив мобильных приложениях Святослав Иванов, Артём Миронов «Едадил»
  • 2.
    • Агрегируем информациюо специальных предложениях в магазинах • Показываем самое выгодное поблизости – можно использовать поиск, сортировки и сравнение цен • Собираем все персональные предложения и купоны • Даём инструменты для отслеживания интересного • Помогаем составить универсальный список покупок и поделиться им с семьёй и друзьями Мобильное приложение про выгодные повседневные покупки
  • 3.
  • 4.
    Какие мобильные приложениянравятся? • Понятный интерфейс • Отзывчивость в работе • Плавные переходы, анимация
  • 5.
    Сначала обычно всёхорошо Молодцы Спасибо вам большое Отличная, нужная программа! Отлично Приложение очень полезное! Супер, отличная программа! Красавы Отличное приложение
  • 6.
    Наращиваем функциональность… После последнегообновления стал тормозить. Каталоги не загружаются Испортили после обновления Перестала грузиться. Очень долго думает... Жалею, что обновил Старая версия была удобнее, похоже, сделали обновление ради обновления. Придётся искать предыдущие версии Тормоза!!! Бог ты мой, да отключите вы это позиционирование, дистанцию до магазина в метрах все и так знают. У меня телефон, а не сервер Пентагона. Мамо мия.
  • 7.
    И еще наращиваем… Скаждым обновлением приложение грузится всё дольше и дольше. А теперь стало вылетать каждые 5 мин. Обидно. Отвратительное приложение. Не загружается нормально Фото не загружаются!!! Верните как было! Приложение тормозит по-страшному!!! Зря потратил время Господи, как же оно тормозит! Даже желание пропало знакомиться с предложенными возможностями! Тормозит Приложение раньше летало, пользуюсь больше года! Сейчас постоянно висит и вылетает. Удалила
  • 8.
  • 9.
    Как мы «разгоняли»Едадил Видео девайса с процессом загрузки? Проблемы: • загрузка данных стала занимать вечность • приложение стало слишком медлительным Что делать: • уменьшить время загрузки • уменьшить лаги интерфейса
  • 10.
  • 11.
    Устройства наших пользователей 73% Android Топустройств Galaxy S3 Neo 2,87% Galaxy Grand Prime 2,56% Galaxy A3 2,12% Galaxy S4 Mini 1,91% Galaxy A5 1,85% Galaxy S4 1,60% Galaxy S3 1,56% Galaxy J1 1,30% Galaxy S5 1,23% Galaxy A5(2016) 1,19% Топ версий Android >= 5.0 57% 4.4.x 26% < 4.4 17%
  • 12.
    Структура API Едадила 1.получаем список id каталогов и магазинов поблизости edapi.net/locationInfo?lat=55.75&lng=37.62 2. получаем список акций (содержимое каталогов) edapi.net/catalogs?ids=121,122,123
  • 13.
    Процесс загрузки данных Время locationInfocatalog 1 catalog 2 catalog 3 catalog n Update UI… locationInfo catalog 2 catalog 1 catalog 3 catalog n Update UI Update UI …  
  • 14.
    Оптимизация по этапамзагрузки Ожидание ответа сервера Десериализация Обработка данных • Оптимизация бэкенда • Кэширование на бэкенде • Кэширование на клиенте • Более быстрый формат данных (Protobuf) • Оптимизация кода
  • 15.
    fun loadLocationInfo(loc: Location){ api.getLocationInfo(lat, lon) .subscribeOn(Schedulers.newThread()) .subscribe { updateLocationInfo(it) notifyUI() loadCatalogs(it.catalogsIds) } } Загрузка списка id каталогов поблизости
  • 16.
    fun loadLocationInfo(loc: Location){ api.getLocationInfo(round(lat, 2), round(lon, 2)) .subscribeOn(Schedulers.newThread()) .subscribe { updateLocationInfo(it) notifyUI() loadCatalogs(it.catalogsIds) } } Загрузка списка id каталогов поблизости
  • 17.
    fun loadCatalogs(ids: List<Int>){ val observables = ids.map { api.getCatalogs(it) } Single.zip(observables, { it }).subscribeOn(Schedulers.newThread()) .subscribe { updateCatalogs(it) notifyUI() } } Содержимое каталогов
  • 18.
    Schedulers.newThread() fun loadCatalogs(ids: List<Int>){ val observables = ids.map { api.getCatalogs(it).subscribeOn(Schedulers.newThread()) } Single.zip(observables, { it }) .subscribe { updateCatalogs(it) notifyUI() } }
  • 19.
    Schedulers.io() fun loadCatalogs(ids: List<Int>){ val observables = ids.map { api.getCatalogs(it).subscribeOn(Schedulers.io()) } Single.zip(observables, { it }) .subscribe { updateCatalogs(it) notifyUI() } }
  • 20.
    Какой scheduler выбрать? •Schedulers.newThread() • Schedulers.io() • Schedulers.from(Executors.newFixedThreadPool(5)) • Schedulers.from( Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1) ) • Schedulers.from( Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2) )
  • 21.
    Schedulers.from(...) fun loadCatalogs(ids: List<Int>){ val observables = ids.map { api.getCatalogs(it).subscribeOn(scheduler) } Single.zip(observables, { it }) .subscribe { updateCatalogs(it) notifyUI() } }
  • 22.
    Форматы передачи данных •Текстовые форматы — XML, JSON + универсальные, с ним умеет работать всё; подходят для многих применений – сериализация-десериализация не самые быстрые • Бинарные вариации на тему JSON —BJSON, MessagePack + компактнее, чем текстовые; быстрее десериализуются • Protocol buffers – есть ограничения по типам данных
  • 23.
    Почему мы выбралиProtobuf • быстрая десериализация • человекочитаемость нам не нужна • лучше «обфусцированность», чем у MessagePack Сравнение форматов сериализации https://github.com/eishay/jvm-serializers/wiki • гарантия обратной совместимости * А потом мы узнали про FlatBuffers
  • 24.
    Бенчмарки Samsung Galaxy S4(2 cpus), 3G интернет, ~100 каталогов, минимальная обработка данных, сервер отдает кэш 18 12 11 10 13 6 5 5 Последовательно Параллельно Schedulers.newThread() Параллельно Schedulers.io() Параллельно newFixedThreadPool JSON (gson) Protobuf (Wire)
  • 25.
    Что, если объединитьзапросы? edapi.net/catalogs?ids=121,122,123 10 8 9 5 4 6 1 каталог 10 каталогов Все каталоги JSON (gson) Protobuf (Wire)
  • 26.
  • 27.
    Пишем полученное налокальную ФС Время   catalog 1 catalog 2 catalog 3 catalog 4 catalog 5 catalog1.json catalog2.json catalog3.json catalog4.json catalog5.jsoncatalog 1 catalog 2 catalog 3 catalog 4 catalogs.protobuf
  • 28.
    Как хранить полученноена устройстве • Файлы с исходными данными + просто; один алгоритм обработки для локальных и удаленных данных – только для малого объема; нужно подготавливать данные при каждом запуске • Файлы с предварительно обработанными данными + обрабатываем один раз, потом просто используем – всё равно приходится считывать в память весь объем данных • Решения, специфичные для платформы (e.g. Core Data) + оперируем готовыми объектами; скорость работы на чтение – не все гладко с производительностью при записи; в целом — pain in the ass
  • 29.
    Как хранить полученноена устройстве • SQLite + структурированное хранилище; + гибкий язык запросов; + не держим всё в памяти, достаём данные порциями по надобности; + предварительную обработку (сортировку, группировку, фильтрацию) осуществляет СУБД; + нативная реализация для большинства платформ; – не всегда удобно работать с объектами — по-хорошему, не помешает ORM.
  • 30.
    Как хранить полученноена устройстве • NoSQL-базы и вариации на тему + как правило, быстрее, чем SQLite; + документоориентированная структура придает гибкости; + часто идут в комплекте с ORM; + можно передавать на вход JSON, который сразу ложится в базу; + есть решения с синхронизацией данных; – бывают проблемы с многопоточностью; – встречаются подводные камни, специфичные для реализации.
  • 31.
    Эволюция хранилища Едадилана iOS SQLite + FMDB • структура БД аналогична таковой на сервере; • самописное подобие ORM; • стабильно и предсказуемо, но все обертки приходится писать самому. Core Data + MagicalRecord • работать с сущностями стало удобнее; • производительность при записи больших объемов проседает; • в целом весьма капризное поведение. Realm • курсоры, уведомления; • очень простые миграции; • производительность радует;
  • 32.
    Оптимизация кода -updateCatalogs class Item { val id = 1234 val categoryId = 12 val retailerId = 3 val dateEnd = "2016-12-31T01:00:00", ... } TimeUtils.compareDates(now, TimeUtils.strToCalendar(item.dateEnd)) >= 0 nowStr.compareTo(item.dateEnd.take(10)) >= 0
  • 33.
    Оптимизация кода -updateCatalogs class Item { val id = 1234 val categoryId = 12 val retailerId = 3 val dateEnd = "2016-12-31T01:00:00", ... } class MyItem { val id = 1234 val category: Category val retailer: Retailer val dateEnd = "2016-12-31T01:00:00", ... } class ItemData { val category: Category val retailer: Retailer ... } model.getItemData(item).category ?
  • 34.
    Оптимизация кода —логи android.util.Log.d("tag", "something " + arg)
  • 35.
    Оптимизация кода —логи Utils.log("tag", "something " + arg) fun log(message: String) { if (BuildConfig.DEBUG) { android.util.Log.d("tag", message) } }
  • 36.
    Оптимизация кода —логи Utils.log("tag", "something %s", arg) fun log(message: String, vararg args: Any?) { if (BuildConfig.DEBUG) { android.util.Log.d("tag", message.format(*args)) } }
  • 37.
    Выводы • Думайте обудущем приложения с самого начала Выпустить сначала простейшую версию — это правильно. Но если полетело, то лучше не затягивать с переработкой потенциально слабых мест. • Если все же затянули с доработками Обязательно запланируйте отдельный технический релиз (или серию). Пользователи не всегда жаждут новые фичи, а вот ожившее приложение точно оценят. • Учитесь на чужих ошибках
  • 38.
  • 39.
    Какие ещё бываютпроблемные места • Длинные списки — таблицы, коллекции • Формирование данных для списка • Конструкция ячейки • Работа с графикой • Асинхронная загрузка изображений • Используем кэш на устройстве • Целевая подготовка изображений • Аналитика • Разумное количество событий, их параметров и логики
  • 40.
    Решения по загружаемымданным • Структурирование передаваемых через API данных • Не стремимся повторить структуру серверного хранилища • Не проецируем структуру данных на интерфейс (и наоборот) • Количество запросов • Зависимость одних запросов от результата выполнения других • Объем передаваемых данных • Приложение-терминал или приложение-СУБД? • Работа в оффлайне • Для мобильных приложений — отдельный API
  • 41.
    Что используют вAndroid • http-клиент (OkHttp + Retrofit) • JSON (GSON, Jackson, Moshi) • Protocol Buffers (Wire) • Другие форматы и библиотеки (MessagePack, BJSON) • Изображения (Picasso, GLide, UIL, Fresco) • RxJava

Editor's Notes

  • #13 Как оказалось, структура не идеальна: - плохо кэшируется - избыточные данные
  • #16 15
  • #17 16
  • #21 Schedulers.newThread() - число потоков неограниченоSchedulers.io() - число потоков неограниченоSchedulers.computation()Schedulers.from(Executors.newFixedThreadPool(5))Schedulers.from(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1))Schedulers.from(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2))
  • #27 26
  • #33 32
  • #34 33
  • #35 Плохо: в релизной сборке не должно быть логов
  • #36 Плохо: в релизной сборке итоговое сообщение вычисляется