Dev Easy Notes
3.17K subscribers
123 photos
1 video
147 links
Работаю в IT уже 8 лет. Рассказываю про разработку простым языком. Полезность скрыта под тупыми шутками и слоем мата. Лучший underground канал про разработку, который вы сможете найти.

По сотрудничеству писать @haroncode
Download Telegram
Channel created
Привет всем кто забрел на наш канал.🎉 Этот канал для тех, кто интересуется разработкой под android или вообще программированием.

💻 На этом канале мы разбираем разные фундаментальные штуки из мира android, jvm и немного computer science. Эти вещи помогают понять все остальное, вроде современых фреймворков, магию библиотек и общих подходов. 

🧐На канале будет мало новостей с мира android, для этого уже есть Android Broadcast, мы тут собрались чтобы смотреть глубже. Интересно будет тем, кто любит сложные штуки, и хочет понять как они работают. Помогать нам будут два наших персонажа, Хукума и Манифесто.
🔥9👍41
Небольшая затравочка перед нормальными постами🤪
👍2
​​Начнем серию постов с темы: «как работает UI с точки зрения многопоточности🧵».

Представьте прогу с одним потоком, все легко и прозрачно, не нужно думать про многопоточность и про все её проблемы🦄. Однако есть минус, пока прога работает, пользователь просто ждет и ничего не может сделать (напоминает Россию).

Ок, увеличим количество потоков, теперь приложение отзывчивое, и можно делать задачи параллельно, в чем проблема?

Проблема в том, что у нас есть UI слой, на котором мы двигаем и рисуем кнопки, возникает вопрос, как его обновлять из разных потоков? В голову приходит два решения:
☝️Просто сделать все View многопоточными и тогда их можно будет обновлять откуда угодно и не париться.
✌️Выделить для UI один специальных поток, и обновлять только через него.

🦾 Лучшие инженеры в свое время пытались сделать 1-й вариант и все попытки провалились. Как показала практика почти нереально сделать так, чтобы View была многопоточной, было бы безумно сложно не словить DeadLock.

🧐 Представьте что вы на View делаете переменную isAnimationRunning, а теперь она может меняться из разных потоков, сейчас она true, а через мгновение уже false. А если просто навесить по всюду synchronized, то очень сильно просядет производительность.

😅 Со специальным потоком гораздо проще, но вопрос по прежнему актуальный, как менять UI из других потоков? На помощь приходит старый добрый паттерн Consumer/Producer. В частности в Android он реализован за счет троицы пацанов Looper, Handler, MessageQueue, о которых так любят спрашивать на собесах и о которых мы поговорим далее…
👍14👏21
👍1
#android #ui
{1/4} Что такое Looper, как работает и что делает?

🙌 Представьте себе бесконечный цикл, допустим for(;;){}. Далее представим, что в этом цикле мы читаем из некоторой очереди Queue<Runnable> значения и выполняем их, получается что-то вроде:

Queue<Runnable> queue;
for(;;){
final Runnable runnable = queue.take();
runnable.run();
}

Это и есть вся суть Looper. Просто бесконечный цикл который получает из очереди сообщения и их выполняет.

Чтобы создать Looper нужно вызвать метод Looper.prepare(). После этого метод Looper.prepare() сохраняет созданный объект в статическое поле типа ThreadLocal.

Реализация инициализации лупера довольна простая, и при этом позволяет в любом месте программы и из любого треда получить лупер, связанный с текущим тредом. Статический метод Looper.myLooper() просто достает лупер из переменной ThreadLocal.

Далее мы запускаем Looper при помощи метода Looper.loop() он уходит в бесконечный цикл, который мы обсудили выше. В следующих постах обсудим что за сообщения, и кто их посылает.
👍23
👍3😁2
#android #ui
{2/4} В прошлом посте мы поговорили про Looper, там упоминалась некоторая очередь Queue<Runnable>. Давай-те подробнее о ней поговорим.

В реальности есть два отличия:
☝️- это не просто очередь из Collection, это отдельный класс, который так и называется MessageQueue
✌️- внутри очереди не просто Runnable, а специальные объекты, которые называются Message

Начнем с класса Message. В классе есть много полей, но нас сейчас интересует только 3️⃣ это callback, when и next.
callback – тот самый Runnable, который будет исполнен Looper'ом
next - ссылка на следующее сообщение
when - просто поле типа long, которое является 🕑временем, когда это сообщение должно быть выполнено

MessageQueue – простой односвязный список. Если заглянуть в MessageQueue то увидим, что там просто одно поле mMessages типа Message. У каждого Message есть ссылка на следующее сообщение Message.next. Другими словами, MessageQueue хранит только ссылку на первое сообщение.

Сообщения в MessageQueue отсортированы по возрастанию значения поля Message.when. Looper вызывает метод MessageQueue.next() в цикле, и получает отсортированное сообщение, которое нужно выполнить, если же очередь пуста, метод MessageQueue.next() блокирует цикл до тех пор, пока сообщение не появится.

Чтобы положить сообщение в очередь нужно вызвать метод MessageQueue.enqueueMessage(). Метод MessageQueue.enqueueMessage() проходит по очереди, проверяя значение Message.when каждого из сообщений и вставляет новое сообщение в положенное место очереди.

Как создается сообщение? Вручную сообщение лучше не создавать, для создания лучше использовать метод Message.obtain(). Message.obtain() возвращает объект message из пула, который представляет собой связный список максимальным размером 5️⃣0️⃣ сообщений. Если все сообщения пула используются, то Message.obtain() создает и возвращает новый объект Message.
👍4🔥3
​​{3/4} В этом посте используется код на Kotlin, просьба Java дИдов не пугаться!

Понемногу у нас с вами складывается картина того, каким образом работает UI в android. Мы разобрали что такое Looper, что такое Message и MessageQueue. Узнали, что в Looper посылаются задачи через MessageQueue. Возникает вопрос, как именно послать задачу в Looper, так как прямого доступа к MessageQueue у нас нет. Тут на сцену выходит Handler.

Прежде чем начнем разбирать Handler введем 2️⃣ понятия:
☝️поток consumer - поток, который ждет сообщения, тот, который вызывал Looper.loop().
✌️поток producer - поток, который создает сообщения и посылает их потоку consumer через Handler.
Поток consumer и поток producer могут быть одним потоком, как это может быть разберем далее.

Каждый Android разработчик хотя бы раз в своих проектах использовал Handler. Как уже сказано выше, нужен он для того, чтобы посылать задачи из потока producer в поток consumer.

У Handler есть несколько конструкторов, интересные пожалуй только 2️⃣ это:
☝️дефолтый коструктор без аргументов Handler()
✌️конструктор с аргументом типа Looper Handler(looper: Looper)

Когда используют конструктор без Looper, Handler пытается найти его через Looper.myLooper() и если его не находит, то падает.
Допустим в Activity.onCreate вы вызовете Handler().post{ doSmth() } - тут создается Handler, который через метод Looper.myLooper() получает Looper который привязан к Main Thread🧶, что аналогично записи Handler(Looper.getMainLooper()).post{ doSmth() }. Но если попытаемся тоже самое сделать на потоке без Looper, конструктор упадет💣:

Thread {
val handler = Handler() // - тут мы упадем
, так как у этого потока нет Looper🔁, подробнее смотри в посте про Looper
handler.post { doSmth() }
}.start()

Конструктор с входным параметром в виде Looper предпочтительнее, так как тогда вы явно задаете Looper🔁, в который будут post’иться сообщения. С ним все довольно очевидно, на вход нужно подать Looper в который будут посылаться задачи.

Теперь посмотрим на слудующий кусок кода:

fun onCreate() { // этот метод вызывает система
Handler().post {
Log.d("hello from handler thread ${Thread.currentThread().name}")
}
Log.d("hello from onCreate thread ${Thread.currentThread().name}")
}

Для начала попробуйте ответить сами, что будет выведено в лог В логах мы увидим следующую последовательность:

- "hello from onCreate thread main"
- "hello from handler thread main"

Когда вызываем метод post, переданный Runnable оборачивается в Message и через MessageQueue подается на Looper consumer потока. Однако система все методы жизненного цикла активности тоже вызывает через Looper потока Main🧶, даже сам метод onCreate() вызывался примерно так:

val activity = getCurrentActivity()
val handler = Handler()
handler.post {
activity.onCreate()
}

Следовательно, когда мы вызываем Handler().post{ Log.d("hello from handler thread ${Thread.currentThread().name}") } эта задача кладется в очередь и выполнится после того, как завершится метод onCreate().

Этот пример очень сильное упрощение того, что происходит на самом деле, но суть та же. Это тот случай, когда поток consumer и поток producer это один и тот же поток. Именно этим фактом, обуславливается асинхронность в UI. Когда мы создаем транзакцию для показа фрагмента, после метода commit эта транзакция также кладется в MessageQueue через Handler, и выполняется позже Looper’ом Main Thread🧶, также происходит и с показом новой🆕 Activity и многими другими вещами вроде анимаций и т.п.

Стоит упомянуть еще одну интересную особенность, так уж вышло, что огромное количество багов связаных с View на Android можно решить просто отложив задачу Handler().postDelayed(100) { //doSmth }.

Помните мы разбирали, что у Message есть специальное поле when, так вот, когда вызываем post у Handler, там используется SystemClock.uptimeMillis()), наподобие System.getCurrentTimeMillis(), а когда вызываем postDelayed, то входной аргумент delay прибавляется к SystemClock.uptimeMillis()) и записывается в поле when, а дальше магия сортировки, которая обсуждалась туть.
👍241🔥1
{4/4} Разобрав тему Handler стоит упомянуть однуинтересную особенность работы с Handler через View.

У каждой View в Android также есть методы post(), postAtTime(), postDelayed(), аналогичные тем, что есть у Handler, но работают немного прикольнее. Они сперва проверяют есть ли в данный момент AttachInfo, или по-другому, приатачена ли View к компоненту, например к Activity.

Если AttachInfo не равен null, тогда Message просто кладется в Handler который есть у этого самого AttachInfo, т.е в Handler главного потока, тот который Handler(Looper.getMainLooper())

Если же AttachInfo в данный момент равен null, т.е View еще не приатачена к компоненту, то Message кладется в специальную очередь, которая уникальна для каждой View. Затем, когда View приатачится к компоненту, система пробежится по этой очереди и запустит все Messages, которые были в очереди в Handler.

Почему это важно знать ❗️.

Когда работаем с Handler через View нужно вручную удалить все задачи, которые еще не выполнены, иначе компонент может утечь💧 или просто упасть💣, так как View уже не будет. Это актуально для длительных задач, если запускаем задачу с delay меньше секунды, то можно забить. 

Однако, если мы делаем что-то вроде refreshlayout.postDelayed(4000) { refreshlayout.isEnabled = false }, и при этом сами не очищаем очередь View вызвав refreshlayout.removeCallbacks(runnable) то можем упасть 💣, так как задача может быть вызвана даже когда уйдем с этого экрана. 

Поэтому для длительных задач 🕰 (которые бывают очень редко) сохраняем Runnable(тот который пихаем в метод post())в поле Fragment/Activity и удаляем его ручками на onDestroyView/onDestroy через метод removeCallbacks(runnable).
👍221
#ui #android
Большой респект тем, кто прочитал первую серию постов. Серию можно перечитывать перед собесами, и тогда вам не будет равных по вопросу Handler, Looper и MessageQueue.

Здесь что-то вроде оглавления:
- Пост про Looper
- Пост про MessageQueue
- Пост про Handler
- Дополнение к посту про Handler
👍32