Android: StateFlow, SharedFlow и BroadcastChannel

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 и фрей­мвор­ках, которые могут в этом помочь.

  1. Spring Framework. Один из самых популяр­ных фрей­мвор­ков для бэкенд‑раз­работ­ки. Spring изна­чаль­но мож­но было исполь­зовать сов­мес­тно с Kotlin, но начиная с пятой вер­сии в фрей­мвор­ке появил­ся ряд рас­ширений для более удоб­ной раз­работ­ки на этом язы­ке. Даже при­меры в докумен­тации Spring теперь при­веде­ны на двух язы­ках. Генера­тор про­ектов start.spring.io теперь так­же под­держи­вает Kotlin.
  2. Ktor. Фрей­мворк для соз­дания асин­хрон­ных кли­ент­ских и сер­верных веб‑при­ложе­ний, раз­работан­ный в JetBrains. Базиру­ется на корути­нах и обла­дает высокой мас­шта­биру­емостью.
  3. Exposed. Реали­зация SQL ORM для Kotlin.
  4. Spek, Kotes, MockK, Kotlin Power Assert. Биб­лиоте­ки тес­тирова­ния и мокин­га спе­циаль­но для Kotlin.
  5. Klaxon, kotlinx.serialization. Биб­лиоте­ки для удоб­ной работы с JSON.
  6. RSocket. Про­токол для соз­дания мик­росер­висов с под­дер­жкой Kotlin.
  7. 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



Report Page