[Видео] Как мы провели очередной Android Paranoid
Android почти исполнилось десять лет.
Мы решили отметить это праздничным чаепитием со всеми, кто пришел в питерский офис Яндекса на второй митап Android Paranoid. Сказано — сделано. К нашему сожалению, маршмеллоу, шоколадное печенье и желейные бобы закончились еще 28 марта.
Вместо них — доклады, записанные на видео, и короткая выжимка полезной информации для Android-разработчиков. Под катом о том,
- что происходит после нажатия на иконку приложения;
- как перевести приложение на Kotlin и уместиться в 300 строк кода;
- как менялись инструменты фоновой работы в Android;
- как быстро получить анимации в RecyclerView.
Про анимации в RecyclerView
Данил Терновых из Яндекс.Денег рассказал о том, как быстро и без затрат получить анимации в RecyclerView.
Для тех, кто хочет попробовать их в работе — демо на GitHub. Подробности реализации — в видео.
«Что происходит после тапа на иконку приложения,
И даже чуть раньше?», — рассказал Владимир Теблоев из Сбербанк-Технологий. Рекомендуем посмотреть видео если вы думали, что вся жизнь в Android ограничивается вьюхами и активити, и ничего не знали про работу ядра, загрузчика, далвик, и все время задавались вопросом — зачем андроиду зигота? Для заинтересовавшихся — выжимка в трех эпизодах.
Эпизод 1 — Уровни системы и Zygote
Давным-давно инженеры молодой мобильной ОС спроектировали четыре уровня работы системы:
- ядро с драйверами и Binder;
- корневые библиотеки, библиотеки ОС и Dalvik;
- Application Framework, неизменяемые компоненты системы — контент-провайдеры, активити-менеджеры и т.д.;
- Пользовательские приложения.
Чуть менее давно Dalvik превратился в Android Runtime, но сути процесса это не изменило — после тапа на иконку Launcher получает сигнал, передает его в менеджер активностей, тот передается в Zygote, а она создает новое приложение.
Zygote — демон, который запускается при старте системы и инициализирует первичную виртуальную машину. Zygote позволяет создавать процессы для любых приложений в Android, клонируя себя и корневые библиотеки, которые необходимы для запуска всех приложений. Так экономятся время и память, потому что в первичном экземпляре Zygote уже инициализированы все нужные библиотеки. Останется только использовать Copy-on-Write и изменить ProcessID.
Слева — первичный экземпляр, в середине — Zygote, отвечающая за компоненты Android, справа — наше приложение.
Эпизод 2 — Жизненный цикл и взаимодействие процессов
В Android существуют пять типов процессов и три приоритета, которые им назначаются.
Критический приоритет назначается активным процессам — тем, с которыми пользователь взаимодействует прямо сейчас. Это может быть открытая активити или музыкальный плеер в UI.
Высокий приоритет обычно получают видимые процессы, например, активити, перекрытые другими. Если на активные процессы будет не хватать памяти, видимый процесс завершится. К тому же приоритету относятся служебные процессы, с которыми пользователь не взаимодействует — они отвечают за загрузку данных, синхронизацию и т.д.
Процессы низкого приоритета относятся к уже остановленным активити. Android сортирует такие процессы в порядке запуска и хранит в памяти, чтобы завершать их в порядке от старых к новым. Последняя категория — «зомби-процессы» — в них может быть инициализирован какой-то поток, но все компоненты жизненного цикла уже уничтожены.
Основной способ взаимодействия процессов — IPC через Binder. Это драйвер, через который работают все корневые структуры Android. Модель взаимодействий — на схеме ниже.
Предположим, что активити в процессе А должна получить данные из другого процесса. Метод Foo () обращается к Binder, который, в свою очередь, сериализует и упаковывает входные данные и передает целевому процессу для обработки. Затем нужные данные десериализуются, процесс Б что-то с ними делает и выполняет операции в обратном порядке.
Отдельно Владимир рассказал обо всех этапах создания активностей в Android. Все детали — в видео.
«Пользователь хочет 60 FPS,
Для этого и нужна фоновая работа».
Владимир Иванов из EPAM уже семь лет пишет под Android и iOS и успел похоронить Windows Phone. Владимир рассказал об эволюции инструментов для выполнения задач вне главного потока на Android. Речь о цепочке — AsyncTask, Loaders, Executors, EventBus, RxJava, Coroutines в Kotlin.
В докладе очень много примеров, здесь — малая часть.
Итерация 1 — AsyncTask
Допустим, мы пишем приложение, которое показывает прогноз погоды. Последовательность действий примерно следующая:
- Определяем метод DoInBackground ();
- С помощью http-клиента делаем запрос на сервер;
- Получаем и парсим ответ;
- Показываем пользователю.
На последнем этапе возникает сложность — мы не можем просто взять и вернуть ответ с фона на UI-поток. Если UI одновременно использует другие потоки, приходится продумывать костыли и сложные блокировки. Чтобы этого не делать, разработчики интерфейсов рекомендуют обновляться только с UI-потока.
Соответственно, нужен способ выполнить что-то на UI. В AsyncTask для этого используется метод onPostExecute, его и используем.
public class LoadWeatherForecastTask
extends AsyncTask < String,
Integer,
Forecast > {
public void onPostExecute(Forecast forecast) {
mTemperatureView.setText(forecast.getTemp());
}
}
У этого подхода одна большая проблема и несколько минусов — AsyncTask умерли, за исключением production-проектов, которым больше пяти лет.
А минусы такие:
1) Много кода для сетевых запросов;
2) AsyncTask не знают ничего про жизненный цикл активностей и потенциально ведут к утечкам памяти;
3) При смене конфигурации на лету (например, экран перевернулся) нужно перевыполнить запрос.
Итерация 2 — Loaders
С Android 3.0 пришли Loaders — команда Android придумала их, чтобы решить проблемы AsyncTask.
class WeatherForecastLoader(context: Context) : AsyncTaskLoader < WeatherForecast > (context) {
override fun loadInBackground() : WeatherForecast {
try {
Thread.sleep(5000)
} catch(e: InterruptedException) {
return WeatherForecast("", 0F, "")
}
return WeatherForecast("Saint-Petersburg", 20F, "Sunny")
}
}
В частности, речь о повторном использовании результата при смене конфигурации. Проблема решается так:
1) LoaderManager, привязанный к активности, хранит ссылки на несколько Loader в специальной структуре;
2) Активность сохраняет все LoaderManager внутри NonConfigurationInstances;
3) При создании новой активности система передает в нее данные из NonConfigurationInstances;
4) Активность восстанавливает LoaderManager со всеми Loader.
Минусы:
1) Все еще много кода;
2) Интерфейсы все еще сложные, а классы все еще абстрактные;
3) Loaders — платформенный API Android, а значит, их нельзя переиспользовать на чистой Java.
Итерация 3 — EventBus и ThreadPoolExecutors
С появлением ThreadPoolExecutors передача данных с фона на UI стала выглядеть так:
1) Заводим класс Background, а в нем — переменную Service;
2) Инициализируем этот класс в ScheduledThreadPoolExecutor с нужным нам размером;
3) Пишем вспомогательные методы, которые делают класс runnable или callable.
public class Background {
private val mService = ScheduledThreadPoolExecutor(5)
fun execute(runnable: Runnable) : Future < *>{
return mService.submit(runnable)
}
fun < T > submit(runnable: Callable < T > ) : Future < T > {
return mService.submit(runnable)
}
}
Кроме выполнения на фоне, все еще нужно возвращать результат на UI. Для этого пишем handler и метод, который что-то постит на UI-треде.
public class Background {…private lateinit
var mUiHandler: Handler
public fun postOnUiThread(runnable: Runnable) {
mUiHandler.post(runnable)
}
}
Не весь UI должен знать, что какой-то конкретный метод выполнился. Чтобы разделить ответственность, придумали EventBus. Это способ передачи событий из фонового потока на UI, при котором на общую шину подключены несколько слушателей, которые и обрабатывают эти события.
Есть несколько готовых реализаций EventBus. Некоторые из них — Google Guava, Otto и GreenBot Eventbus.
Из минусов:
- Источник данных о событии ничего не знает о том, как оно должно обрабатываться;
- По опыту докладчика, через некоторое время код с EventBus становится невозможно поддерживать.
Итерация четвертая — RxJava, или «Хватит это терпеть!»
Кто-то должен был придумать удобный инструмент для фоновой работы. В итоге у нас есть RxJava — большой фреймворк для работы с потоками событий.
Предположим, мы пишем код, который должен авторизовываться на GitHub. Нужно завести по методу на каждую нужную операцию (в нашем случае — логин и получение списка репозиториев).
interface ApiClientRx {
fun login(auth: Authorization)
: Single < GithubUser >
fun getRepositories(reposUrl: String, auth: Authorization)
: Single < List < GithubRepository >>
}
Результатом выполнения будет Single — поток из нуля или одного события. Итог работы — интерфейс из двух методов, которые возвращают все, что нужно пользователю.
Минусы:
- Крутая кривая обучения, учить долго и сложно;
- Много операторов, разницу между которыми сложно понять;
- На простой код из двух запросов и двух операторов создается около 20 объектов, что ведет к избыточному использованию памяти;
- Нерелевантные stacktrace, из 30 строк только одна может относиться к вашему коду.
Плюсы:
- RxJava — стандарт де-факто. Владимир провел опрос в твиттере и выяснил, что 65% разработчиков в новых проектах будут использовать RxJava;
- Мощный API;
- RxJava — фреймворк с открытым исходным кодом, у которого есть большое сообщество;
- Код на RxJava легко покрывается юнит-тестами.
Итерация пятая — Coroutines
Coroutines — библиотека для фоновой работы с поддержкой внутри языка Kotlin.
Ее плюсы:
1. Не блокирующий подход — основной поток выполняется во время фоновой работы и встраивает в себя результаты по мере выполнения;
2. Асинхронный код в синхронном стиле
private fun attemptLogin() {
launch(UI) {
val auth = BasicAuthorization(login, pass) try {
showProgress(true) val userInfo = login(auth).await() val repoUrl = userInfo.repos_url val list = getRepositories(repoUrl, auth).await() showRepositories(this@LoginActivity, list.map {
it - >it.full_name
})
} catch(e: RuntimeException) {
Toast.makeText(this@LoginActivity, e.message, LENGTH_LONG).show()
} finally {
showProgress(false)
}
}
}
3) Средства языка вместо операторов;
4) Просто изучать — кривая обучения почти не кривая;
5) После небольшого обдумывания юнит-тесты становятся почти такими же, как для синхронного кода.
Минусы:
1) Недавно вышли из статуса экспериментальных;
2) Это не часть языка, а библиотека;
3) Не для всего есть адаптеры;
4) Coroutines — не замена RxJava. Они не сильно помогут в сложных случаях со сложным потоком событий.
Остальное про Coroutines (включая примеры) лучше послушать в докладе:
Как уместить код в 300 строк, программируя на Kotlin
Год назад, на Google IO 2017 анонсировали то, что Kotlin стал официальным языком Android. Доклад Юрия Чечеткина из Альфа-Банка о том, как начать переезжать на новый язык, сократить классы до 300 строк и не сойти с ума.
Доклад — практический ликбез по тому, как писать компактно и красиво. Он ориентирован на продвинутую аудиторию, которая знает основные особенности Kotlin.
В докладе очень много примеров использования и сравнений кода на двух языках, поэтому здесь приведем только интересные факты и выводы.
Основные проблемы с миграцией на Kotlin:
- Legacy-код на Java. Большие классы на несколько тысяч строк кода очень сложны для конвертации средствами среды разработки;
- Зависимости — Lombok, Stream API и т.д.;
- Завышенные требования к коду внутри команды. Проводятся регулярные автоматические проверки кода на ограничение в 300 строк и code review;
- Kotlin — новый язык, и сложно сформулировать требования, пока нет единых конвенций;
- Kotlin компилируется дольше;
- Синтаксический сахар — «большая сила, но большая ответственность».
Выводы:
- Спустя год использования Kotlin стало меньше кода — он стал чище, стало удобнее делать code review;
- Нет старых методов Java, нет лишних зависимостей, только стандартные возможности языка;
- Меньше костылей и багов;
- Больше возможностей — некоторые вещи, реализуемые на Kotlin, невозможно написать на Java.
Остальное — в видео.
Следите за мероприятиями и буднями команды Я.Денег в ВК, фейсбуке и инстаграме.
Все конференции и митапы Яндекса — на Я.Встречах.