Программа лупер для андроид

Обновлено: 06.07.2024


Основой любого приложения является его главный поток. На нем происходят все самые важные вещи: создаются другие потоки, меняется UI. Важнейшей его частью является цикл. Так как поток главный, то и его цикл тоже главный - в простонародье Main Loop.

Тонкости работы главного цикла уже описаны в Android SDK, а разработчики лишь взаимодействуют с ним. Поэтому, хотелось бы разобраться подробней, как работает главный цикл, для чего нужен, какие проблемы решает и какие у него есть особенности.

Это вторая часть цикла статей по разбору главного цикла в Android. В первой части мы разобрались с тем, что такое главный цикл и как он работает. В этой же части давайте разберемся как Main Loop устроен в Android SDK. Разбираться будем в контексте Android SDK версии 30.

Looper

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

Может быть создан для любого из потоков и только один

Первое, что бросается в глаза - приватный конструктор.

Создать Looper можно только используя метод prepare.

При вызове публичного метода prepare вызывается его приватная реализация. Она принимает в себя параметр quitAllowed. Он будет true, если для данного Looper есть возможность завершится во время работы приложения. Для главного потока этот параметр всегда будет false, так как если завершится главный поток, то завершится и приложение. Для побочных же потоков этот параметр всегда равен true.

Также в методе prepare можно заметить обращение к полю sThreadLocal типа ThreadLocal. Что же это такое?

ThreadLocal это такое хранилище в котором для каждого из потоков будет хранится свое значение. Допустим я из потока 1 кладу в это хранилище true, затем если я обращусь из этого же потока к хранилищу - я получу true. Но если я обращусь к этому хранилищу из другого потока, то мне вернется null, так как для этого потока значение еще не было записано.


Looper использует этот механизм вкупе с приватным конструктором для того, чтобы обеспечить уникальность Looper для каждого из потоков. Внутри метода prepare с помощью ThreadLocal он сначала проверяет был ли уже создан Looper для текущего потока, если это так, то бросает исключение которое скажет о том, что негоже создавать несколько Looper для одного потока. Если же Looper для текущего потока еще не был создан, то он создает новый Looper и сразу же записывает его в ThreadLocal.

Для получения экземпляра Looper, созданного в методе prepare, есть метод myLooper. Он просто каждый раз обращается к sThreadLocal для получения значения для текущего потока.

С такой логикой Looper можно создать для любого из потоков, пользоваться и при этом точно знать, что для данного потока Looper только один. Допустим у нас есть 5 потоков и каждый из них создает и обращается к Looper. В итоге у нас будет создано 5 экземпляров Looper, но при обращении к Looper.myLooper каждый из потоков будет получать свой уникальный экземпляр.

Главный среди равных

Отдельный метод prepareMainLooper как раз занимается тем, что создает Looper для текущего потока и записывает его в отдельное статическое поле sMainLooper, тем самым как-бы объявляя его главным. Теперь если кто-то попробует вызвать prepareMainLooper с другого потока, то будет брошено исключение которое скажет нам, что главный вообще-то может быть только один.

Еще у главного потока есть свой отдельный getter - getMainLooper, ведь обращение к главному циклу может понадобиться где угодно. Таким образом, разработчики всегда будут знать кто тут главный Looper.

Теперь давайте ближе взглянем на особенности самого цикла, а значит на метод loop.

Логирование

Первое что бросается в глаза в методе loop, это то, что у нас вместо цикла while используется for с двумя точками с запятой. Такой подход вроде как производительнее.

Также можно заметить что остановка бесконечного цикла делается не с помощью переключения отдельной переменной isAlive, а помощью получение null от MessageQueue.next.

Инициализированный объект Printer хранится в поле mLogging, то есть у каждого из Looper может быть свой личный Printer. Выставляется Printer через отдельный сеттер. Если же Printer не задать, то и логирования не будет.

Внутри самого метода loop Printer используется трижды:

Но это еще не все методы слежки.

Подсчет времени

Для выставления значений этих полей создан отдельный метод setSlowLogThresholdMs. Эти поля всегда выставляются парой.

Также есть возможность задать это время с помощью системной переменной. Имя которой формируется по следующему принципу: log.looper.<”идентификатор процесса”>.<”имя потока, в нашем случае это будет main”>.slow.

Теперь посмотрим как это всё работает внутри метода loop.

Выглядит как-то путано, не правда ли? Сначала значение полей записываются в локальные переменные. Затем проверяется, не было ли задано ограничение с помощью системной переменной, если это так, то берется именно оно. Если оба значение для время доставки и обработки больше нуля, то метод loop понимает, что время начать считать.

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

Сам Observer хранится статической переменной sObserver, то есть наблюдатель выставляется сразу для всех экземпляров Looper. Выставляется он через отдельный сеттер.

Сама логика вызова методов Observer довольно простая.

Это пожалуй все интересные особенности класса Looper в Android SDK.

ActivityThread

Теперь давайте рассмотрим где же всё-таки у нас идет работа с самим Looper. А происходит это всё также в методе main и находится он в классе ActivityThread.

В нем сначала вызывается метод prepareMainLooper. Далее выставляется реализация Printer. И под самый конец метода вызывается метод loop запускающий главный цикл. Последней строкой этого метода бросается исключение. Таким образом, как только цикл завершится, то и завершится весь процесс.

Если хотите поподробнее узнать о том как запускается процесс в андроид то рекомендую посмотреть эту статью.

MessageQueue

Main Thread не ждет

Первая особенность MessageQueue заключается в том, что вместо стандартных методов из Java wait и notify используются нативные методы nativePollOnce и nativeWake.

Почему же нельзя воспользоваться обычными wait и notify? Дело в том, что у Android приложений помимо Java слоя есть еще и прослойка C++ в которой на главном потоке тоже могут происходит различные операции которые стоит выполнить. Следовательно воспользоваться wait у нас не получится, так как это усыпит главный поток без передачи управления прослойке C++.

В прослойке C++ так же есть свой Looper, но подробнее мы разберем его в следующей статье.

Вызов C++ конечно интересен сам по себе, но есть в MessageQueue что-то, что может пригодится обычному разработчику? Конечно есть.

IdleHandler

Давайте посмотрим на реализацию этого механизма. По своей сути IdleHandler это обычный интерфейс с одним единственным методом - queueIdle. В нем и будет содержатся действие которое мы планируем выполнить.

Как можно заметить, этот метод возвращает boolean. Если вернуть false, то наше действие больше не повторится, если же вернуть true - то наше действие выполнится еще раз. Поэтому лучше лишний раз не ставить true, дабы избежать ситуаций когда у нас появляется бесконечно повторяющееся действие на главном потоке.

В классе MessageQueue в поле mIdleHandlers находится список еще не выполненных IdleHandler, а также есть метод для добавления нового IdleHandler - addIdleHandler.

Единственной особенностью addIdleHandler является синхронизация.

Настало время выполнить IdleHandler.

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

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

При этом выполнение обернуто в try-catch. После выполнения в зависимости от результата метода queueIdle IdleHandler удалиться из общего списка на выполнение. Если во время выполнения IdleHandler он бросит исключение, то он так же удалиться из списка на выполнение.

От чего-то полезного перейдем к тому, чем вы по идее никогда не должны пользоваться, ну разве что очень редко.

syncBarrier

К сожалению (или к счастью) методы работы с syncBarrier помечены аннотацией Hide, а значит мы не сможем вызвать их из своего кода честными методами.

Основной способ использования этого механизма появился в Android 5. В нем появился выделенный поток для рендеринга (до этого рендеринг происходил на главном потоке). Из-за этого пришлось придумывать как останавливать обработку главного потока, а конкретно его задач связанных с интерфейсом, пока поток рендеринга считывал дерево View.


Message

Pool, obtain, recycle

Handler

Зачем он нужен


post и postDelayed



На мой взгляд, это пожалуй все самое интересное из Android SDK связанное с Looper, MessageQueue и Message. Поэтому можно сказать, что как главный цикл работает в Android SDK и какие особенности имеет мы разобрались. По крайней мере на слое Java, но есть же еще и упомянутый C++ слой. Да и не секрет, что Android приложения пишутся не только с помощью Java Android SDK, есть Flutter, React Native, Chrome и игры. Какие особенности есть у них мы кратко разберем в следующей и финальной части этого цикла статей.

Читайте также: