Android under the hood
1.56K subscribers
62 photos
1 video
72 links
Пишу об Android разработке, программировании и о всяких интересных штуках.

while (isAlive) { beHappy(); }

лс: @dmitry_tsyvtsyn
Download Telegram
Начнём с того, что даже простое чтение переменной может вызвать проблемы в многопоточных программах, так как у каждого процессорного ядра есть свои кэши, поэтому для актуального чтения используется @Volatile.

Но что если нам нужно прочитать какое-то значение, пройтись в when операторе, а потом записать новое, можно конечно попробовать сделать это через @Volatile:

@Volatile var _state: Int

when(_state) {
NONE -> _state = PENDING
...
}


Но проблема в том, что может произойти ситуация когда поток X прочитает актуальное значение, а в этот момент поток Y запишет другое, что приведет к нелогичным данным.

Глянем решение с compareAndSet(), пример взят из исходников StateFlow:

val _state = atomic(0)

while(true) {
val state = _state.value
when(state) {
NONE -> if (_state.compareAndSet(state, PENDING)) break
...
}
}


Метод compareAndSet() под капотом реализован процессорной инструкцией, эта инструкция выполняется атомарно и следовательно не нуждается в синхронизации между потоками, работает она следующим образом:

1) читает актуальное значение переменной _state из памяти как это делает @Volatile
2) сравнивает актуальное значение с текущим state, которое как мы уже знаем может быть неактуальным
3) если значение актуально, записывает новое PENDING и возвращает true
4) если значение неактуально возвращает false

Когда поток Y запишет новое значение, поток X начнёт выполнять метод compareAndSet(), который вернёт false, так как данные неактуальны, далее начнётся новая итерация цикла, это будет продолжаться до тех пор пока на потоке X не появятся актуальные данные и только когда это произойдёт запишутся новые.

P.S. Использование бесконечного цикла с инструкцией compareAndSet() это нормальная практика, применяется много где: начиная с корутин и заканчивая Android SDK.

Пишите ваше мнение в комментах и всем хорошего кода!
👍30🔥6👏5
Всех с первым днём зимы!

Ещё весной я написал пост о том что собираюсь делиться не только техническими штуками, но и другими полезными вещами. Я подумал над этим и решил что этот канал должен остаться техническим с уклоном на внутрянку и базовые штуки, ведь в этом его основная фишка, а для всего остального будет другой канал @justy_blog, так что если интересно залетайте)

Далее я немного зашел в тупик в поиске новых материалов, поэтому буду признателен если накидаете идей про что конкретно хотелось бы узнать касательно внутрянки и базы, может объяснить какую-то идею, подход и тд, предлагайте, не стесняйтесь)

Пишите в комментах ваше мнение и всем хорошей недели!
👍5🐳1
В прошлом посте скинули классную статью по Jetpack Compose оптимизациям, я бы хотел дополнить ее небольшим таким выводом: большинство оптимизаций кода (не токо Compose) сводится к сокращению используемой памяти и количества вычислений.

Например вы пишите алгоритм с маппингом и фильтрацией над списком, который будет выполняться условно на устройстве с 256MB ОЗУ, на Kotlin можно написать вот такое:

val output = input
.map { it.toNewModel() }
.filter { it.check() }
.take(3)

Этот код не очень хороший, так как есть пара нюансов:

1) Если глянуть под капот map, filter и take можно заметить что там создается ArrayList, а так как результаты map и filter нам нужны только в процессе вычисления, то создавать для них отдельные списки явно лишние затраты в памяти.

2) Маппится сразу весь список, а потом также весь фильтруется несмотря на то что там может быть сотня значений, хотя нам нужны только первые три, это однозначно лишние вычисления.

Как можно решить обе проблемы? Для начала конечно можно попробовать заменить ArrayList на Sequence, это решит вторую проблему, но под капотом Sequence используются итераторы, а это дополнительные объекты, занимающие драгоценную для нас память. В итоге все сводится к линейной и простой логики как в старом добром :

val input = ArrayList<OldModel>()
val output = ArrayList<NewModel>(3)
var index = 0
while (index < input.size) {
val newModel = input[index].toNewModel()
if (newModel.check()) {
output.add(newModel)
if (output.size == 3) break
}
index++
}


Вот так можно заоптимизировать практически любой код, держа в уме что конкретно вам надо (сократить объем используемой ОЗУ) и более менее понимать как устроены те или иные штуки (функции map / filter / take).

Пишите в комментах ваше мнение и всем хороших выходных!
👍19😁2🥴1
Корутины очень мощная библиотека, предоставляющая удобные средства для организации асинхронного программирования, но что еще круче корутины дают возможность работать с внутренним объектом Continuation и создавать свои собственные реализации!

Например у нас есть вот такой BroadcastReceiver:

class SomeReceiver : BroadcastReceiver() {
// пригодится в дальнейшем чтобы подписаться на изменения
private var update: (Intent) -> Unit = {}

override fun onReceive(
ctx: Context,
result: Intent
) {
update.invoke(result)
}
}


На выходе мы хотим получить Flow<Intent>, на который можно будет подписаться. Давайте попробуем набросать решение, опираясь на знание о Continuation:

fun ComponentActivity.pickIntents(): Flow<Intent> {
val receiver = SomeReceiver()
registerReceiver(receiver, ...)

return flow {
while(true) {
val intent = receiver.pickIntent()
if (checkActive()) {
emit(intent)
} else {
break
}
}
}.onCompletion {
unregisterReceiver(receiver)
}
}

// SomeReceiver
suspend fun pickIntent() = suspendCancellableCoroutine { cont ->
val receiver = this
receiver.update = { result ->
cont.resume(
value = result,
onCancellation = {
receiver.update = {}
}
)
}
}

suspend fun checkActive(): Boolean =
coroutineContext[Job]?.isActive == true


Функция suspendCancellableCoroutine вернет значение COROUTINE_SUSPENDED, при котором дальнейший код корутины не будет выполняться до тех пор пока не произойдет событие в BroadcastReceiver'е и не вызовется Continuation.resume(). После этого suspendCancellableCoroutine выйдет из состояния приостановки и мы получим Intent.

Вот таким нехитрым образом, зная базовые кирпичики библиотеки можно строить решения с очень высокой инкапсуляцией кода, кстати если вам не очень понятно что такое Continuation, у меня есть довольно крутая статья с разбором внутрянки корутин.

P.S. Прикрепляю ссылку на Gist примера

Всем хорошей недели!
👏26👍17🔥3😴3❤‍🔥1
Рекурсия на основе корутин.

Возьмем к примеру алгоритм факториала, его можно написать как рекурсивно так и с помощью циклов. В случае с рекурсией могут быть проблемы с переполнением стэка вызовов (StackOverflowError), а вариант с циклом плох тем что не всегда дает преимущество в простоте написания кода.

У корутин есть классное решение, объединяющее преимущества обеих подходов:

// я опустил generic типы для лаконичности
val factorial = DeepRecursiveFunction { num ->
if (num == 0) 1
else num * callRecursive(num - 1)
}
println(factorial.invoke(10))


При вызове DeepRecursiveFunction.invoke() под капотом создается DeepRecursiveScopeImpl, где запускается бесконечный цикл:

class DeepRecursiveScopeImpl<T, R>(
// это тело DeepRecursiveFunction
block: suspend DeepRecursiveScope<T, R>.(T) -> R
) : Continuation<R> {

private var cont = this

suspend fun callRecursive(value: T): R {
// тут сохраняется Continuation для текущего вызова рекурсии, каждый последующий Continuation ссылается на предыдущий
}

override fun resumeWith(...) {
// когда рекурсия доходит до базового случая, результат передается с самого последнего Continuation объекта до самого первого, которым является сам класс DeepRecursiveScopeImpl, в итоге ссылка на Continuation зануляется, а бесконечный цикл возвращает конечный результат
}

fun runCallLoop(): R {
while (true) {
// при каждом шаге цикла создается Continuation для каждого вызова рекурсии, это происходит до тех пор пока block не вернет базовый случай
}
}

}


В отличии от привычной нам рекурсии, где каждый шаг хранился в стэке, здесь он хранится в Continuation объекте, то есть вместо стэковой памяти используется heap память.

P.S. Буду благодарен если пройдете опрос и напомимаю о втором своем канале, где пишу обо всем.

Всех с наступающим Новым годом!
👏27👍3🥰2🔥1
Gradle скрипты.

Бывают ситуации когда нужно написать какой-нибудь прикладной скрипт для вашего проекта, например вычислить количество модулей где используются импорты Retrofit'а. Для таких штук удобнее всего написать Gradle таску на Kotlin DSL и сделать это в самом главном build.gradle.kts файле (полный код):

tasks.register("modules_with_retrofit") {
    val imports = listOf("retrofit2")

    /*
    возвращает true если в указанной папке есть .java или .kt файлы с Retrofit импортами
    */
    fun Project.isRetrofit(
        sourcesFolderPath: String
    ): Boolean { ... }

    val modules = rootProject.allprojects
    val count = modules.fold(0) { result, module ->
        val java = module.isRetrofit(
            "src/main/java/"
        )
        val kotlin = module.isRetrofit(
            "src/main/kotlin/"
        )
        if (java || kotlin) result + 1
        else result
    }

    println("RESULT => $count")
}


Для запуска таски достаточно написать в терминале:

./gradlew modules_with_retrofit


Раньше вместо Gradle Kotlin DSL я использовал Python, что было не очень эффективно по двум причинам:

1) Python ничего не знает о Gradle модулях, из-за этого много шаблонного и ненужного кода.
2) Python не мой основной язык, поэтому много чего приходилось гуглить.

P.S. Рекомендую канал одного крутого чувака - Everlasting Loops, канал о всяких интересных штуках из мобильной разработки, вы вряд ли о них прочитаете в доке.

Пишите в комментах ваше мнение и всем хорошего кода!
12👍9🔥3🤔1
Как вы отдохнули на Новогодних праздниках? Я вот вообще не умею отдыхать и поэтому решил начать 2025 год со сложной технической статьи по корутинкам и как по мне получилось очень круто, буду верить что эта статья станет легендой.

Если кратко то статья о состояниях Job и внутряночных механизмах отмены корутин, после прочтения вы получите ответы на следующие вопросы:

- как работает функция delay() под капотом?
- как отменяются suspend функции?
- что такое CancellableContinuation?
- как работает SupervisorJob?
- почему CoroutineExceptionHandler работает только если его указать в top-level корутине?
- где выбрасывается CancellationException исключение?
- как корутина дожидается завершения своих детей?
- и другие

P.S. Спасибо всем кто поставил лайкосы под моим сообщением, где я обещал написать еще 2 статьи по корутинам, что ж одна уже есть)

Всем хорошего кода!
🔥60👍10😴31
Виртуальные потоки в Java 21.

Недавно я узнал что в новых версиях Java появилась штука, похожая на корутины - виртуальные потоки:

// специальный пул виртуальных потоков
var executor = Executors.newVirtualThreadPerTaskExecutor()
try (executor) {
// создаем 50к виртуальных потоков
IntStream.range(0, 50_000).forEach(i -> {
executor.submit(() -> {
...
});
});
}


Как видите создание виртуальных потоков не такая дорогостоящая операция, кстати у корутин был аналогичный пример в доке, где еще говорилось что корутины это легковесные потоки:

fun main() = runBlocking {
// запускаем 50к корутин
repeat(50_000) {
launch {
...
}
}
}


Безусловно виртуальные потоки далеки от того функционала, который могут предоставить корутины, но мне было интересно что они из себя представляют, вот несколько фактов о них:

1) Виртуальный поток наследует класс Thread, сохраняя общий интерфейс, но переопределяя метод Thread.start(), где больше нет вызова нативного метода - классический Thread создается и запускается системой.

2) Виртуальный поток выполняется на некотором пуле потоков - в корутинах под капотом тоже используются пулы, только над ними есть такая абстракция как CoroutineDispatcher, например: Dispatchers.IO.

3) Иерархия виртуальные потоков не такая мощная по функционалу как в корутинах - родитель только предоставляет пул дочерним виртуальным потокам.

4) Виртуальные потоки отменяются абсолютно также как и классические - с помощью методов Thread.interrupt() и Thread.isInterrupted() или через Boolean флаги.

5) Исключения в виртуальных потоках обрабатываются также как и в обычных - если исключение необработанно поток просто завершится.

По итогу мы имеем такой же интерфейс, как и у обычных потоков Thread, только виртуальные потоки не создаются системой, а являются обыкновенными объектами, выполняющими свои задачи на пуле классических потоков Thread.

Пишите в комментах ваше мнение и всем хорошего кода!
👍22❤‍🔥1
В прошлом году я пробовал организовать встречи Android и iOS разрабов в Барнауле, но к сожалению активных людей не собралось и чат тупо умер(

Сейчас я снова ищу заинтересованных людей у кого есть желание поболтать о мобильной разработке, поделиться своим опытом / знаниями, рассказать о своих проектах и тд, в общем добавляйтесь если вы из Барнаула:
https://xn--r1a.website/+hX4C_cSIgw00N2Ey
буду признателен если расскажите о себе когда зайдете.

P.S. В будущем хотелось бы превратить сообщество в площадку для собственных митапов и офисных вечеринок.

Всем хорошего завершения недели!
🔥14
Захотел поиграть в Minecraft, но все закончилось исходниками.

Недавно я решил создать свой собственный Minecraft сервер и в качестве ядра выбрал Spigot, это одна из популярных и хорошо оптимизированных реализаций. Но перед тем как приступить к созданию сервера мне захотелось глянуть исходники Spigot, я по быстрому пробежался по основным репозиториям и заметил одну крутую штуку - сам по себе репозиторий Spigot это всего лишь набор Git патчей для другого репозитория CraftBukket.

Проект CraftBukket является полностью готовым ядром, которое можно брать и использовать для создания серверов, поэтому он обязан меняться с выходом новых версий Minecraft'а, а Spigot как я уже упоминал это всего лишь набор Git патчей для CraftBukket'а. В итоге не было никакого смысла создавать два отдельных репозитория и дважды поднимать версию Minecraft'а, намного проще написать небольшой скриптик, который будет скачивать исходники CraftBukket и применять патчи Spigot, для этого как раз используется BuildTools.

Только сейчас дошло что описал принцип fork'ов в действии: берем проект X, че-то меняем и получаем проект Y, если в проекте X поменяется что-то глобальное делаем какой-нибудь git rebase.

P.S. Если вы знаете крутые хостинги для Minecraft серверов, напишите пожалуйста в комментах, буду очень признателен)

Всем хорошей недели!
👍6🔥4🤯1🤣1
Всем привет, короче я запилил ванильный сервак на самой свежей версии Minecraft 1.21.4, кто хочет поиграть вступайте в чатик, там будут все подробности:

https://xn--r1a.website/+qXpvPEzDV8QzOTcy

Также в этом месяце я планирую написать на Хабре простенькую статью о том как устроен CoroutineContext и CoroutineScope под капотом, так что хейтеры Minecraft прошу не отписываться)

Всем хорошего дня!
15😁5👌3
Получение реализации интерфейса по имени класса в Kotlin.

В Kotlin есть возможность написать имя класса вместо реализации интерфейса если интерфейс реализован companion object'ом этого класса:

interface Key

class Value {

companion object KeyImpl : Key {
...
}
}

// мы можем обратиться к реализации ключа по имени класса
val key1: Key = Value
// полная форма
val key2: Key = Value.KeyImpl


Если декомпилировать байт-код, сгенерированный для этого примера можно увидить что вместо имени класса подставляется companion оbject реализация:

public interface Key {}

public final class Value {

// companion object это статическое поле
// другими словами синглетон класса
public static final KeyImpl KeyImpl = new KeyImpl();

public static final class KeyImpl implements Key {}
}

// в обеих ситуациях используется полная форма
Key key1 = (Key) Value.KeyImpl;
Key key2 = (Key) Value.KeyImpl;


Если честно я недавно узнал об этом когда в очередной раз залез в исходники корутин, если вы не знали в CoroutineContext'е используется аналогичный механизм для изящного извлечения элементов:

// вместо использования Job.Key используется имя класса
val job = coroutineContext[Job]
// аналогично с CoroutineDispatcher
val dispatcher = coroutineContext[CoroutineDispatcher]


Классы Job и CoroutineDispatcher содержат реализации ключей в своих companion object'ах, а CoroutineContext переопределяет оператор получения значения по ключу (квадратные скобки), поэтому такой код будет работать.

Пишите в комментах ваше мнение и всем хорошего кода!
🤯20👍12
Всем привет, объявляю конкурс по лучшему дизайну!

Общее описание задания: разработать дизайн для мобильного приложения под Android / iOS, можно сделать как общий дизайн, так и два разных, основная идея приложения - это поиск напарников для игр. В двух словах: пользователь заходит в приложение через Google или другие возможные способы авторизации, заполняет свою анкету: имя, аватарка, возраст, краткое описание, указывает контакты Discord / Telegram / VK, выбирает игры в которые играет, у каждой игры будет свой уникальный набор свойств, это нужно для более специфичной фильтрации, далее пользователь попадает на главную страницу, где отображаются анкеты в вертикальном списке, вверху экрана можно настроить фильтры.

Экраны которые надо сверстать:

1) Экран создания анкеты, указываются основные данные, такие как имя, аватарка, описание, возраст, контакты Telegram / Discord / VK и самая важная часть список игр в которые играет пользователь, для каждой игры можно настроить свой уникальный набор свойств, например для рейтинговых игр это уровень рейтинга и тд, также необязательно заполнение анкеты делать как один экран, можно разделить на несколько и идти по шагам.
2) Основной экран с поиском: вверху набор фильтров который можно раскрыть, тута можете придумывать любые переходы, в качестве фильтров пока можно взять название игры, возраст игрока и накинуть что-нибудь специфичное когда выбираете определенную игру, например если это рейтинговая игра то для такой игры можно добавить уровень рейтинга, игры выбираете на свое усмотрение, какие хотите, также будет сортировка, например по дате последнего захода пользователя.
3) Остальная часть основного экрана: это список анкет, в каждой из которых указывается - сколько времени назад был пользователь в сети, имя, аватарка, краткое описание, возраст, список игр, в списке игр каждый элемент можно раскрыть чтобы посмотреть более конкретные вещи касательно этой игры, опять же какие-то поля можно выкинуть если вы считаете что так будет лучше или наоборот что-то добавить.

Цвета можно использовать любые, редакторы тоже, самое главное это интересные и уникальные дизайны с прикольными фишками и идеями, так что прямо следовать моему описанию экранов не надо, делайте в соответствии со своими творческими взглядами.

Приз: 33 333 рублей.

Прием работ до 22 февраля.

P.S. Если вы хотите проявить себя в качестве собеседующего вступайте в @tech_talk_community, интересная инициатива, возможно в будущем трансформируется в полноценную площадку для проведения собесов.

Присылайте работы в личные сообщения и пишите если что-то непонятно по конкурсу, желаю вам победить)
🔥12😁8🫡2
Всем привет, написал еще одну интересную статейку по корутинам - CoroutineContext / CoroutineScope под капотом, после прочтения которой вы сможете ответить на следующие вопросы:

1) Почему корутины нельзя запустить без CoroutineScope?
2) Что такое CoroutineScope и какие есть реализации?
3) Что такое CoroutineContext? На основе какого паттерна построен?
4) Что является ключом в CoroutineContext? Как суммируются его элементы?
5) Как организуется Structured Concurrency?

Также хотел поделиться, недавно замержили мой первый pull request в репозиторий корутин!
https://github.com/Kotlin/kotlinx.coroutines/pull/4341

Пишите в комментах ваше мнение и всем хорошего кода!
🔥34👍73🥴2😴1
Media is too big
VIEW IN TELEGRAM
Всем привет, решил попробовать записать небольшой пробный видос, в дальнейшем собираюсь сделать плейлист где буду разбирать всякие штуки, неуверен пока насколько хватит энтузиазма, но буду рад если глянете видос и напишите свою оценку в комментах)

P.S. Планирую установить хромакей чтобы убрать задний фон, ну и буду учиться чаще смотреть в камеру)

Всем хорошего кода!
🔥49😢4❤‍🔥3
Пару слов про CoroutineScheduler.

Я был уверен что CoroutineDispatcher в JVM реализации использует пул потоков ThreadPoolExecutor из Java, но как всегда бывает, вроде во всем разобрался, а нет нифига, найдется что-то новое и в моем случае это CoroutineScheduler.

CoroutineScheduler - это своего рода аналогия ThreadPoolExecutor, которая хранит и переиспользует потоки, а также распределяет между ними задачи, на этой штуке как раз и построены Dispatchers.Default и Dispatchers.IO, рассмотрим ключевые моменты:

1) Все потоки хранятся в одном массиве, как для Dispatchers.IO, так и для Dispatchers.Default, поэтому в случае бездействия одного из диспатчеров потоки другого могут быть спокойно переиспользованы.

2) Потоки создаются лениво, когда это надо, тут важный момент - количество потоков для Dispatchers.Default будет ограничено количеством ядер процессора, а для Dispatchers.IO потоки будут создаваться пока не достигнут 64 штук (если в системном свойстве не указано другое значение).

3) Все потоки в CoroutineScheduler представлены абстракцией Worker, у каждого Worker'а есть своя очередь задач, в качестве задачи выступает выполнение корутины, помимо очереди для отдельного Worker'а есть еще две глобальные очереди - одна для CPU задач, другая для IO задач.

4) При выполнении задачи сначала проверяется на каком потоке она выполняется, если на одном из потоков CoroutineScheduler'а то сразу ставится в очередь актуального Worker'а, в противном случае добавляется в глобальную очередь, откуда Worker'ы в дальнейшем ее заберут.

5) Могут быть ситуации когда Worker содержит пустую очередь задач, то есть полностью свободен, в таком случае он попытается украсть задачу либо из глобальных очередей CPU и IO, либо у других Worker'ов, это оптимизация для более эффективного распределения задач.

Всем хорошего кода!
👍47🤔31
Цикл while везде решает.

У вас когда нибудь была задача написать функцию, которая делает какой-то запрос и в случае ошибки повторно перезапрашивает данные? Мне недавно попался такой кейс, вроде бы фигня, но решение оказалось настолько простым и изящным, что даже захотелось поделиться им:

// опустил возвращаемый тип для простоты
fun fetchPosts(retryCount: Int = 0) {
var currentRetryAttempt = 0
while (currentRetryAttempt <= retryCount) {
try {
// сделать запрос и вернуть че то
} catch (_: Exception) {
// попробовать еще, если осталась попытка
currentRetryAttempt++
}
}
return emptyList()
}


Вообще люблю цикл while, сейчас в современной Kotlin разработке его все сложнее встретить, так как чаще используются более высокоуровневые методы такие как forEach например.

P.S. Если вас интересуют более общие темы, связанные с миром IT такие как информационная безопасность или IoT рекомендую канал одного крутого чувака Max Knyazev is typing…

Всем хорошего кода!
🤣17👍162
Как потоку эффективно дождаться следующих задач?

В одном из предыдущих постов я разбирал как работает корутиновский пул потоков CoroutineScheduler, так вот после того как корутина выполнилась, поток должен дождаться следующей задачи, самый простой способ сделать это - проверять новые задачи в бесконечном цикле:

while(true) {
if (taskQueue.isEmpty()) {
continue
}
val task = taskQueue.poll()
task.run()
}


Новые задачи добавляются в очередь и если она непустая выполняются, проблема такого подхода в том что процессор будет выполнять тысячи или даже сотни тысяч бесполезных проверок каждую секунду.

Чтобы поток не занимался бесполезной работой, а просто дожидался следующих задач была придумана удобная обертка над Unsafe API - LockSupport класс:

while (true) {
if (taskQueue.isEmpty()) {
LockSupport.park()
}
val task = taskQueue.poll()
task.run()
}

fun addTask(task: Task) {
taskQueue.push(task)
LockSupport.unpark(this)
}


Суть идеи в том что метод LockSupport.park() переводит текущий поток в состояние бездействия, а метод LockSupport.unpark() наоборот возвращает его из этого состояния, поэтому при отсутствии задач процессор не будет занят лишними инструкциями, если хотите конкретно разобраться во всей этой теме рекомендую глянуть исходники класса CoroutineScheduler,

P.S. Есть еще полезный метод LockSupport.parkNanos() если хотите задать время жизни потока при отсутствии задач, кстати в CoroutineScheduler есть такая логика - гляньте поле idleWorkerKeepAliveNs.

Всем хорошего кода!
👍31😁3🔥2