
Основой любого приложения является его главный поток. На нем происходят все самые важные вещи: создаются другие потоки, меняется UI. Важнейшей его частью является цикл. Так как поток главный, то и его цикл тоже главный - в простонародье Main Loop.
Тонкости работы главного цикла уже описаны в Android SDK, а разработчики лишь взаимодействуют с ним. Поэтому, хотелось бы разобраться подробней, как работает главный цикл, для чего нужен, какие проблемы решает и какие у него есть особенности.
Это первая часть цикла разбора главного цикла в Android. Вообще, лучший способ понять, как что-то работает - сделать это самому. Поэтому, прежде чем лезть в дебри Android SDK давайте попробуем написать свой цикл, правда без блэкджека и прочего. Наоборот, это будет минимально работоспособный цикл, но зато хорошо демонстрирующий основную логику, без лишней мишуры.
По всем правилам приличия представлюсь — меня зовут Перевалов Данил, а теперь давайте перейдём к теме.
Как вообще работают программы
Но для начала, давайте разберемся как вообще работают простые программы в Java.
С точки зрения системы - все что есть у программы это просто метод main который она вызовет при старте и завершит процесс после его выполнения.

В коде это выглядит примерно так - у нас есть класс и внутри него метод main, который и вызовется системой. В данном случае мы просто выведем Hello World в консоль.
package myjavacode;
public class MyClass {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}Вместо вывода в лог мы можем открыть экран, выполнить сложную операцию или отправить запрос в сеть. Суть не изменится: после выполнения метода main - программа закроется. Программа или, если говорить в контексте Android - приложение, живет пока что-то делает, затем просто завершается.
Почему так сделано? Изначально программы делались для командной строки, где основным методом взаимодействия является либо передача входных данных в виде аргументов, либо ввод данных пользователем в командную строку в процессе исполнения программы. После того как программа выполняла свою основную задачу ей просто не было смысла работать дальше, и такая программа завершала работу.
В программах использующих UI и в частности в Android приложениях - все не так. Приложение не закрывается как только сделает все что ему было предписано на старте. Оно терпеливо ждет действий пользователя, кликов и прочего, и затем реагирует на них. Поэтому, приложения с UI должны жить и работать пока пользователь сам его не закроет (ну или пока приложение не упадет, или система сама его не закроет по причине нехватки памяти). Но вот проблема: как только последняя строчка метода main выполнится - приложение закроется само, так как посчитает, что оно сделало все что нужно.
Как не дать приложению закрыться?
Для начала давайте разберемся с тем, как же нам не дать приложению завершаться самостоятельно. Самый простой и самый действенный метод - (почти) бесконечный цикл. Проще всего его создать через обычный while.
public class MyClass {
private static boolean isAlive = true;
public static void main(String[] args) {
while(isAlive) {
doAction();
}
}
private static void doAction() {
}
}По сути, мы просто добавили (почти) бесконечный цикл в котором вызывается метод doAction и теперь наше приложение уже не будет закрываться само, ведь цикл то бесконечный, а значит и приложению всегда есть что делать. Оно будет бесконечно выполнять метод doAction пока мы не попросим его наконец остановится переключив переменную isAlive в состояние false. Проблема только в том, что наше приложение пока ничего не делает. Метод doAction то пустой.
Заставляем приложение что-то делать
Теперь добавим возможность приложению выполнять какие-либо действия. Просто написать код всей программы в методе doAction не очень хороший вариант. В приложении могут быть сотни кнопок и текстовых полей на каждое из которых нужно написать свое действие. Если писать все в одном методе то он довольно быстро превратится в нечитаемое чудовище размером в несколько десятков тысяч строк.
Поэтому воспользуемся функцией обратного вызова - в простонародье callback. Благо в Java уже есть интерфейс Runnable который хорошо для этого подходит. У него есть всего один метод run, который можно переопределить и написать туда свое действие.
Для того, чтобы понимать какое действие надо выполнить следующим поместим их в очередь. Пока для нее сгодится обычный ArrayList.
public class MyClass {
private static List<Runnable> availableActions = new ArrayList<>();
private static boolean isAlive = true;
public static void main(String[] args) {
while(isAlive) {
doAction();
}
}
private static void doAction() {
if (availableActions.size > 0) {
Runnable currentAction = availableActions.get(0);
currentAction.run();
availableActions.remove(currentAction);
}
}
}Теперь в нашем цикле мы проверяем есть ли доступные действия. Если есть, то выполняем действие и удаляем его из списка доступных действий. Осталось только добавить какое-то действие в нашу очередь.
Для этого давайте представим, что у класса System есть возможность добавить слушатель на нажатия экрана вызвав метод - registerClickListener. В обратном вызове слушателя добавим какое-нибудь действие в очередь. Например, выведем в лог Click on screen.
public class MyClass {
private static List<Runnable> availableActions = new ArrayList<>();
private static boolean isAlive = true;
public static void main(String[] args) {
System.registerClickListener((clickEvent) -> {
availableActions.add(() -> Log.d("Click on screen"));
});
while(isAlive) {
doAction();
}
}
private static void doAction() {
if (availableActions.size > 0) {
Runnable currentAction = availableActions.get(0);
currentAction.run();
availableActions.remove(currentAction);
}
}
}Отлично! Теперь у нас есть приложение которое способно выводить сообщение в лог при нажатии на экран. Без цикла у нас бы ничего не вышло, ведь сразу после регистрации слушателя нажатий на экран, приложение закрылось бы и соответственно ни один клик не был бы обработан.
Новые действия могут добавляться в нашу очередь как от вызовов системы, так и внутри самих действий. То есть какое-то действие добавляет новое действие, а то добавляет ещё 5 новых и так до бесконеч��ости.
Вроде все хорошо, но есть один нюанс… Наше приложение теперь делает «что-то» постоянно. Даже когда у него нет доступных действий оно бесконечно проверяет не появились ли они. Тем самым оно постоянно загружает ядро процессора по максимуму, что явно не лучшим образом скажется на фоновых процессах, энергопотреблении и температуре самого процессора.
Заставляем приложение ничего не делать
Для того, чтобы дальнейшие действия выглядели менее абсурдными для тех кто разбирается - представим, что обратный вызов на нажатия на экран вызывается с какого-то отдельного потока системы.
Нам нужно указать, что если у нас пока нет доступных действий, то пора ничего не делать. Для этого воспользуемся стандартным методом wait() который заставляет текущий поток ждать и соответственно - бездействовать.
Так же, когда случится обратный вызов от нажатия на экран нам надо сказать нашему потоку, что пора поработать и проверить наличие доступных для выполнения действий. Для этого воспользуемся стандартным методом notify() который «разбудит наш поток».
public class MyClass {
private static List<Runnable> availableActions = new ArrayList<>();
private static boolean isAlive = true;
public static void main(String[] args) {
System.registerClickLister((clickEvent) -> {
availableActions.add(() -> Log.d("Click on screen"));
availableActions.notify();
});
while(isAlive) {
doAction();
}
}
private static void doAction() {
if (availableActions.size > 0) {
Runnable currentAction = availableActions.get(0);
currentAction.run();
availableActions.remove(currentAction);
} else {
availableActions.wait();
}
}
}Теперь наше приложение не отжирает все доступные ресурсы, а спокойно ждет, пока придет следующее действие, чтобы его обработать. Как только все доступные для выполнения действия заканчиваются оно засыпает, а когда происходит клик и появляется новое действие - оно просыпается.
Самый главный цикл в жизни программы
Сам по себе подход с использованием цикла называется Event Loop (если вам больше нравится на русском - Цикл событий). Если же Event Loop обеспечивает работу главного потока - то он уже «поднялся», он не какой-то простой Event Loop, он - Main Loop (Главный цикл). По сути он является ядром всего приложения, обеспечивая его работу. Весь код выполняемый на главном потоке (Main Thread) проходит через него. Практически все приложения в которых есть UI (и не только они) ис��ользуют его.

Вариантов реализации Main Loop множество, но нас сейчас интересует конкретно то - как это реализовано в Android. В целом у нас получился неплохой колхозный вариант Main Loop, но он не дает понимания всех нюансов работы главных циклов. Поэтому давайте вернемся к коду на котором мы остановились и добавим немного комфортной городской среды в наш колхоз.
Распределяем ответственности по классам
В целом у нас написано что-то похожее на рабочий код. Приложение само не закрывается, ждет команд от пользователя и умеет их выполнять. Но как-то все не по принципам SOLID, особенно с буквой S (Single-Responsibility Principle) большие проблемы. Вся логика в одном классе и ни о каком разделении ответственности речи идти не может. Давайте попытаемся это исправить, да и в целом хочется накинуть новых возможностей.
Для начала давайте сделаем обертку над Runnable и назовем ее… к примеру Message.
Message
Для этого просто создадим новый класс Message. Одним из полей которого как раз и будет наш Runnable. Назовем его callback.
class Message {
Runnable callback;
public long when;
}Дополнительно добавим еще одно поле when. Оно будет хранить значение времени в которое нужно выполнить действие. Ведь не всегда нужно выполнять что-то здесь и сейчас. Иногда, чтобы все было хорошо, нужно подождать 500 миллисекунд. Для реализации такого механизма в поле when будет записываться время с момента старта приложения плюс время через которое должно произойти действие записанное в callback. Тоесть when = время с момента старта приложения + задержка для сообщения. Допустим я добавляю действие и хочу, чтобы оно выполнилось через 500 миллисекунд, а с момента старта приложения прошло 2000 миллисекунд, тогда в when у нас будет 2000 + 500 = 2500. Если же мне важно выполнить действие как можно скорее, то тогда в поле when надо записать 0.
Теперь давайте разберемся с нашим ArrayList который содержит действия.
ArrayList
Тут сразу стоп!!! Мы ведь добавили поле when, тем самым позволяя создавать отложенные сообщения, а следовательно у нас появятся сообщения которые могут находится в очереди очень долго ожидая своего часа.
Может сложиться следующая ситуация: у меня есть список из трёх сообщений, первые два должны выполняться как можно скорее, а третье… допустим через час. При этом первое сообщение добавляет ещё 10 сообщений в нашу очередь, каждое из которых должно выполняться как можно скорее. Это значит, что их надо добавить в очередь сразу после второго сообщения. Получается вставка в середину списка, а как все знают у ArrayList с операцией вставки есть проблемы.

Вставка в ArrayList имеет сложность O(n), а значит, чем больше у нас будет сообщений в очереди, тем больше времени она будет занимать.
Поэтому хорошим решением будет заменить ArrayList на связный список у которого сложность вставки O(1), а значит количество элементов не будет влиять на время операции. Правда у связного списка есть проблемы с временем доступа к произвольному элементу, но это не касается первого элемента. А нам как раз нужен доступ только к первому элементу.
После того как сообщение выполнит свою работу оно станет ненужным. Поэтому двусвязный список тут не очень подходит, ведь придется каждый раз при удалении первого сообщения обращаться и ко второму, чтобы удалить ссылку на первое. А вот односвязный список - вполне подходит. Проблема только в том, что в Java нет стандартной реализации односвязного списка. Не беда! Сделаем сами. Для этого просто добавим поле next типа Message в сам Message.
class Message {
Runnable callback;
public long when;
Message next;
}Теперь у нас каждое сообщение содержит ссылку на следующее, таким образом формируя список. Если в поле next записан null, то это значит текущее сообщение является последним в списке.
Наш односвязный список готов и можно двигаться дальше.
Очередь сообщений
Теперь бы надо где-то прописать логику работы с сообщениями. Для этого создадим новый класс. Пусть будет MessageQueue. Это конечно не Queue в прямом понимании этого типа, так как у нас есть вставка в середину. С другой стороны - мы всегда берем для работы первое сообщение, так что называть класс MessageList еще более странная затея.
class MessageQueue {
Message messages;
}Пока в классе у нас будет единственное поле messages со ссылкой на начало списка сообщений, то есть ближайшее сообщение которое мы планируем обработать. Соответственно если поле messages null, то список пустой, а значит новых сообщений нет.
Возвращаем текущее сообщение
Теперь надо добавить метод который будет возвращать текущее сообщение. Для того, кто будет вызывать этот метод оно, по сути, будет следующим. Потому и назовем метод соответствующе - next.
class MessageQueue {
Message messages;
Message next() {
Message current = messages;
if (current == null) {
return null;
} else {
messages = current.next;
}
return current;
}
}В нем все просто:
берем текущее сообщение
у него забираем ссылку на следующее за ним сообщение
делаем его текущим.
Таким образом мы сделали текущим то сообщение, что было следующим, тем самым продвинув очередь. А то чтобы было текущим возвращаем тому, кто вызвал этот метод.
Теперь нужно учесть, что сообщение имеет поле when, которое позволяет выполнить сообщение в указанное время, а значит сообщение не всегда нужно отдавать, оно еще может быть не готово выполнится. Для этого добавим проверку сообщения по времени.
Message next() {
Message current = messages;
final long now = SystemClock.uptimeMillis();
if (current == null || now < current.when) {
return null;
} else {
messages = current.next;
return current;
}
}Для этого берем время которое прошло с момента старта приложения и записываем в переменную now. Далее просто сравниваем now и время когда сообщение в переменной current должно выполниться. Если now меньше, то сообщаем тем кто вызвал метод, что следующего сообщения как будто бы и нет.
Новое сообщение
Получать следующее сообщение мы научились, теперь надо научится добавлять новое. Для это создадим метод enqueueMessage который добавляет новое сообщение.
boolean enqueueMessage(Message newMessage) {
if (newMessage == null) {
return false;
}
Message current = messages;
if (current == null) {
messages = newMessage;
} else {
Message previous;
while(true) {
previous = current;
current = current.next;
if (current == null) {
break;
}
}
previous.next = newMessage;
}
return true;
}Внутри него сначала проверим есть ли у нас хоть одно запланированное действие. Если нет, то делам новое сообщение первым в очереди. Если же есть, то пробежимся в цикле по нашей очереди, найдем последнее, то есть то, что имеет в поле next - null и добавим новое сообщение в самый конец, записав его в этот самый next.
Теперь нужно учесть, что enqueueMessage может вызываться с разных потоков, а это значит, что нам нужно синхронизировать добавление сообщений. Иначе может случится плохая ситуация, когда два потока одновременно попробуют добавить сообщение и в лучшем случае мы получим потерю одного из сообщений, в худшем аварийное завершение программы.
Просто засунем код нашего метода в блок synchronized.
boolean enqueueMessage(Message newMessage) {
if (newMessage == null) {
return false;
}
synchronized (this) {
Message current = messages;
if (current == null) {
messages = newMessage;
} else {
Message previous;
while(true) {
previous = current;
current = current.next;
if (current == null) {
break;
}
}
previous.next = newMessage;
}
}
return true;
}А так же добавим synchronized в метод next, так как он тоже обращается к messages и опять же может случится нечто нехорошее.
Message next() {
synchronized (this) {
Message current = messages;
final long now = SystemClock.uptimeMillis();
if (current == null || now < current.when) {
return null;
} else {
messages = current.next;
return current;
}
}
}Теперь нужно учесть, что мы можем добавить новое сообщение в середину очереди. Для этого добавим сравнение сообщений по полю when когда ищем последнее сообщение. То есть теперь мы ищем не просто сообщение к которого next равен null, но так же и смотрим, чтобы у следующего сообщения значение when было меньше чем у нового. Ну и соответственно из-за вставки в середину нам нужно заполнить поле next у нового сообщения

С точки зрения кода это будет выглядеть следующим образом:
boolean enqueueMessage(Message newMessage) {
if (newMessage == null) {
return false;
}
synchronized (this) {
Message current = messages;
if (current == null) {
messages = newMessage;
} else {
Message previous;
while(true) {
previous = current;
current = current.next;
if (current == null || newMessage.when < current.when) {
break;
}
}
newMessage.next = previous.next;
previous.next = newMessage;
}
}
return true;
}
С очередью сообщений пока все. Теперь пора переработать сам цикл.
Запускаем цикл
Заводим новый класс и называем его Looper. В нем у нас содержится очередь сообщений (MessageQueue), переменная isAlive которая отвечает за то, продолжать ли приложению работать, а также два метода:
loop - в котором запускается и крутится наш цикл
shutdown - который переключает isAlive в false тем самым останавливая обработку сообщений и завершая приложение
Давайте присмотримся к нашему основному методу loop поближе.
class Looper {
private static Looper instance = new Looper();
final MessageQueue messageQueue;
private static boolean isAlive = true;
public Looper() {
messageQueue = new MessageQueue();
}
public static void loop() {
Looper currentInstance = instance;
while (isAlive) {
Message nextMessage = currentInstance.messageQueue.next();
if (nextMessage != null ) {
nextMessage.callback.run();
} else {
try {
instance.messageQueue.wait();
} catch (InterruptedException e) {
}
}
}
}
public static void shutdown() {
isAlive = false;
}
}В нем мы получаем объект Looper и запускаем уже привычный нам бесконечный цикл, в котором запрашиваем новое сообщение у MessageQueue. Если сообщение есть, то выполняем действие, если же нет, то засыпаем.
Но уснуть то мы уснули, а когда же просыпаться? Нужно теперь куда-то добавить метод notify. По хорошему это надо делать там, где у нас добавляется новое сообщение, а происходит это внутри метода MessageQueue.enqueueMessage. Но вот засыпать в одном классе, а просыпаться в другом - идея так себе, ведь это будет сложно контролировать.
Также можно заметить, что мы не очень хорошо работаем с сообщениями которые собирались выполнить в определенное время и заполнили им поле when. Да, мы не выполним действие сообщения раньше времени за счет проверки внутри MessageQueue, но мы можем проспать его выполнение, если у нас не будут поступать новые сообщения. Ведь спим мы пока не придет новое сообщение и на самом деле маловероятно, что придет оно именно в тот момент когда надо будет выполнить сообщение по времени. Обе эти проблемы можно решить переносом ожидания нового сообщения внутрь MessageQueue.
Переносим ожидание в MessageQueue
Давайте сделаем так, чтобы мы гарантированно отдавали сообщение в методе next, а если сообщения нет, то дожидаемся его.
Message next() {
int nextWaitTime = -1;
while (true) {
try {
if (nextWaitTime >= 0) {
wait(nextWaitTime);
}
} catch (InterruptedException e) {
}
synchronized (this) {
Message current = messages;
if (current != null) {
final long now = SystemClock.uptimeMillis();
if (now < current.when) {
nextWaitTime = (int) (current.when - now);
} else {
messages = current.next;
return current;
}
} else {
nextWaitTime = 0;
}
}
}
}Также внутри метода next появился цикл который отвечает за ожидание сообщения. В нем мы проверяем есть сообщения, если нет - то спим пока не вызовут notify. Ес��и есть, то смотрим нужно ли сейчас выполнять действие текущего сообщения. Если да, то возвращаем сообщение, если же нет, то засыпаем пока не придет время выполнить действие текущего сообщения.
В метод enqueueMessage же добавим вызов notify, чтобы пробудить наш цикл в методе next.
boolean enqueueMessage(Message newMessage) {
if (newMessage == null) {
return false;
}
synchronized (this) {
Message current = messages;
if (current == null) {
messages = newMessage;
} else {
Message previous;
while (true) {
previous = current;
current = current.next;
if (current == null || newMessage.when < current.when) {
break;
}
}
newMessage.next = previous.next;
previous.next = newMessage;
}
notify();
}
return true;
}Ну и напоследок в самом Looper уберем ожидание. Теперь он просто выполняет действия сообщений.
public static void loop() {
Looper currentInstance = instance;
while (isAlive) {
Message nextMessage = currentInstance.messageQueue.next();
nextMessage.callback.run();
}
}Общая схема
Общая логика работы получилась такой:
система запускает наш процесс, что в итоге ведет к вызову метода loop у Looper.
Looper внутри себя обращается к методу next у MessageQueue за новым сообщением
MessageQueue видя, что сообщений пока нет - останавливает текущий поток с помощью метода wait
система кидает нам какое-нибудь событие, например клик на экран, что в итоге добавляет новое сообщение нашу в очередь сообщений через метод enqueueMessage у MessageQueue и будит текущий поток
метод next у MessageQueue просыпается и видит что у него появилось новое сообщение
это новое сообщение MessageQueue возвращается в Looper
Looper просто выполняет callback из сообщения
обратно к пункту 2

Отлично! В итоге у нас вполне рабочий цикл событий. Даже что-то близкое к тому, как все устроено в Android, но в Android классах кода куда больше. Например, в нашем Looper 25 строк, а в Android 493, правда это с учетом JavaDoc. Все потому, что Looper, Message, MessageQueue обладают в Android SDK дополнительными возможностями.
В следующей статье мы поближе познакомимся с реализацией главного цикла в Android SDK. Будет интересно (ну или нет).
Main Loop (Главный цикл) в Android Часть 2. Android SDK
Main Loop (Главный цикл) в Android Часть 3. Другие главные циклы
