Новый пост, новая проблема, сегодня поговорим о Reordering. 👩🏫 Начнем с определения: в многопоточной среде результаты операций, произведённых другими потоками, могут наблюдаться не в том порядке, в котором мы ожидаем. Лааадно, вы же не думали, что я тут буду душнить. 🙌 Как всегда, разберем на пальцах пример:
Этот код может показаться немного запутанным, но это самый показательный пример который я смог придумать. Итак, вопрос, что будет выведено в консоль?
Кто читал прошлые посты уже догадываются какой ответ 🙃. Варианты которые тут могут быть: 0,0; 0,1; 1,0; 1,1. Разберем каждый из кейсов.
👉 Кейс первый 0 и 1. С этим кейсом все просто, представляем, что потоки у нас стартуют одновременно без задержек, тогда в переменную
👉 Кейс второй 0 и 0. В данном случае второй поток стартует с некоторой задержкой, и первый поток успевает изменить переменную
👉 Кейс третий 1 и 1. В этом кейсе наоборот первый поток стартует с задержкой, и второй поток успевает затереть переменную
👉 Кейс четвертый 1 и 0. Самый загадочный из всех кейсов, воспроизвести его безумно сложно, практически нереально, но в теории возможно. Как такое может произойти? Вкратце, это еще одна оптимизация которую может сделать компилятор, процессор или окружение.
☝️Для начала договоримся, что действие это либо запись, либо чтение с переменной. Как вы все знаете компиляторы и процессоры очень сложные штуки 🧐. Компилятор может переставить действия местами если посчитает, что так будет быстрее. 💻 Процессор и JVM могут выполнять действия не в том порядке как они расположены в коде, и могут выполнять их как им это покажется нужным.
Еще раз наглядно на коде:
Нет никакой гарантии того, что операции присваивания в
Следовательно, в последнем кейсе это и произошло, инструкции с присваиванием поменялись местами. JVM спокойно могла их выполнить не в том, порядке в котором они расположены, так как это никак не повлияет на логику. Другими словами было:
Стало:
Избежать этих перестановок можно, а как и когда это нужно поговорим позже)
var x = 0
var y = 1
thread {
var a = x
y = 0
print(a)
}
thread {
var b = y
x = 1
print(b)
}
Этот код может показаться немного запутанным, но это самый показательный пример который я смог придумать. Итак, вопрос, что будет выведено в консоль?
Кто читал прошлые посты уже догадываются какой ответ 🙃. Варианты которые тут могут быть: 0,0; 0,1; 1,0; 1,1. Разберем каждый из кейсов.
👉 Кейс первый 0 и 1. С этим кейсом все просто, представляем, что потоки у нас стартуют одновременно без задержек, тогда в переменную
a
у нас сохранится значение 0, а в переменную b
значение 1, все просто.👉 Кейс второй 0 и 0. В данном случае второй поток стартует с некоторой задержкой, и первый поток успевает изменить переменную
y
. В этом случае в переменную a
у нас сохраняется значение 0 и в переменную b
тоже сохраняется значение 0.👉 Кейс третий 1 и 1. В этом кейсе наоборот первый поток стартует с задержкой, и второй поток успевает затереть переменную
x
. Тогда в переменную a
у нас сохранится значение 1, и в переменную b
значение 1, в целом все очевидно .👉 Кейс четвертый 1 и 0. Самый загадочный из всех кейсов, воспроизвести его безумно сложно, практически нереально, но в теории возможно. Как такое может произойти? Вкратце, это еще одна оптимизация которую может сделать компилятор, процессор или окружение.
☝️Для начала договоримся, что действие это либо запись, либо чтение с переменной. Как вы все знаете компиляторы и процессоры очень сложные штуки 🧐. Компилятор может переставить действия местами если посчитает, что так будет быстрее. 💻 Процессор и JVM могут выполнять действия не в том порядке как они расположены в коде, и могут выполнять их как им это покажется нужным.
Еще раз наглядно на коде:
x = 3
y = 5
Нет никакой гарантии того, что операции присваивания в
x
будет выполнена первой. Если у вас однопоточная программа, то эти перестановки вообще никак не влияют на ее выполнение, и можно вообще на это забить. 🧵Однако если у программы несколько потоков, и они еще и обращаются к одному и тому же месту, то всегда помните что JVM, компилятор или процессор могут переупорядочить действия.Следовательно, в последнем кейсе это и произошло, инструкции с присваиванием поменялись местами. JVM спокойно могла их выполнить не в том, порядке в котором они расположены, так как это никак не повлияет на логику. Другими словами было:
var b = y
x = 1
Стало:
x = 1
var b = y
Избежать этих перестановок можно, а как и когда это нужно поговорим позже)
👍3❤1
Happens-before. На каждом собесе где меня спрашивали про многопоточность, задавали вопрос про Happens-before. В целом это не сложная концепция, но порой сложно конкретно ответить на этот вопрос, давай те разберем и эту тему.
Начнем с того, что это не проблема многопоточности, а скорее некоторая абстракция, или даже набор правил. Для наглядности начнем с кода. Представим, что у нас есть две функции
Далее у нас есть два потока, поток first и поток second, функция
Теперь следим за руками, если сказано, что
Помните мы разбирали, проблему с Visibility, так вот если сказано, что гарантируется
Как использовать это на практике? Самый простой способ просто использовать синхронизацию:
Представим, что поток first точно запуститься первым. В примере получается, что
Для примера, несколько вещей в java в которых гарантируется happens-before:
👉Правило запуска потока. Вызов Thread.start на потоки происходит перед каждым действием в запущенном потоке.
👉Правило мониторного замка. Операция unlock на мониторном замке происходит перед каждой последующей операцией lock на том же самом мониторном замке.
👉Правило финализатора. Завершение конструктора объекта происходит перед началом финализатора этого объекта.
🧐Немного запутанная штука, но по сути достаточно понять что показано в картинке и уметь это как-то объяснить.
Начнем с того, что это не проблема многопоточности, а скорее некоторая абстракция, или даже набор правил. Для наглядности начнем с кода. Представим, что у нас есть две функции
operationFirst()
и operationSecond()
, которые что-то делают, как-то изменяют состояние объекта:
class Some {
private var x = 0
private var y = 0
fun operationFirst(){
x++
}
fun operationSecond(){
y++
}
}
Далее у нас есть два потока, поток first и поток second, функция
operationFirst()
вызывается в потоке first, функция operationSecond()
вызывается в потоке Second.
val some = Some()
val first = thread { some.operationFirst() }
val second = thread { some.operationSecond() }
Теперь следим за руками, если сказано, что
operationFirst() happens-before operationSecond()
это означает что все изменения, которые сделал поток first до момента вызова функции operationFirst()
и изменения, которые произошли в самой функции operationFirst()
будут видны потоку second в момент вызова функции operationSecond()
. Помните мы разбирали, проблему с Visibility, так вот если сказано, что гарантируется
operationFirst() happens-before operationSecond()
, то это значит, что проблемой с Visibility точно не будет, поток second точно увидит актуальное значение переменных. Мы также затрагивали проблему Reordering? Если гарантируется happens-before, то переупорядочивание нам тоже не страшно.Как использовать это на практике? Самый простой способ просто использовать синхронизацию:
val lock = Lock()
val first = thread { lock.withLock { some.operationFirst() } }
val second = thread { lock.withLock { some.operationSecond() } }
first.join()
second.join()
Представим, что поток first точно запуститься первым. В примере получается, что
operationFirst() happens-before operationSecond()
, следовательно, все что будет сделано в потоке first, увидит поток second в момент исполнения функции operationSecond()
.Для примера, несколько вещей в java в которых гарантируется happens-before:
👉Правило запуска потока. Вызов Thread.start на потоки происходит перед каждым действием в запущенном потоке.
👉Правило мониторного замка. Операция unlock на мониторном замке происходит перед каждой последующей операцией lock на том же самом мониторном замке.
👉Правило финализатора. Завершение конструктора объекта происходит перед началом финализатора этого объекта.
🧐Немного запутанная штука, но по сути достаточно понять что показано в картинке и уметь это как-то объяснить.
👍4❤1
Последняя проблема в списке, но не по значению – DeadLock. DeadLock один из сбоев жизнеспособности. Эта самая популярная проблема многопоточности на практике, которая приносит больше всего проблем. Суть проблемы очень проста, её можно описать небольшим снипетом кода:
☝️Значит есть два потока, которые ждут друг друга, и итоге программа зависает и никуда не продвигается. Пример кода объясняющий суть немного синтетический, на практике DeadLock возникает немного по другой причине. Еще один пример с кодом, погнали:
🧵 Поток A вызывает метод
Решил сильно не растягивать пост, этого достаточно для базового понимания того, что такое DeadLock. Помимо причин приведённых в посте есть еще множество способов вызывать DeadLock. Также есть еще различные виды сбоев жизнеспособности которые мы возможно обсудим отдельно.
И как всегда проблему обозначили, а как её диагностировать и решать обсуждаем отдельно.
val treadList = mutableListOf<Thread>()
treadList += Thread { thread[1].join() }
treadList += Thread { thread[0].join() }
treadList.forEach { it.start() }
☝️Значит есть два потока, которые ждут друг друга, и итоге программа зависает и никуда не продвигается. Пример кода объясняющий суть немного синтетический, на практике DeadLock возникает немного по другой причине. Еще один пример с кодом, погнали:
class LeftRightDeadlock {
private val leftLock = ReentrantLock()
private val rightLock = ReentrantLock()
fun leftRight() {
leftLock.withLock {
rightLock.withLock {
doSomething()
}
}
}
fun rightLeft() {
rightLock.withLock {
leftLock.withLock {
doSomething()
}
}
}
}
🧵 Поток A вызывает метод
leftRight()
, в этот момент поток B вызывает метод rightLeft()
, так как они захватывают замки в разных порядках, оба потока ждут освобождения ресурса, которые не будут освобождены, как на рисунке. Решил сильно не растягивать пост, этого достаточно для базового понимания того, что такое DeadLock. Помимо причин приведённых в посте есть еще множество способов вызывать DeadLock. Также есть еще различные виды сбоев жизнеспособности которые мы возможно обсудим отдельно.
И как всегда проблему обозначили, а как её диагностировать и решать обсуждаем отдельно.
👍6❤1
Начнем с решением проблемы Visibility.
Итак, два потока🧵, меняют переменную, которая сначала записывается в кеши, а не в оперативную память. Есть 2️⃣ варианта решения проблемы.
👉Первый это модификатор volatile. В языках java, c++, c# для этого существует специальный модификатор volatile (в языке kotlin это делается при помощи аннотации @Volatile). Когда вы ставите этот модификатор над переменной, это говорит компилятору и окружению о том, что когда мы работаем с этой переменной, сразу записывать данное значение в оперативную память минуя кеши.
👉Второй вариант использовать такую штуку, как монитор. В java монитор реализован при помощи замков, которые мы упоминали в прошлом посте. Если не углубляться в подробности, при помощи этой штуки мы говорим, что допустим вот эту функцию одновременно может вызывать лишь один поток. Остальные просто будут ожидать. Если мы с захватом монитора модифицируем переменную, у нас автоматически решается проблема Visibility. Однако, чтение тоже должно быть с захватом монитора.
Вот так просто мы решаем проблему Visibility.
Итак, два потока🧵, меняют переменную, которая сначала записывается в кеши, а не в оперативную память. Есть 2️⃣ варианта решения проблемы.
👉Первый это модификатор volatile. В языках java, c++, c# для этого существует специальный модификатор volatile (в языке kotlin это делается при помощи аннотации @Volatile). Когда вы ставите этот модификатор над переменной, это говорит компилятору и окружению о том, что когда мы работаем с этой переменной, сразу записывать данное значение в оперативную память минуя кеши.
👉Второй вариант использовать такую штуку, как монитор. В java монитор реализован при помощи замков, которые мы упоминали в прошлом посте. Если не углубляться в подробности, при помощи этой штуки мы говорим, что допустим вот эту функцию одновременно может вызывать лишь один поток. Остальные просто будут ожидать. Если мы с захватом монитора модифицируем переменную, у нас автоматически решается проблема Visibility. Однако, чтение тоже должно быть с захватом монитора.
Вот так просто мы решаем проблему Visibility.
❤3👍1
Была небольшая пауза, но мы идем дальше. Следующая проблема Atomicity.
Значит у нас есть Long/Double которые 64 бита, и при записи записываются первые 32 бита, затем вторые, когда один поток 👍, когда больше 👎.
Решение у этой проблемы аналогично предыдущей. Если у полей класса, типа Long/Double поставить модификатор volatile, это прикажет окружению записывать данные атомарно. Другими словами теперь операция записи/чтения в Long/Double будут происходить в одну операцию, и теперь не паримся если несколько потоков. ☝️Важно запомнить что если модифицируем переменную из нескольких потоков, и переменная Long/Double обязательно ставим volatile.
И конечно второй вариант использовать монитор. Если чтение/запись в поле Long/Double будет происходить через использование монитора, то в этом случае у нас тоже гарантируется атомарность. Это очевидно ведь мы используем монитор, а это означает что другие потоки в этот момент ждут. В таком случае, даже если операция записи будет в 5 операций это ни на что не повлияет. ☝️Однако помните, что в этом случае и чтение и запись, должны быть из монитора иначе вся магия пропадает.
Значит у нас есть Long/Double которые 64 бита, и при записи записываются первые 32 бита, затем вторые, когда один поток 👍, когда больше 👎.
Решение у этой проблемы аналогично предыдущей. Если у полей класса, типа Long/Double поставить модификатор volatile, это прикажет окружению записывать данные атомарно. Другими словами теперь операция записи/чтения в Long/Double будут происходить в одну операцию, и теперь не паримся если несколько потоков. ☝️Важно запомнить что если модифицируем переменную из нескольких потоков, и переменная Long/Double обязательно ставим volatile.
И конечно второй вариант использовать монитор. Если чтение/запись в поле Long/Double будет происходить через использование монитора, то в этом случае у нас тоже гарантируется атомарность. Это очевидно ведь мы используем монитор, а это означает что другие потоки в этот момент ждут. В таком случае, даже если операция записи будет в 5 операций это ни на что не повлияет. ☝️Однако помните, что в этом случае и чтение и запись, должны быть из монитора иначе вся магия пропадает.
👍3
Решение проблемы Reordering.
😅Это довольно забавно, но для решения этой проблемы инструменты не меняются. Снова 2️⃣ варианта.
👉 Модификатор volatile, запрещает окружению переставлять действия местами. Это значит, чтобы, избежать четвертого кейса который мы разбирали тут, достаточно поставить volatile перед полями класса.
👉 И конечно же вариант использовать монитор. Если производить операции будет только один поток, а второй будет ждать очереди, проблема Reordering просто отпадает.
😅Это довольно забавно, но для решения этой проблемы инструменты не меняются. Снова 2️⃣ варианта.
👉 Модификатор volatile, запрещает окружению переставлять действия местами. Это значит, чтобы, избежать четвертого кейса который мы разбирали тут, достаточно поставить volatile перед полями класса.
👉 И конечно же вариант использовать монитор. Если производить операции будет только один поток, а второй будет ждать очереди, проблема Reordering просто отпадает.
👍3
Последняя проблема из списка, Deadlock. 🔐
Вероятность того, что вы столкнетесь с такой проблемой в продакшен коде крайне мала, однако стоит хотя бы примерно понимать как искать проблему 🔎, если все таки стрельнет.
☝️Для начала стоит запомнить фразу: "Программа, которая никогда не приобретает более одного замка за один раз, не может столкнуться с взаимной блокировкой, которая инициируется порядком блокировки." Это значит, что когда пишете код, старайтесь делать так, чтобы поток не захватывал более двух замков сразу. Если по другому никак, то проверяйте, чтобы все потоки захватывали замки в одном и том же порядке.
Есть три основных инструмент которые вы можете использовать для того, чтобы найти причину deadlock:
👉 Поточный дамп
👉 API явных замков
👉 Статические анализаторы кода и ревью кода
Вкратце пройдемся по каждому инструменту.
В JVM есть механизм, по которому мы можем получить информацию о том, что делает каждый конкретный поток. Используя этот инструмент можно найти где именно у нас происходит deadlock. Для этого запускаем программу, затем приказываем JVM собрать поточный дамп, обычно для этого есть спец кнопка📌 в IDE. Затем мы получаем некоторый листинг. В листинге нужно найти потоки состояние которых: "waiting to lock monitor". Это знак того, что скорее всего поток ждет ресурс который другой поток не может освободить. В иллюстрации с посту есть пример того, как выглядит этот листинг.
Второй инструмент это использовать явные замки 🔐, т.е объекты типа Lock. В API этих замков есть метод tryLock с timeout ⏲ который мы можем задать. Суть этого метода сводится к тому, что мы просто заменяем обычные методы lock на tryLock с каким-нибудь временем, например 3 секунды. Затем запускаем программу и начинаем тестить, если программа упала 💥, мы явно увидим где конкретно, а дальше уже отталкиваясь от этого можно найти конкретную причину.
Последний инструмент, наверное самый лучший из всех, это наши глаза 👀 и статические анализаторы кода 🤖. На данный момент на рынке очень много линтеров, которые умеют подсказывать о возможных ошибках в многопоточности из коробки. Стоит погуглить их и затащить в свой проект. Ну и конечно никто не отменяет код ревью, очень внимательно проверять места, где используются примитивы многопоточности 🧵. Если мы говорим про Android разработку, то таких мест будет крайне мало, так как Rx и корутины покрывают 90% кейсов. Во всех остальных случаях супер внимательно смотрим не наговнокодил ли коллега👨💻 и очень внимательно с максимальной критикой просматриваем свой код.
Этого должно хватить если вы вдруг столкнулись с такой проблемой.
Вероятность того, что вы столкнетесь с такой проблемой в продакшен коде крайне мала, однако стоит хотя бы примерно понимать как искать проблему 🔎, если все таки стрельнет.
☝️Для начала стоит запомнить фразу: "Программа, которая никогда не приобретает более одного замка за один раз, не может столкнуться с взаимной блокировкой, которая инициируется порядком блокировки." Это значит, что когда пишете код, старайтесь делать так, чтобы поток не захватывал более двух замков сразу. Если по другому никак, то проверяйте, чтобы все потоки захватывали замки в одном и том же порядке.
Есть три основных инструмент которые вы можете использовать для того, чтобы найти причину deadlock:
👉 Поточный дамп
👉 API явных замков
👉 Статические анализаторы кода и ревью кода
Вкратце пройдемся по каждому инструменту.
В JVM есть механизм, по которому мы можем получить информацию о том, что делает каждый конкретный поток. Используя этот инструмент можно найти где именно у нас происходит deadlock. Для этого запускаем программу, затем приказываем JVM собрать поточный дамп, обычно для этого есть спец кнопка📌 в IDE. Затем мы получаем некоторый листинг. В листинге нужно найти потоки состояние которых: "waiting to lock monitor". Это знак того, что скорее всего поток ждет ресурс который другой поток не может освободить. В иллюстрации с посту есть пример того, как выглядит этот листинг.
Второй инструмент это использовать явные замки 🔐, т.е объекты типа Lock. В API этих замков есть метод tryLock с timeout ⏲ который мы можем задать. Суть этого метода сводится к тому, что мы просто заменяем обычные методы lock на tryLock с каким-нибудь временем, например 3 секунды. Затем запускаем программу и начинаем тестить, если программа упала 💥, мы явно увидим где конкретно, а дальше уже отталкиваясь от этого можно найти конкретную причину.
Последний инструмент, наверное самый лучший из всех, это наши глаза 👀 и статические анализаторы кода 🤖. На данный момент на рынке очень много линтеров, которые умеют подсказывать о возможных ошибках в многопоточности из коробки. Стоит погуглить их и затащить в свой проект. Ну и конечно никто не отменяет код ревью, очень внимательно проверять места, где используются примитивы многопоточности 🧵. Если мы говорим про Android разработку, то таких мест будет крайне мало, так как Rx и корутины покрывают 90% кейсов. Во всех остальных случаях супер внимательно смотрим не наговнокодил ли коллега👨💻 и очень внимательно с максимальной критикой просматриваем свой код.
Этого должно хватить если вы вдруг столкнулись с такой проблемой.
👍6❤1
Большая часть проголосовала за IPC (я кстати сам его тоже выбрал), поэтому похардкодим немножечко🦾...
Для начала немного вспомним CS, а конкретнее разницу м/у потоком и процессом. Нужно понимать разницу, чтобы понять что такое вообще IPC, поэтому пройдемся поверхностно.
📝 Процесс это некоторый объект в системе, которому ОС выделяет время процессора, память, доступ к сети, доступ к железкам и т.п. Запускаете программу, ОС для этой программы создает процесс, который получает время процессора и память.
👣Идем дальше, каждый процесс в системе как бы интроверт, он не может обратиться к участку памяти который выделен другому процессу. И это понятно почему, было бы крайне не приятно если бы левая прога начала читать участок памяти где крутится банковское приложение.
У каждого процесса есть состояние. Упростим до 2-х, он может быть либо активен, либо быть приостановлен. Одно из неудобств программирования под Android, что если пользователь свернет наше приложение, система может грохнуть 🧨 наш процесс. Под "грохнуть процесс" подразумевается, что ОС не видит смысла больше выделять оперативную память для этого процесса (нашего приложения) и просто выгружает его, или по другому забирает память 👨🦳.
В таком случае, все наши данные которые мы сохраняли в static поля или во ViewModel просто убьются. Однако данные которые успели записаться в Bundle потом восстановятся, не забываем про это.
🧵 Далее идет поток. Поток в некотором грубом смысле это более легкая версия процесса. Мы открываем приложение, система создает для него процесс. Уже в этом процессе мы можем создавать потоки, которые по сути являются просто участками кода которые выполняются паралельно.
🧵 Потоки в отличие от процессов уже имеют доступ к памяти друг друга, потому как находятся в общем участке памяти процесса. Значит создав переменную мы спокойно можем менять её значение из нескольких потоков. Конечно есть свои сложности, но это можно легко сделать.
📵С процессом такое не проканает, нельзя сделать переменную которую можно поменять их нескольких процессов. По сути процессы мало что знаю друг о друге.
Бывают случаи когда нам нужно или получить данные с другого процесса, или наоборот что-то сообщить процессу. Что это за случай разберем в след постах.
Для начала немного вспомним CS, а конкретнее разницу м/у потоком и процессом. Нужно понимать разницу, чтобы понять что такое вообще IPC, поэтому пройдемся поверхностно.
📝 Процесс это некоторый объект в системе, которому ОС выделяет время процессора, память, доступ к сети, доступ к железкам и т.п. Запускаете программу, ОС для этой программы создает процесс, который получает время процессора и память.
👣Идем дальше, каждый процесс в системе как бы интроверт, он не может обратиться к участку памяти который выделен другому процессу. И это понятно почему, было бы крайне не приятно если бы левая прога начала читать участок памяти где крутится банковское приложение.
У каждого процесса есть состояние. Упростим до 2-х, он может быть либо активен, либо быть приостановлен. Одно из неудобств программирования под Android, что если пользователь свернет наше приложение, система может грохнуть 🧨 наш процесс. Под "грохнуть процесс" подразумевается, что ОС не видит смысла больше выделять оперативную память для этого процесса (нашего приложения) и просто выгружает его, или по другому забирает память 👨🦳.
В таком случае, все наши данные которые мы сохраняли в static поля или во ViewModel просто убьются. Однако данные которые успели записаться в Bundle потом восстановятся, не забываем про это.
🧵 Далее идет поток. Поток в некотором грубом смысле это более легкая версия процесса. Мы открываем приложение, система создает для него процесс. Уже в этом процессе мы можем создавать потоки, которые по сути являются просто участками кода которые выполняются паралельно.
🧵 Потоки в отличие от процессов уже имеют доступ к памяти друг друга, потому как находятся в общем участке памяти процесса. Значит создав переменную мы спокойно можем менять её значение из нескольких потоков. Конечно есть свои сложности, но это можно легко сделать.
📵С процессом такое не проканает, нельзя сделать переменную которую можно поменять их нескольких процессов. По сути процессы мало что знаю друг о друге.
Бывают случаи когда нам нужно или получить данные с другого процесса, или наоборот что-то сообщить процессу. Что это за случай разберем в след постах.
👍4
После того как мы осознали разницу между потоком и процессом разберем задачу. Есть два (🪑) приложения, допустим одно приложение это калькулятор💻, а второе банковское приложение💰. Банковское приложение не умеет считать, следовательно, для расчета оно должно обратиться к приложению калькулятор, который как мы знаем находится в другом процессе. Как это сделать?
💡Ответом будет IPC. IPC расшифровывается как inter-process communication, или по-русски межпроцессная (не путать с межпроцессорным) коммуникация. Это механизм, который позволяет общаться процессам друг с другом.
У IPC есть куча методов, учитывая, что мы говорим в контексте системы Android, разберем основные которые можно использовать в этой системе:
🌐 По сети. Приложение калькулятор делает локальный сервер, а банковское приложение просто стучится к этому серверу. Два приложения на одном устройстве, значит даже интернет не нужен.
📁Через файл. Банковское приложение оставляет заявку на расчет в некотором файле. Калькулятор постоянно следит за этим файлом. Как только в нем оказывается заявка, калькулятор делает расчет и пишет результат в другой файл, который уже подхватывает банковское приложение.
👉Content provider. Эта штука специфична для платформы Android. Content provider один из основных компонентов Android приложения, который позволяет делиться данными нашего приложения с другими. Его например использует Яндекс . Когда вы залогинились в одном приложении, вы автоматически логинитесь в других приложениях Яндекса.
🌚Фреймворк Binder❓
У каждого варианта есть свои недостатки и свои плюсы. Рассмотрим недостатки каждого:
👉Вариант с сетью. Основная проблема в том, что система может неожиданно прибить 🧨 приложение сервер. Учитывая, что Android последних версий вообще ужесточает правила для приложений работающих в фоне, этот вариант почти нереален.
👉С файлом еще больше проблем: может закончиться место, другой процесс прибьет файл, приложение калькулятор может упасть и испортить файл, или система просто прибьет приложение калькулятор.
👉 С Content provider в этом плане все намного лучше, однако он очень ограничен . По своей сути работа с Content provider похожа на работу с обычной базой данных 🗄. Мы можем получить данные, или сохранить, но мы не можем вызывать функцию у другого процесса и попросить дернуть наше приложение позже с ответом.
И остается Binder, темная лошадка 🐎, о которой знают даже не все Android разработчики. Что это и как использовать разберем в следующих постах.
💡Ответом будет IPC. IPC расшифровывается как inter-process communication, или по-русски межпроцессная (не путать с межпроцессорным) коммуникация. Это механизм, который позволяет общаться процессам друг с другом.
У IPC есть куча методов, учитывая, что мы говорим в контексте системы Android, разберем основные которые можно использовать в этой системе:
🌐 По сети. Приложение калькулятор делает локальный сервер, а банковское приложение просто стучится к этому серверу. Два приложения на одном устройстве, значит даже интернет не нужен.
📁Через файл. Банковское приложение оставляет заявку на расчет в некотором файле. Калькулятор постоянно следит за этим файлом. Как только в нем оказывается заявка, калькулятор делает расчет и пишет результат в другой файл, который уже подхватывает банковское приложение.
👉Content provider. Эта штука специфична для платформы Android. Content provider один из основных компонентов Android приложения, который позволяет делиться данными нашего приложения с другими. Его например использует Яндекс . Когда вы залогинились в одном приложении, вы автоматически логинитесь в других приложениях Яндекса.
🌚Фреймворк Binder❓
У каждого варианта есть свои недостатки и свои плюсы. Рассмотрим недостатки каждого:
👉Вариант с сетью. Основная проблема в том, что система может неожиданно прибить 🧨 приложение сервер. Учитывая, что Android последних версий вообще ужесточает правила для приложений работающих в фоне, этот вариант почти нереален.
👉С файлом еще больше проблем: может закончиться место, другой процесс прибьет файл, приложение калькулятор может упасть и испортить файл, или система просто прибьет приложение калькулятор.
👉 С Content provider в этом плане все намного лучше, однако он очень ограничен . По своей сути работа с Content provider похожа на работу с обычной базой данных 🗄. Мы можем получить данные, или сохранить, но мы не можем вызывать функцию у другого процесса и попросить дернуть наше приложение позже с ответом.
И остается Binder, темная лошадка 🐎, о которой знают даже не все Android разработчики. Что это и как использовать разберем в следующих постах.
👍6
Тему я назвал хардкорной ⛓ не просто так, с каждым постом сложность будет возрастать, однако я постараюсь объяснить на пальцах, погнали 👉
☝️Мы разбираем тему отталкиваясь от проблемы. В прошлом посте мы разбирали пример с банковским приложением и калькулятором. Этот пример довольно синтетический, он далек от реальности. Разберем реальную проблему.
У нас на устройствах есть📍GPS и приложения, которые хотят использовать его: такси, карты, навигатор и т.д. Туева хуча приложений и давать прямой доступ довольно рискованно, потому как вдруг левое приложение захочет в фоне отслеживать нашу позицию🥷.
🤔Значит доступ к датчику GPS должен быть только у некоторого системного сервиса (по сути приложение, только без UI). Сервиса, который поставляется самой системой и которому мы можем доверять (не можем и гугл знает о вас все 🤷♂️).
👉 Теперь приложения не ходят на прямую к датчику GPS, а просят данные у системного сервиса, который в свою очередь решает кому эти данные отдавать, а кому нет, как часто и с какой точностью. Как реализовать такой механизм, в чем отличие этого сервиса от нашего приложения?
💻Каждое приложение в Android работает в своем процессе и со своим экземпляром JVM это называется "Песочница". Механизм песочницы основан на том, что каждому приложению назначается уникальный user ID (UID) и group ID (GID). Значит, у каждого приложения в Android есть свой непривилегированный пользователь 👨.
🤴 В системе также есть привилегированные пользователи, имена и идентификаторы которых жестко зашиты в систему⚙️. Они нужны для сервисов которые имеют доступ к критичным секциям ресурсов (GPS, контакты, ваши любовные смс-ки). У обычных приложений нет доступа к критичным секциям, так как у них непривилегированный пользователь 🤕.
🤗Все круто, безопасно, но как теперь данные то получать с сервиса, ведь наше приложение и сервис работают в разных процессах? Как вы уже догадались ни один из способов описанных в предыдущем посте не подойдет. И тут на сцену выходит Binder.
👉 Binder IPC это фреймворк межпроцессной коммуникации, который пришел на замену System V IPCs. Кто не в теме, System V IPCs по сути это набор низкоуровневых функций, которые реализованы на уровне ядра, позволяющие обмениваться данными между процессами так, будто у них есть общая память (надеюсь меня не читают лютые линуксойды, иначе закидают ссанымы тряпками за такое упрощение 😅).
👉Ну так вот, этот Binder IPC невероятно сложная и крутая штука, которая решает нашу проблему. Фреймворк позволяет синхронно и асинхронно вызывать методы удаленных объектов (значит методы приложений/сервисов, которые работают в другом процессе) так, будто они локальные.
🎛 Все остальные способы взаимодействия между процессами, включая Intents и Content Provider, реализованы используя Binder IPC. При помощи фреймворка мы можем попросить системный сервис дергать функции нашего приложения когда, допустим меняется локация пользователя 📍.
Что это такое мы разобрали, а как он работает разберем в следующем посте 🤚
☝️Мы разбираем тему отталкиваясь от проблемы. В прошлом посте мы разбирали пример с банковским приложением и калькулятором. Этот пример довольно синтетический, он далек от реальности. Разберем реальную проблему.
У нас на устройствах есть📍GPS и приложения, которые хотят использовать его: такси, карты, навигатор и т.д. Туева хуча приложений и давать прямой доступ довольно рискованно, потому как вдруг левое приложение захочет в фоне отслеживать нашу позицию🥷.
🤔Значит доступ к датчику GPS должен быть только у некоторого системного сервиса (по сути приложение, только без UI). Сервиса, который поставляется самой системой и которому мы можем доверять (не можем и гугл знает о вас все 🤷♂️).
👉 Теперь приложения не ходят на прямую к датчику GPS, а просят данные у системного сервиса, который в свою очередь решает кому эти данные отдавать, а кому нет, как часто и с какой точностью. Как реализовать такой механизм, в чем отличие этого сервиса от нашего приложения?
💻Каждое приложение в Android работает в своем процессе и со своим экземпляром JVM это называется "Песочница". Механизм песочницы основан на том, что каждому приложению назначается уникальный user ID (UID) и group ID (GID). Значит, у каждого приложения в Android есть свой непривилегированный пользователь 👨.
🤴 В системе также есть привилегированные пользователи, имена и идентификаторы которых жестко зашиты в систему⚙️. Они нужны для сервисов которые имеют доступ к критичным секциям ресурсов (GPS, контакты, ваши любовные смс-ки). У обычных приложений нет доступа к критичным секциям, так как у них непривилегированный пользователь 🤕.
🤗Все круто, безопасно, но как теперь данные то получать с сервиса, ведь наше приложение и сервис работают в разных процессах? Как вы уже догадались ни один из способов описанных в предыдущем посте не подойдет. И тут на сцену выходит Binder.
👉 Binder IPC это фреймворк межпроцессной коммуникации, который пришел на замену System V IPCs. Кто не в теме, System V IPCs по сути это набор низкоуровневых функций, которые реализованы на уровне ядра, позволяющие обмениваться данными между процессами так, будто у них есть общая память (надеюсь меня не читают лютые линуксойды, иначе закидают ссанымы тряпками за такое упрощение 😅).
👉Ну так вот, этот Binder IPC невероятно сложная и крутая штука, которая решает нашу проблему. Фреймворк позволяет синхронно и асинхронно вызывать методы удаленных объектов (значит методы приложений/сервисов, которые работают в другом процессе) так, будто они локальные.
🎛 Все остальные способы взаимодействия между процессами, включая Intents и Content Provider, реализованы используя Binder IPC. При помощи фреймворка мы можем попросить системный сервис дергать функции нашего приложения когда, допустим меняется локация пользователя 📍.
Что это такое мы разобрали, а как он работает разберем в следующем посте 🤚
👍11❤1
💻 Чтобы глубже и яснее понять как работает Binder IPC вернемся на шаг назад и вспомним что такое Service в Android. Документация Android говорит нам о том, что есть 3 типа сервисов: Background services, Foreground services, и Bound services. Нас интересует последний, Bound services. Cуть в том, что этот тип позволяет подключить Service к Activity и напрямую вызывать методы Service из Activity. Другими словами Bound service позволяет сделать общение между Activity и Service по клиент-серверной модели.
👉 Все из вас кто хоть когда-то залазил в класс Service в Android должны были заметить, что там есть метод onBind() который возвращает объект IBinder. В большинстве случаев мы просто забиваем и возвращаем null, но ведь этот метод там явно не просто так, да еще и интерфейс так называется IBinder, прям в тему, смекаете😉? Чтобы сделать Bound Service, необходимо определить класс, унаследованный от класса Binder и определить в нем некоторую логику которая будет исполняться на сервисе. Например, что-то вроде:
Затем нужно запустить этот сервис через специальный метод контекста bindService(). Когда сервис подключится к нашей Activity вызовется callback onServiceConnected() у объекта ServiceConnection. После чего мы приводим входной аргумент IBinder к нашему конкретному классу и можем напрямую вызывать методы сервиса из нашей Activity:
Это все, теперь можно сделать межпроцессное общение❓
Неа😄 это работает только в том случае, если и Service и Activity работают в одном процессе.
Тогда для чего такие сложности и зачем это нужно❓
🤷 По большей части если речь идет о выполнении в одном процессе оно и правда может быть лишним, так как данные можно передавать и через data слой посредством например RxJava.
Нам сейчас это нужно для понимания того, как работает Binder. Ведь для общения между двумя процессами, нужно лишь слегка исправить наш пример. А что конкретно нужно исправить, обсудим в следующем посте📝
👉 Все из вас кто хоть когда-то залазил в класс Service в Android должны были заметить, что там есть метод onBind() который возвращает объект IBinder. В большинстве случаев мы просто забиваем и возвращаем null, но ведь этот метод там явно не просто так, да еще и интерфейс так называется IBinder, прям в тему, смекаете😉? Чтобы сделать Bound Service, необходимо определить класс, унаследованный от класса Binder и определить в нем некоторую логику которая будет исполняться на сервисе. Например, что-то вроде:
class CalculatorService : Service() {
override fun onBind(intent: Intent?): IBinder = Calculator()
class Calculator : Binder() {
fun sum(first: Int, second: Int): Int = first + second
}
}
Затем нужно запустить этот сервис через специальный метод контекста bindService(). Когда сервис подключится к нашей Activity вызовется callback onServiceConnected() у объекта ServiceConnection. После чего мы приводим входной аргумент IBinder к нашему конкретному классу и можем напрямую вызывать методы сервиса из нашей Activity:
val intent = Intent(this, CalculatorService::class.java)
bindService(intent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val calculator = service as CalculatorService.Calculator
val result = calculator.sum(1, 2)
}
override fun onServiceDisconnected(name: ComponentName?) = Unit
}, BIND_AUTO_CREATE)
Это все, теперь можно сделать межпроцессное общение❓
Неа😄 это работает только в том случае, если и Service и Activity работают в одном процессе.
Тогда для чего такие сложности и зачем это нужно❓
🤷 По большей части если речь идет о выполнении в одном процессе оно и правда может быть лишним, так как данные можно передавать и через data слой посредством например RxJava.
Нам сейчас это нужно для понимания того, как работает Binder. Ведь для общения между двумя процессами, нужно лишь слегка исправить наш пример. А что конкретно нужно исправить, обсудим в следующем посте📝
👍4❤1👎1
В прошлом посте разобрали ситуацию как подключить Service к компоненту, чтобы обмениваться данными. Для этого мы использовали наследник класса Binder.
Возникает вопрос, что теперь делать если этот компонент и сервис находятся в разных процессах? Варианта два: Messenger и AIDL.
☝️Для начала рассмотрим вариант с Messenger. По сути Messenger это некая обертка над Handler, которая позволяет принимать сообщения из других процессов. У Messenger довольно простое API, рассмотрим на примере. Для начала делаем сервис:
👉 Затем уже известный код в Activity:
На этом все 👐, теперь можно отправлять сообщения в другой сервис. На практике Messenger используют крайне редко, функционал довольно ограничен, да и построено все на коллбэках.
Если мы захотим, чтобы такой сервис еще и отвечал обратно, т.е отправлял сообщения нам, то на клиенте мы тоже должны создавать объект Messenger, который нужно будет передавать в каждом сообщении. И опять-таки, сообщения мы будем принимать через Handler, что означает, что мы можем принимать сообщения только на том потоке, где этот Handler создан. Если кто-то хочет примеров код, то го сюда и сюда.
Чувствуете муторная фигня ☹️, очень много действий нужно сделать, и все общение при этом асинхронное. Мы же хотим, чтобы общение происходило синхронно, чтобы вызывать функции на другом процессе, как будто они в нашем, без этих сообщений и коллбэков.
Для этого опускаемся еще на уровень ниже ⤵️.
Возникает вопрос, что теперь делать если этот компонент и сервис находятся в разных процессах? Варианта два: Messenger и AIDL.
☝️Для начала рассмотрим вариант с Messenger. По сути Messenger это некая обертка над Handler, которая позволяет принимать сообщения из других процессов. У Messenger довольно простое API, рассмотрим на примере. Для начала делаем сервис:
class CalculatorMessengerService : Service() {
class IncomingHandler(private val context: Context, looper: Looper) : Handler(looper) {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_SAY_HELLO -> Toast.makeText(context, "hello", Toast.LENGTH_SHORT).show()
else -> super.handleMessage(msg)
}
}
}
override fun onBind(intent: Intent?): IBinder? {
val messenger = Messenger(IncomingHandler(this))
return messenger.binder
}
}
👉 Затем уже известный код в Activity:
val intent = Intent(this, CalculatorMessengerService::class.java)
bindService(intent, object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) = Unit
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val messenger = Messenger(service)
val message = Message.obtain(null, MSG_SAY_HELLO, 0, 0)
messenger.send(message)
}
}, BIND_AUTO_CREATE)
На этом все 👐, теперь можно отправлять сообщения в другой сервис. На практике Messenger используют крайне редко, функционал довольно ограничен, да и построено все на коллбэках.
Если мы захотим, чтобы такой сервис еще и отвечал обратно, т.е отправлял сообщения нам, то на клиенте мы тоже должны создавать объект Messenger, который нужно будет передавать в каждом сообщении. И опять-таки, сообщения мы будем принимать через Handler, что означает, что мы можем принимать сообщения только на том потоке, где этот Handler создан. Если кто-то хочет примеров код, то го сюда и сюда.
Чувствуете муторная фигня ☹️, очень много действий нужно сделать, и все общение при этом асинхронное. Мы же хотим, чтобы общение происходило синхронно, чтобы вызывать функции на другом процессе, как будто они в нашем, без этих сообщений и коллбэков.
Для этого опускаемся еще на уровень ниже ⤵️.
👍10
Второй способ взаимодействовать с сервисом на другом процессе это AIDL.
👉 AIDL или Android Interface Definition Language, это язык, который позволяет нам описывать интерфейсы межпроцессного общения. Язык чем-то похож на урезанную java с некоторыми интересными ключевыми словами.
🙌 Суть в чем, описываем в интерфейсе те функции какие мы хотим вызывать на другом процессе, а также входные аргументы и тип результата. Пример того, как может выглядеть этот интерфейс:
В простых примерах синтаксис вообще не отличается от java (кстати заметили ублюдский🤬 префикс "I" будто мы в C#, в AIDL по-прежнему используется этот пережиток прошлого). После того как мы написали интерфейс, компилятор генерирует классы Proxy и Stub.
Proxy – то, что использует клиент, чтобы вызывать функции другого процесса так, будто они находятся в одном процессе.
Stub – использует сервис, или по другому сервис теперь должен реализовать методы, которые мы определили в интерфейсе.
Проще будет понять на примере, сервис будет выглядеть так:
На стороне клиента мы должны теперь получить Proxy, делаем мы это через байндинг этого сервиса и уже сгенеренных методов ICalculator:
На этом все, теперь можно напряму вызывать методы другого процесса.
Proxy делает "маршалинг" (Marshalling), т.е перегоняет аргументы функций в последовательность байт. Stub соответственно делает "анмаршалинг" (Unmarshaling), перегоняет поток байт обратно в объекты, которые описаны в AIDL.
🤓 Еще раз пройдемся по всем компонентам которые у нас есть, советую параллельно смотреть в картинку.
У каждого Binder есть Token, по сути это просто некоторый desriptor, который помогает системе понять что это конкретно за реализация Binder. Если заглянуть в метод sum, который сгенерирован в proxy, мы увидим что там первой строчкой при маршалинге будет:
Эта строчка означает, что когда мы передаем массив байт в Stub, первые байты будут нести информацию о самом Binder.
Интерфейс IBinder это интерфейс, который описывает методы нужные для межпроцессной коммуникации. Binder стандартная реализация интерфейса IBinder, в Binder реализованы все сложные методы коммуникации и который мы можем расширять, чтобы использовать. Stub наследует Binder и нам остается только реализовать те методы которые мы указали в AIDL. Proxy использует объект IBinder, просто передать данные в Stub.
И теперь представь 🌈, все Intent, ContentProvider, все сервисы которые мы получаем через getSystemService() - все это работает через Binder и AIDL, круто не правда ли?
🎉 Если ты дочитал до сюда и примерно сможешь это пересказать, то я тебя поздравляю ты охренеть какая умничка и теперь сможешь выпендится на собесе. Тема очень сложная и используется только на очень больших проектах которым мало одного процесса или в каких-нибудь SDK, как например Яндекс.Метрика.
В посте было очень много упрощений, чтобы не делать его супер большим, если тебе понравилась тема стоит посмотреть этот доклад, который расскажет все это, но подробнее.
👉 AIDL или Android Interface Definition Language, это язык, который позволяет нам описывать интерфейсы межпроцессного общения. Язык чем-то похож на урезанную java с некоторыми интересными ключевыми словами.
🙌 Суть в чем, описываем в интерфейсе те функции какие мы хотим вызывать на другом процессе, а также входные аргументы и тип результата. Пример того, как может выглядеть этот интерфейс:
// ICalculator.aidl
interface ICalculator {
int sum(int first, int second);
}
В простых примерах синтаксис вообще не отличается от java (кстати заметили ублюдский🤬 префикс "I" будто мы в C#, в AIDL по-прежнему используется этот пережиток прошлого). После того как мы написали интерфейс, компилятор генерирует классы Proxy и Stub.
Proxy – то, что использует клиент, чтобы вызывать функции другого процесса так, будто они находятся в одном процессе.
Stub – использует сервис, или по другому сервис теперь должен реализовать методы, которые мы определили в интерфейсе.
Проще будет понять на примере, сервис будет выглядеть так:
override fun onBind(intent: Intent?): IBinder {
return object : ICalculator.Stub() {
override fun sum(first: Int, second: Int): Int {
return first + second
}
}
}
На стороне клиента мы должны теперь получить Proxy, делаем мы это через байндинг этого сервиса и уже сгенеренных методов ICalculator:
val intent = Intent(this, CalculatorMessengerService::class.java)
bindService(intent, object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) = Unit
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val proxy = ICalculator.Stub.asInterface(service)
val sum = proxy.sum(1, 4)
}
}, BIND_AUTO_CREATE)
На этом все, теперь можно напряму вызывать методы другого процесса.
Proxy делает "маршалинг" (Marshalling), т.е перегоняет аргументы функций в последовательность байт. Stub соответственно делает "анмаршалинг" (Unmarshaling), перегоняет поток байт обратно в объекты, которые описаны в AIDL.
🤓 Еще раз пройдемся по всем компонентам которые у нас есть, советую параллельно смотреть в картинку.
У каждого Binder есть Token, по сути это просто некоторый desriptor, который помогает системе понять что это конкретно за реализация Binder. Если заглянуть в метод sum, который сгенерирован в proxy, мы увидим что там первой строчкой при маршалинге будет:
_data.writeInterfaceToken(DESCRIPTOR);
Эта строчка означает, что когда мы передаем массив байт в Stub, первые байты будут нести информацию о самом Binder.
Интерфейс IBinder это интерфейс, который описывает методы нужные для межпроцессной коммуникации. Binder стандартная реализация интерфейса IBinder, в Binder реализованы все сложные методы коммуникации и который мы можем расширять, чтобы использовать. Stub наследует Binder и нам остается только реализовать те методы которые мы указали в AIDL. Proxy использует объект IBinder, просто передать данные в Stub.
И теперь представь 🌈, все Intent, ContentProvider, все сервисы которые мы получаем через getSystemService() - все это работает через Binder и AIDL, круто не правда ли?
🎉 Если ты дочитал до сюда и примерно сможешь это пересказать, то я тебя поздравляю ты охренеть какая умничка и теперь сможешь выпендится на собесе. Тема очень сложная и используется только на очень больших проектах которым мало одного процесса или в каких-нибудь SDK, как например Яндекс.Метрика.
В посте было очень много упрощений, чтобы не делать его супер большим, если тебе понравилась тема стоит посмотреть этот доклад, который расскажет все это, но подробнее.
🔥10
{1/3} Итак, Classloader. Погнали.
👉 Нужна эта штука как можно догадаться по названию для загрузки классов. Происходит это так. Есть код написанный на языках kotlin/java, затем мы отдаем эти файлы компилятору javac который уже генерирует файлы с расширением .class.
Далее эти файлы упаковываются в jar (или в apk если мы говором про android) что по сути является обычным архивом, просто с другим расширением, чтобы не путать с zip.
🤓 Суть в чем, JVM не загружает сразу все классы, она подгружает их лениво 🦥. Могут быть классы которые так и не подгрузятся JVM, несмотря на то, что они скомпилированны и лежат в jar.
За загрузку классов отвечает специальный класс (мде вот такая вот тавтология)Classloader. Это некоторый волшебный класс, один из тех классов который находится на границе между JVM и нативным кодом.
👉 Когда JVM считает, что ей нужно загрузить определенный класс, она просить Classloader это сделать. Classloader в свою очередь лезет в файловую систему, в сеть, еще куда-нибудь чтобы эти самые классы подгрузить.
Classloader это всего лишь интерфейс, а реализаций может быть много. Например, может быть Classloader который загружает классы по сети, так работали Applet.
☝️Чтобы понять идею того, как вообще работает Classloader, нужно уяснить 3 основных принципа. Представим что у нас есть реализация Classloader Parent, и есть класс наследник который расширяет функционал класса Parent – Child. Child наследует Parent все логично
👉 Нужна эта штука как можно догадаться по названию для загрузки классов. Происходит это так. Есть код написанный на языках kotlin/java, затем мы отдаем эти файлы компилятору javac который уже генерирует файлы с расширением .class.
Далее эти файлы упаковываются в jar (или в apk если мы говором про android) что по сути является обычным архивом, просто с другим расширением, чтобы не путать с zip.
🤓 Суть в чем, JVM не загружает сразу все классы, она подгружает их лениво 🦥. Могут быть классы которые так и не подгрузятся JVM, несмотря на то, что они скомпилированны и лежат в jar.
За загрузку классов отвечает специальный класс (мде вот такая вот тавтология)Classloader. Это некоторый волшебный класс, один из тех классов который находится на границе между JVM и нативным кодом.
👉 Когда JVM считает, что ей нужно загрузить определенный класс, она просить Classloader это сделать. Classloader в свою очередь лезет в файловую систему, в сеть, еще куда-нибудь чтобы эти самые классы подгрузить.
Classloader это всего лишь интерфейс, а реализаций может быть много. Например, может быть Classloader который загружает классы по сети, так работали Applet.
☝️Чтобы понять идею того, как вообще работает Classloader, нужно уяснить 3 основных принципа. Представим что у нас есть реализация Classloader Parent, и есть класс наследник который расширяет функционал класса Parent – Child. Child наследует Parent все логично
👍4
{2/3} 3 принципа: принцип делегирования, принцип видимости и принцип уникальности.
👉 Принцип делегирования
Если система попросит Child загрузить какой-то класс, то в первую очередь Child передаст этот запрос на загрузку класса к Parent. И если Parent не знает откуда взять такой класс или не может его загрузить, только тогда уже Child попытается его загрузить.
👉 Принцип видимости
Child видит все классы который загрузил Parent. Однако Parent не может видеть классы, которые загрузил Child.
👉 Принцип уникальности
Этот принцип гарантирует нам, что класс будет загружен только единожды. Другими словами, если Parent загрузил класс, то Child уже точно этот класс загружать не будет. Помимо этого Parent обязан закешировать класс и больше не пытаться его загрузить из внешнего источника.
По дефолту есть 3 основных Classloader: Bootstrap, Extension, System или Application ClassLoader.
Рассмотрим вкратце кто за что отвечает:
Bootstrap - подгружает классы, которые поставляются JRE, т.е базовые классы пакета java.lang, всякие String и т.д.
Extension - наследник класса Bootstrap;, отвечает за загрузку классов из папки jre/lib/ext или в директории которая будет прописана в системных настройках под ключом java.ext.dirs. Мы можем положить свой jar в эту папку и не прокидывать эту зависимость в наше приложение. Однако запускаться корректно оно теперь будет только на этом компе)
Application ClassLoader; - наследник класса Extension, и этот Classloader уже отвечает за загрузку class файлов из нашего jar/apk. Именно этот Classloader подгружает классы нашего приложения.
Для Application ClassLoader нужно указать с какого файла нужно начать подгружать классы нашего приложения, другими словами относительный путь до класса, в котором есть тот самый
Есть 3️⃣ варианта, команда -cp при запуске нашего jar из консоли, переменная среды CLASSPATH, или самое распространённое это сделать в jar Manifest файл. В этом Manifest файле прописать строку Class-Path. Последнее обычно делает gradle за нас.
Для Android приложения этого делать не нужно, за нас это все делает система, нам же просто нужно указать точки входа типа Activity, Service и т.д.
👉 Принцип делегирования
Если система попросит Child загрузить какой-то класс, то в первую очередь Child передаст этот запрос на загрузку класса к Parent. И если Parent не знает откуда взять такой класс или не может его загрузить, только тогда уже Child попытается его загрузить.
👉 Принцип видимости
Child видит все классы который загрузил Parent. Однако Parent не может видеть классы, которые загрузил Child.
👉 Принцип уникальности
Этот принцип гарантирует нам, что класс будет загружен только единожды. Другими словами, если Parent загрузил класс, то Child уже точно этот класс загружать не будет. Помимо этого Parent обязан закешировать класс и больше не пытаться его загрузить из внешнего источника.
По дефолту есть 3 основных Classloader: Bootstrap, Extension, System или Application ClassLoader.
Рассмотрим вкратце кто за что отвечает:
Bootstrap - подгружает классы, которые поставляются JRE, т.е базовые классы пакета java.lang, всякие String и т.д.
Extension - наследник класса Bootstrap;, отвечает за загрузку классов из папки jre/lib/ext или в директории которая будет прописана в системных настройках под ключом java.ext.dirs. Мы можем положить свой jar в эту папку и не прокидывать эту зависимость в наше приложение. Однако запускаться корректно оно теперь будет только на этом компе)
Application ClassLoader; - наследник класса Extension, и этот Classloader уже отвечает за загрузку class файлов из нашего jar/apk. Именно этот Classloader подгружает классы нашего приложения.
Для Application ClassLoader нужно указать с какого файла нужно начать подгружать классы нашего приложения, другими словами относительный путь до класса, в котором есть тот самый
public static void main
Есть 3️⃣ варианта, команда -cp при запуске нашего jar из консоли, переменная среды CLASSPATH, или самое распространённое это сделать в jar Manifest файл. В этом Manifest файле прописать строку Class-Path. Последнее обычно делает gradle за нас.
Для Android приложения этого делать не нужно, за нас это все делает система, нам же просто нужно указать точки входа типа Activity, Service и т.д.
👍3
Эскюзмуа, я немножечко опаздасьон. Давай-те предствим что я просто поставил рассылку продолжения серии постов на через 3 месяца 😄
Итак вернемся к нашей теме! Как происходит загрузка класса? JVM запускает тот самый
Application Classloader не лезет сразу же в файловую систему, чтобы подгрузить классы. Для начала он ищет этот класс в кеше, если его там нет, то просит загрузить этот класс своего предка Последний использует аналогичный метод, эдакая обратная порука, где каждый пытается спихнуть работу на старшего.
Если ни один из Parentо’в не смог загрузить класс, только тогда Application Classloader лезет в файл/сеть и куда-то еще чтобы найти этот класс. И если он его не находит то кидает тот самый ClassNotFoundException или NoClassDefFoundError. Последние две ошибки вы могли ловить если в gradle указали compileOnly вместо implement (т.е сделали так, чтобы классы были доступны только во время компиляции)
👉Для чего это нужно?
1️⃣ Ну во-первых, чтобы вы знали куда копать если вдруг встретите ClassNotFoundException или NoClassDefFoundError.
2️⃣ Во-вторых эта технология уже ближе к хардкорному rocket science, и я надеюсь, что кто меня читает не хотят всю жизнь перекладывать жесоны. Знание этой технологии позволяет делать штуки вроде JRebel, она позволяет сделать подгрузку измененых классов без перезапуска приложения, что очень кстати для тех кто разрабатывает сервера. Помимо этого, она позволяет сделать штуку вроде импакт анализа о которой мы поговорим позже)
Итак вернемся к нашей теме! Как происходит загрузка класса? JVM запускает тот самый
static void main, и начинает подгружать классы указанные в import. Сначала она идет в Application Classloader.
Application Classloader не лезет сразу же в файловую систему, чтобы подгрузить классы. Для начала он ищет этот класс в кеше, если его там нет, то просит загрузить этот класс своего предка Последний использует аналогичный метод, эдакая обратная порука, где каждый пытается спихнуть работу на старшего.
Если ни один из Parentо’в не смог загрузить класс, только тогда Application Classloader лезет в файл/сеть и куда-то еще чтобы найти этот класс. И если он его не находит то кидает тот самый ClassNotFoundException или NoClassDefFoundError. Последние две ошибки вы могли ловить если в gradle указали compileOnly вместо implement (т.е сделали так, чтобы классы были доступны только во время компиляции)
👉Для чего это нужно?
1️⃣ Ну во-первых, чтобы вы знали куда копать если вдруг встретите ClassNotFoundException или NoClassDefFoundError.
2️⃣ Во-вторых эта технология уже ближе к хардкорному rocket science, и я надеюсь, что кто меня читает не хотят всю жизнь перекладывать жесоны. Знание этой технологии позволяет делать штуки вроде JRebel, она позволяет сделать подгрузку измененых классов без перезапуска приложения, что очень кстати для тех кто разрабатывает сервера. Помимо этого, она позволяет сделать штуку вроде импакт анализа о которой мы поговорим позже)
👍3