Android: StateFlow, SharedFlow и BroadcastChannel
Debian-LabСегодня будет: обнаружение динамически загружаемого приложением кода, реверс приложения для шифрования файлов, рассказ об истории создания StateFlow, SharedFlow и BroadcastChannel в Kotlin, статья с подборкой инструментов для серверной разработки на Kotlin, объяснение сути контрактов Kotlin, заметка о функции Surround With в IDEA и Android Studio. А также подборка свежих библиотек для разработчиков.
Обнаружение динамически загружаемого кода
Detecting Dynamic Loading in Android Applications With /proc/maps — статья о том, как определить, загружает ли приложение дополнительный исполняемый код уже после своего старта. Так часто делают зловреды, чтобы избежать обнаружения зловредного кода.
Метод крайне простой. Достаточно иметь рутованный смартфон с установленным приложением и кабель для подключения к ПК. Далее выполняем команду adb shell
, чтобы открыть консоль устройства, получаем права root с помощью команды su
, узнаем имя PID (идентификатор процесса) нужного нам приложения:
$ ps -A | grep имя.пакета.приложения
PID будет во второй колонке. Теперь выполняем следующую команду, подставляя полученный PID:
cat /proc/PID/maps | grep '/data/data'
Файл /proc/PID/maps
синтетический. Он содержит таблицу всех отображенных в памяти процесса файлов. Первая часть приведенной команды читает этот файл. Вторая часть команды (grep /data/data
) оставляет в выводе только файлы из приватного каталога приложения (/data/data/имя.пакета.приложения
). Именно оттуда зловреды обычно загружают дополнительный код.
Дешифровка зашифрованных приложением файлов
Decrypting images hidden with Calculator+ — разбор способа реверса и последующей расшифровки файлов приложения Calculator+ — Photo Vault & Video Vault hide photos. Пример крайне простой и поэтому хорошо подходит для демонстрации основ реверса приложений для Android.
Итак, есть приложение Calculator+, которое имеет неожиданную функцию секретного хранилища зашифрованных фотографий и видеороликов. Задача: вытащить эти фотографии, не используя само приложение.
1. Извлекаем приложение из смартфона:
$ adb shell pm list packages|grep calc
$ adb shell pm path eztools.calculator.photo.vault
$ adb pull /data/app/eztools.calculator.photo.vault-OP_MBoGMZN-LZ5wr50dNWA==/base.apk
2. Используем JADX-GUI для декомпиляции приложения обратно в исходный код Java.
3. С помощью встроенной функции поиска ищем все строки, имеющие отношение к шифрованию: encrypt, decrypt, AES и так далее. Автору удалось найти следующий фрагмент кода:
FileInputStream fileInputStream = new FileInputStream(this.f6363e.f6361a);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
EncryptUtils.m10791a("12345678", fileInputStream, byteArrayOutputStream);
byteArrayOutputStream.flush();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
this.f6362d = byteArrayInputStream;
aVar.mo2601d(byteArrayInputStream);
Метод EncryptUtils.m10791a()
оказался искомым методом шифрования.
Как видно из кода, метод шифрует файл алгоритмом DES и ключом, переданным ему в качестве аргумента. А в аргументе всегда передается строка 12345678.
4. Расшифровать файлы не составит труда, но сначала необходимо найти, где они хранятся. Для этого надо проследить за методами, вызывающими метод шифрования. В итоге нашелся такой код:
public static final File m18904s(Context context) {
C3655i.m20371c(context, "context");
File externalFilesDir = context.getExternalFilesDir("photo_encrypt");
if (externalFilesDir != null) {
return externalFilesDir;
}
C3655i.m20376h();
throw null;
}
Это код создания объекта класса File во внешнем приватном каталоге приложения (context.getExternalFilesDir): /sdcard/Android/data/eztools.calculator.photo.vault/files
.
5. Теперь файлы можно извлечь и расшифровать. Автор написал для этого скрипт на Python, приводить его здесь я не буду.
Разработчику
SharedFlow, StateFlow и broadcast channels
Shared flows, broadcast channels — статья Романа Елизарова (текущего главы разработки Kotlin) о развитии средств коммуникации между корутинами и текущем положении дел в этой области.
Статья начинается с рассказа о каналах (Channel) — средстве коммуникации потоков исполнения (в данном случае корутин), позаимствованном из модели параллельного программирования CSP и языка Go. Каналы позволяют удобно и легко обмениваться данными и синхронизировать состояние нескольких корутин без опасности столкнуться с проблемой одновременного изменения состояния объекта.
В дополнение к классическим каналам в библиотеке kotlinx-coroutines также появился так называемый BroadcastChannel. Это специальный тип канала, позволяющий получать отправленные в канал сообщения сразу нескольким корутинам (подписчикам). По сути, это была реализация паттерна event bus.
В целом каналы были весьма неплохим решением, но только до тех пор, пока разработчику не требовалось организовать высокопроизводительный конвейер обработки данных, когда данные передаются по каналу одной корутине, затем подвергаются обработке и с помощью другого канала передаются следующей корутине.
Каналы, в силу своей природы как механизма синхронизации, просто не могли обеспечить приемлемой производительности в таком сценарии использования.
Эту проблему решил новый механизм передачи данных — Flow. Он позволяет создать так называемый холодный поток данных, который будет порожден только при подключении потребителя и создаст проблемы с производительностью, только если источник и потребитель будут работать в разных потоках. Основная идея Flow в том, чтобы позволить программисту описать алгоритм генерации потока данных и запускать этот поток лишь тогда, когда в нем возникнет необходимость. К примеру, следующий код не создает никаких данных до тех пор, пока на объекте coldFlow
не будет вызван метод collect
:
val coldFlow = flow {
while (isActive) {
emit(nextEvent)
}
}
При этом каждый потребитель получит собственную копию данных. И в этом же его проблема. Flow превосходно справляется с задачей организации конвейеров обработки данных, но его нельзя использовать, когда поток данных должен существовать сам по себе: например, для организации обработки системных событий в том самом паттерне event bus.
Решить эту задачу призван SharedFlow. Это своего рода производительный аналог BroadcastChannel, он существует независимо от наличия потребителей (которые теперь называются подписчиками) и отдает всем подписчикам одни и те же данные (в противовес классическому Flow, который создает индивидуальный поток данных для каждого потребителя).
Создать event bus с помощью SharedFlow крайне просто:
class BroadcastEventBus {
private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow()
suspend fun postEvent(event: Event) {
// Корутина будет приостановлена до получения сообщения подписчиками
_events.emit(event)
}
}
По умолчанию корутина‑производитель приостанавливает свое исполнение до тех пор, пока потребители не получат новое сообщение. Это поведение можно изменить: создать буфер сообщений, который сможет породить производитель перед тем, как его корутина будет приостановлена, или указать размер истории сообщений, чтобы вновь подключившиеся подписчики могли получить уже опубликованные сообщения.
Если же задача требует, чтобы производитель не приостанавливался ни при каких обстоятельствах, а потребители получали только последнее сообщение, то для этого есть StateFlow. Это основанная на Flow реализация паттерна «Состояние»:
class StateModel {
private val _state = MutableStateFlow(initial)
val state = _state.asStateFlow()
fun update(newValue: Value) {
// Производитель не приостанавливается
_state.value = newValue
}
}
Как говорит Роман, val x: StateFlow<T>
можно представить как асинхронную версию var x: T
с функцией подписки на обновления.
Учитывая все сказанное выше, можно утверждать, что Flow стал полноценной, более производительной и приближенной к реальным задачам заменой каналов. Однако в одном случае каналы имеют преимущества: когда сообщение должно быть обработано ровно один раз (не больше и не меньше).
class SingleShotEventBus {
private val _events = Channel<Event>()
val events = _events.receiveAsFlow()
suspend fun postEvent(event: Event) {
// Корутина будет приостановлена при переполнении
_events.send(event)
}
}
Этот пример схож с приведенным ранее BroadcastEventBus
. Он также отдает сообщения во внешний мир с помощью Flow. Но есть важное отличие: в первом случае при отсутствии подписчиков сообщение будет выброшено, во втором производитель будет приостановлен до тех пор, пока не появится потребитель.
Kotlin и контракты
How to Make the Compiler Smarter — статья о том, как сделать компилятор Kotlin умнее, используя контракты.
Компилятор Kotlin (и анализатор кода в среде разработки) отличается мощной системой выведения типов. Например, в следующем фрагменте кода компилятор способен самостоятельно понять, что переменная s
не равна null при использовании в качестве аргумента функции print
:
fun printLengthOfString() {
val s: String? = "Hey Medium!"
if (s != null) {
print("The length of '$s' is ${s.length}")
}
}
Но стоит немного изменить код, и компилятор становится бессилен:
fun printLengthOfString() {
val s: String? = "Hey Medium!"
if (s.isNotNull()) {
print("The length of '$s' is ${s.length}")
}
}
fun String?.isNotNull(): Boolean {
return this != null
}
К счастью, начиная с Kotlin 1.3 в нашем распоряжении есть механизм, позволяющий подсказать компилятору, как быть с теми или иными типами в разных ситуациях. Мы можем как бы заключить с компилятором контракт, что если приведенная выше функция isNotNull
возвращает true, то это значит, что строка не равна null. Компилятор будет это учитывать при выведении типов.
Контракты пишутся в самом начале функции примерно в таком виде:
fun String?.isNotNull(): Boolean {
contract {
returns(true) implies(this@isNotNull != null)
}
return this != null
}
В данном случае контракт как раз сообщает, что если функция возвращает true (returns(true)
), то строка не равна null (implies (this@isNotNull != null)
). Компилятор будет это учитывать, и приведенный выше пример уже не будет ошибочным.
Вторая часть контракта (функция implies
) называется эффектом. Есть также другой эффект — CallsInPlace
. Он нужен для того, чтобы сообщить компилятору, что указанная в аргументе лямбда была выполнена один или больше раз.
Работает это так. Допустим, у нас есть такой код:
var a: Int
{
a = 42
}
print(a)
Этот код не будет скомпилирован, так как компилятор не уверен, что код внутри лямбды (a = 42
) был выполнен хотя бы раз. Теперь добавим в код контракт:
fun main() {
var a: Int
initialize {
a = 42
}
print(a)
}
fun initialize(myLambda: () -> Unit) {
contract {
callsInPlace(myLambda, InvocationKind.AT_LEAST_ONCE)
}
myLambda()
}
Теперь код успешно компилируется, потому что мы заверили компилятор, что лямбда initialize
будет выполнена хотя бы один раз.
Серверная разработка на Kotlin
Server-Side Development with Kotlin: Frameworks and Libraries — статья разработчиков из JetBrains о серверной разработке на языке Kotlin и фреймворках, которые могут в этом помочь.
- Spring Framework. Один из самых популярных фреймворков для бэкенд‑разработки. Spring изначально можно было использовать совместно с Kotlin, но начиная с пятой версии в фреймворке появился ряд расширений для более удобной разработки на этом языке. Даже примеры в документации Spring теперь приведены на двух языках. Генератор проектов start.spring.io теперь также поддерживает Kotlin.
- Ktor. Фреймворк для создания асинхронных клиентских и серверных веб‑приложений, разработанный в JetBrains. Базируется на корутинах и обладает высокой масштабируемостью.
- Exposed. Реализация SQL ORM для Kotlin.
- Spek, Kotes, MockK, Kotlin Power Assert. Библиотеки тестирования и мокинга специально для Kotlin.
- Klaxon, kotlinx.serialization. Библиотеки для удобной работы с JSON.
- RSocket. Протокол для создания микросервисов с поддержкой Kotlin.
- GraphQL Kotlin. Библиотека для удобной работы с GraphQL из Kotlin.
Многие другие фреймворки, включая Micronaut, Quarkus, Javalin, Spark Java, Vaadin, CUBA и Vert.x, также поддерживают Kotlin и имеют документацию и примеры кода на Kotlin. Большой список Kotlin-библиотек можно найти здесь.
Живые шаблоны и Surround With
IntelliJ IDEA / Android Studio Tricks: Surround With — статья об очень полезной, но далеко не всем известной функции IDEA под названием Surround With.
В IDEA (и, как следствие, Android Studio) есть функция Live Templates (живые шаблоны), которая представляет собой нечто вроде сокращений для длинных строк кода и конструкций. Типичный пример — шаблон ifn
— if null. Ты просто пишешь ifn, затем нажимаешь Tab, и среда разработки сама вставляет в код конструкцию if (x == null) { }
, автоматически выделяет x
и ставит на него курсор. Функция настолько умная, что даже попытается предсказать имя переменной x
на основании того, какие переменные текущей области видимости могут иметь значение null.
У шаблонов есть родственная функция под названием Surround With. Она работает примерно так же, но по отношению к уже имеющемуся выражению. Например, если навести курсор на любую строку кода и нажать Ctrl + Alt + T, среда разработки покажет на экране меню с выбором шаблонов. Среди них есть шаблон, который обрамит выражение в блок try/catch, в if/else и так далее.
И конечно же, IDEA позволяет создавать собственные шаблоны. Открой настройки, далее Editor → Live Templates, затем кнопка +. Полезные примеры шаблонов:
by lazy { $SELECTION$ }
CoroutineScope(Dispatchers.Main).launch {
$SELECTION$
}
withContext(Dispatchers.IO) {
$SELECTION$
}
Библиотеки
- Red Screen Of Death — заменяет стандартное сообщение о краше приложения на экран со стектрейсом;
- Spotlight — библиотека для создания экранов обучения;
- CodeView — встраиваемый редактор кода;
- BlurHashExt — Kotlin-расширения для библиотеки BlurHash;
- LocationFetcher — библиотека для получения текущих координат с использованием Kotlin Flow;
- Android_dbinspector — библиотека для просмотра содержимого базы данных приложения прямо на устройстве;
- iiVisu — визуализатор звукового спектра;
- Brackeys-IDE — редактор кода с подсветкой синтаксиса;
- ScreenshotDetector — демоприложение, которое определяет, что был сделан скриншот приложения;
- GraphQL Kotlin — библиотека для создания клиентов и серверов GraphQL на Kotlin;
- Exhaustive — аннотация, позволяющая пометить выражение
when
как исчерпывающее (не имеющее других вариантов); - Lifecycle-Delegates — библиотека для создания полей, которые будут проинициализированы в момент выполнения lifecycle-колбэка (onCreate, onStart и так далее);
- Bundler — библиотека для удобного создания бандлов (Bundle);
- ReadTime — библиотека для получения примерного времени на чтение текста.
⏳ Наш основной канал - @debian_lab
🌆 Наш канал с приватными сливами - @slivkins_lab