Оптимизация работы с данными в мобильных приложениях /...

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

Upload: ontico

Post on 06-Jan-2017

126 views

Category:

Engineering


2 download

TRANSCRIPT

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

• Агрегируем информацию о специальных предложениях в магазинах

• Показываем самое выгодное поблизости – можно использовать поиск, сортировки и сравнение цен

• Собираем все персональные предложения и купоны• Даём инструменты для отслеживания интересного• Помогаем составить универсальный список покупок

и поделиться им с семьёй и друзьями

Мобильное приложение про выгодные повседневные покупки

Рост аудитории

Какие мобильные приложения нравятся?• Понятный интерфейс• Отзывчивость в работе• Плавные переходы, анимация

Сначала обычно всё хорошо

МолодцыСпасибо вам большое

Отличная, нужная программа!

ОтличноПриложение очень полезное!

Супер, отличная программа!

КрасавыОтличное приложение

Наращиваем функциональность…После последнего обновления стал тормозить.

Каталоги не загружаются

Испортили после обновленияПерестала грузиться. Очень долго думает...

Жалею, что обновилСтарая версия была удобнее, похоже, сделали обновление ради обновления. Придётся искать предыдущие версии

Тормоза!!!Бог ты мой, да отключите вы это позиционирование, дистанцию до магазина в метрах все и так знают. У меня телефон, а не сервер Пентагона. Мамо мия.

И еще наращиваем…С каждым обновлением приложение грузится всё дольше и дольше. А теперь стало вылетать каждые 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.057%4.4.x

26%

< 4.418%

Структура 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 каталогов, минимальная обработка данных, сервер отдает кэш

Последовательно

Параллельно Schedulers.newThread()

Параллельно Schedulers.io()

Параллельно newFixedThreadPool

18

12

11

10

13

6

5

5

Protobuf (Wire) JSON (gson)

Что, если объединить запросы?edapi.net/catalogs?ids=121,122,123

1 каталог

10 каталогов

Все каталоги

10

8

9

5

4

6

Protobuf (Wire) JSON (gson)

HTTP/2 OkHttp на Android 5.0+

Пишем полученное на локальную ФС

Время

catalog 1 catalog 2 catalog 3 catalog 4 catalog 5 catalog1.jsoncatalog2.jsoncatalog3.jsoncatalog4.jsoncatalog5.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• курсоры, уведомления;• очень простые миграции;• производительность радует;

Оптимизация кода - updateCatalogsclass 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

Оптимизация кода - updateCatalogsclass 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