Android handler как работает

Полный список

— разбираемся, что такое Handler и зачем он нужен

Для полного понимания урока желательно иметь представление о потоках (threads) в Java.

Так просто ведь и не объяснишь, что такое Handler. Можете попробовать почитать официальное описание, но там достаточно нетривиально и мало написано. Я попробую здесь в двух словах рассказать.

В Android к потоку (thread) может быть привязана очередь сообщений. Мы можем помещать туда сообщения, а система будет за очередью следить и отправлять сообщения на обработку. При этом мы можем указать, чтобы сообщение ушло на обработку не сразу, а спустя определенное кол-во времени.

Handler — это механизм, который позволяет работать с очередью сообщений. Он привязан к конкретному потоку (thread) и работает с его очередью. Handler умеет помещать сообщения в очередь. При этом он ставит самого себя в качестве получателя этого сообщения. И когда приходит время, система достает сообщение из очереди и отправляет его адресату (т.е. в Handler) на обработку.

Handler дает нам две интересные и полезные возможности:

1) реализовать отложенное по времени выполнение кода

2) выполнение кода не в своем потоке

Подозреваю, что стало не сильно понятнее, что такое Handler, а главное – зачем он вообще нужен 🙂 . В ближайшие несколько уроков будем с этим разбираться, и все станет понятно.

В этом уроке сделаем небольшое приложение. Оно будет эмулировать какое-либо долгое действие, например закачку файлов и в TextView выводить кол-во закачанных файлов. С помощью этого примера мы увидим, зачем может быть нужен Handler.

Project name: P0801_Handler
Build Target: Android 2.3.3
Application name: Handler
Package name: ru.startandroid.develop.p0801handler
Create Activity: MainActivity

ProgressBar у нас будет крутиться всегда. Позже станет понятно, зачем. TextView – для вывода информации о закачке файлов. Кнопка Start будет стартовать закачку. Кнопка Test будет просто выводить в лог слово test.

В обработчике кнопки Start мы организуем цикл для закачки файлов. В каждой итерации цикла выполняем метод downloadFile (который эмулирует закачку файла), обновляем TextView и пишем в лог информацию о том, что кол-во закачанных файлов изменилось. Итого у нас должны закачаться 10 файлов и после закачки каждого из них лог и экран должны показывать, сколько файлов уже закачано.

По нажатию кнопки Test – просто выводим в лог сообщение.

downloadFile – эмулирует закачку файла, это просто пауза в одну секунду.

Все сохраним и запустим приложение.

Мы видим, что ProgressBar крутится. Понажимаем на кнопку Test, в логах появляется test. Все в порядке, приложение отзывается на наши действия.

Теперь расположите AVD на экране монитора так, чтобы он не перекрывал вкладку логов в Eclipse (LogCat). Нам надо будет видеть их одновременно.

Если мы нажмем кнопку Start, то мы должны наблюдать, как обновляется TextView и пишется лог после закачки очередного файла. Но на деле будет немного не так. Наше приложение просто «зависнет» и перестанет реагировать на нажатия. Остановится ProgressBar, не будет обновляться TextView, и не будет нажиматься кнопка Test. Т.е. UI (экран) для нас станет недоступным. И только по логам будет понятно, что приложение на самом деле работает и файлы закачиваются. Нажмите Start и убедитесь.

Экран «висит», а логи идут. Как только все 10 файлов будут закачаны, приложение оживет и снова станет реагировать на ваши нажатия.

А все почему? Потому что работа экрана обеспечивается основным потоком приложения. А мы заняли весь этот основной поток под свои нужды. В нашем случае, как будто под закачку файлов. И как только мы закончили закачивать файлы – поток освободился, и экран стал снова обновляться и реагировать на нажатия.

Для тех, кто имеет опыт кодинга на Java, я ничего нового не открыл. Для остальных же, надеюсь, у меня получилось доступно объяснить. Тут надо понять одну вещь — основной поток приложения отвечает за экран. Этот поток ни в коем случае нельзя грузить чем-то тяжелым – экран просто перестает обновляться и реагировать на нажатия. Если у вас есть долгоиграющие задачи – их надо вынести в отдельный поток. Попробуем это сделать.

Т.е. мы просто помещаем весь цикл в новый поток и запускаем его. Теперь закачка файлов пойдет в этом новом потоке. А основной поток будет не занят и сможет без проблем прорисовывать экран и реагировать на нажатия. А значит, мы будем видеть изменение TextView после каждого закачанного файла и крутящийся ProgressBar. И, вообще, сможем полноценно взаимодействовать с приложением. Казалось бы, вот оно счастье 🙂

Все сохраним и запустим приложение. Жмем Start.

Приложение вылетело с ошибкой. Смотрим лог ошибок в LogCat. Там есть строки:

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Смотрим, что за код у нас в MainActivity.java в 37-й строке:

При попытке выполнить этот код (не в основном потоке) мы получили ошибку «Only the original thread that created a view hierarchy can touch its views». Если по-русски, то «Только оригинальный поток, создавший view-компоненты, может взаимодействовать с ними». Т.е. работа с view-компонентами доступна только из основного потока. А новые потоки, которые мы создаем, не имеют доступа к элементам экрана.

Т.е. с одной стороны нельзя загружать основной поток тяжелыми задачами, чтобы не «вешался» экран. С другой стороны – новые потоки, созданные для выполнения тяжелых задач, не имеют доступа к экрану, и мы не сможем из них показать пользователю, что наша тяжелая задача как-то движется.

Тут нам поможет Handler. План такой:

— мы создаем в основном потоке Handler
— в потоке закачки файлов обращаемся к Handler и с его помощью помещаем в очередь сообщение для него же самого
— система берет это сообщение, видит, что адресат – Handler, и отправляет сообщение на обработку в Handler
— Handler, получив сообщение, обновит TextView

Чем это отличается от нашей предыдущей попытки обновить TextView из другого потока? Тем, что Handler был создан в основном потоке, и обрабатывать поступающие ему сообщения он будет в основном потоке, а значит, будет иметь доступ к экранным компонентам и сможет поменять текст в TextView. Получить доступ к Handler из какого-либо другого потока мы сможем без проблем, т.к. основной поток монополизирует только доступ к UI. А элементы классов (в нашем случае это Handler в MainActivity.java) доступны в любых потоках. Таким образом Handler выступит в качестве «моста» между потоками.

Перепишем метод onCreate:

Здесь мы создаем Handler и в нем реализуем метод обработки сообщений handleMessage. Мы извлекаем из сообщения атрибут what – это кол-во закачанных файлов. Если оно равно 10, т.е. все файлы закачаны, мы активируем кнопку Start. (кол-во закачанных файлов мы сами кладем в сообщение — сейчас увидите, как)

Метод onclick перепишем так:

Мы деактивируем кнопку Start перед запуском закачки файлов. Это просто защита, чтобы нельзя было запустить несколько закачек одновременно. А в процессе закачки, после каждого закачанного файла, отправляем (sendEmptyMessage) для Handler сообщение с кол-вом уже закачанных файлов. Handler это сообщение примет, извлечет из него кол-во файлов и обновит TextView.

Все сохраняем и запускаем приложение. Жмем кнопку Start.

Кнопка Start стала неактивной, т.к. мы ее сами выключили. А TextView обновляется, ProgressBar крутится и кнопка Test нажимается. Т.е. и закачка файлов идет, и приложение продолжает работать без проблем, отображая статус закачки.

Когда все файлы закачаются, кнопка Start снова станет активной.

Подытожим все вышесказанное.

1) Сначала мы попытались грузить приложение тяжелой задачей в основном потоке. Это привело к тому, что мы потеряли экран – он перестал обновляться и отвечать на нажатия. Случилось это потому, что за экран отвечает основной поток приложения, а он был сильно загружен.

Читайте также:  Call phone android studio

2) Мы создали отдельный поток и выполнили весь тяжелый код там. И это бы сработало, но нам надо было обновлять экран в процессе работы. А из не основного потока доступа к экрану нет. Экран доступен только из основного потока.

3) Мы создали Handler в основном потоке. А из нового потока отправляли для Handler сообщения, чтобы он нам обновлял экран. В итоге Handler помог нам обновлять экран не из основного потока.

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

Eclipse может подчеркивать Handler желтым цветом и ругаться примерно такими словами: «This Handler class should be static or leaks might occur«. Тем самым он сообщает нам, что наш код немного плох и может вызвать утечку памяти. Тут он прав абсолютно, но я в своих уроках все-таки буду придерживаться этой схемы, чтобы не усложнять.

А на форуме я отдельно расписал, почему может возникнуть утечка и как это можно пофиксить. Как закончите тему Handler-ов, обязательно загляните туда и почитайте — http://forum.startandroid.ru/viewtopic.php?f=30&t=1870.

На следующем уроке:

— посылаем простейшее сообщение для Handler

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Источник

Полный список

— работаем с Handler и Runnable

Кроме обработки сообщений, мы можем попросить Handler выполнить кусок кода – Runnable. В прошлых уроках мы работали с сообщениями, которые содержали атрибуты. Мы их обрабатывали в Handler и в зависимости от значений атрибутов выполняли те или иные действия. Runnable же – это кусок кода, который мы пошлем вместо атрибутов сообщения, и он будет выполнен в потоке, с которым работает Handler. Нам уже ничего не надо обрабатывать.

Для отправки кода в работу используется метод post. Как и сообщения, Runnable может быть выполнен с задержкой (postDelayed), и может быть удален из очереди (removeCallbacks). Напишем приложение, которое продемонстрирует все эти возможности.

Project name: P0841_HandlerRunnable
Build Target: Android 2.3.3
Application name: HandlerRunnable
Package name: ru.startandroid.develop.p0841handlerrunnable
Create Activity: MainActivity

ProgressBar, отображающий текущий прогресс. CheckBox, который будет включать отображение доп.информации в TextView.

В onCreate мы прописываем обработчик для CheckBox. При включении галки отображается TextView и в работу отправляется задание showInfo. При выключении галки – задание showInfo удаляется из очереди.

Далее в новом потоке эмулируем какое-либо действие — запускаем счетчик с паузами. В каждой итерации цикла отправляем в работу задание updateProgress, которое обновляет ProgressBar.

updateProgress – код, который обновляет значение ProgressBar.

showInfo – код, который обновляет TextView и сам себя планирует на выполнение через 1000 мсек. Т.е мы включаем CheckBox, showInfo срабатывает первый раз и само себя планирует на следующий раз. Т.е. этот код лежит в очереди сообщений, обрабатывается и снова кладет себя туда. Так продолжается, пока мы явно его не удалим из очереди (removeCallbacks), выключив CheckBox.

Будем выводить что-нибудь в лог из showInfo, чтобы увидеть, когда он работает, а когда нет.

Все сохраним и запустим приложение. Побежал ProgressBar.

Появился TextView, который отображает текущее значение счетчика.

В логи при этом добавляется раз в секунду запись:

Выключим CheckBox. Текст исчез.

И логи перестали идти. Значит, задание showInfo успешно удалилось из очереди и больше не работает.

Если снова включим CheckBox – оно снова начнет срабатывать и само себя помещать в очередь с задержкой исполнения. Выключаем CheckBox – удаляем его из очереди.

На следующем уроке:

— рассмотрим еще пару способов запуска Runnbale в UI-потоке

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Источник

Main Loop (Главный цикл) в Android Часть 2. Android SDK

Основой любого приложения является его главный поток. На нем происходят все самые важные вещи: создаются другие потоки, меняется 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 каждый из потоков будет получать свой уникальный экземпляр.

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

Правда тут появляется вопрос — если Looper может быть несколько, то какой из них является главным циклом? Ведь я могу создать несколько потоков, для каждого из них создать Looper, то как потом другим программистам понять кто же из них главный и куда им слать сообщения? Создатели Android подумали так же. Поэтому в Looper есть следующий код:

Читайте также:  Manage your android devices

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

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

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

Логирование

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

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

Куда более интересное отличие, что в Looper из Android SDK у нас появляется логирование. Для него используется класс под названием Printer. По сути его единственной функцией является вывод сообщения с помощью метода println.

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

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

в первый раз когда мы принимаем сообщение. Ссылка из поля mLogging записывается в final переменную logging. Это нужно, чтобы не было ситуаций когда во время обработки сообщения мы сменили Printer в поле mLogging и логирование по одному сообщению произошло в разные места;

во второй раз когда он сообщает нам о том, что началась обработка сообщения и выводит информацию о самом сообщении;

в третий раз когда он сообщает нам о том, что обработка сообщения завершена и выводит информацию о самом сообщении;

Но логирование не является единственным способом отслеживания работы Looper. Дополнительно используется класс Trace. Он нужен для трассировки стека методов через SysTrace. С помощью SysTrace мы в Profiler из Android Studio можем просматривать этот самый стек и время исполнения каждого из методов в нем. Для этого, перед тем как начнет обрабатываться новое сообщение вызывается Trace.traceBegin и когда обработка сообщения завершится Trace.traceEnd.

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

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

Looper считает время доставки и обработки сообщений и если это время больше ожидаемого, то он сообщит нам об этом. Это может понадобиться в поисках источников фризов и лагов. Допустим у нас экран 60 Гц, значит желательно, чтобы каждое сообщение обрабатывалось не более 1000 / 60 = 16,6 мс (на самом деле нужно меньше, но не суть), иначе главный поток не успеет подготовить данные для отрисовки и у нас используется прошлый кадр. Из-за этого будет казаться будто бы изображение зависло, а значит интерфейс перестанет быть плавным.

Для этого у нас имеется два поля типа long: mSlowDeliveryThresholdMs отвечающий за время доставки сообщения и mSlowDispatchThresholdMs отвечающий за время обработки сообщения.

Выставляем mSlowDispatchThresholdMs равным 16 и Looper сам будет уведомлять нас о всех сообщениях которые обрабатывались дольше этого времени и соответственно являются причиной подвисания.

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

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

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

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

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

После того как обработка сообщения завершится вызывается статический метод showSlowLog отдельно для времени доставки и отдельно для времени обработки.

В самом методе все довольно просто — из времени окончания вычитается время начала, таким образом получается длительность обработки или доставки. Если эта длительность больше чем ожидаемая, то происходит вывод в лог информации о сообщении.

Интересный момент тут в том, что логирование происходит с помощью класса Slog, а не обычного Log. Slog это специальный класс который выводит логи от имени системы. Так что, имейте ввиду, что если установить фильтр по имени вашего процесса в logcat, то вы не увидите этих сообщений.

Наблюдатели и try/catch

И это еще не все способы наблюдения за Looper. До этого информация выводилась либо в лог, либо в SysTrace. Но что если надо следить за Looper прямо в коде? Для этого используются внутренний interface Looper — Observer.

Он содержит в себе методы наблюдения за стартом обработки сообщения, за окончанием обработки сообщения и за вероятным исключением при обработке сообщения. Последний метод может понадобиться, чтобы как-то использовать исключение которое привело к падению приложения, например отправить информацию о нем на удаленный сервер, как это делает Firebase Crashlytics.

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

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

В момент обработки сообщения внутри метода loop проверяется — есть ли сейчас наблюдатель, если наблюдатель имеется то у него вызывается метод messageDispatchStarting. Методы messageDispatched и dispatchingThrewException вызываются в соответствующих местах.

Можно заметить, что обработка сообщения обернута в try-catch-finally. Это необходимо, чтобы в случае ошибки правильно отработали методы трассировки SysTrace, а так же вызов метода dispatchingThrewException у наблюдателя. И лишь потом будет брошено исключение которое и завершит наше приложение.

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

ActivityThread

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

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

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

MessageQueue

Теперь рассмотрим какими особенностями обладает MessageQueue — класс отвечающий за работу очереди сообщений в Android SDK.

Main Thread не ждет

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

Когда мы пытаемся запросить следующее сообщение и его не оказывается, то вместо wait вызывается nativePollOnce, в который передается время на которое надо уснуть.

Когда мы пытаемся добавить новое сообщение у нас вместо метода notify вызывается метод nativeWake.

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

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

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

IdleHandler

Это особый механизм, который позволяет выполнять какие-либо действия на главном потоке когда все сообщения из очереди будут выполнены. Он хорошо подходит для действий которым неважно когда они будут выполнены — сейчас или через пол секунды. С помощью этого механизма можно избавиться от некоторых фризов, убрав какое-то тяжелое или не очень действие из основной очереди сообщений.

Читайте также:  Flud 4pda pro android

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

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

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

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

Теперь, надо как-то узнать, что основная очередь сообщений опустела и настало время выполнения IdleHandler’ов. Для этого в методе next, после того как станет понятно, что доступных для выполнения сообщений в основной очереди нет, выполнится следующий код:

По сути произойдет проверка, что в ходе выполнения метода next, IdleHandler’ы еще не запускались, а также что сообщений в очереди, которые нужно обработать прямо сейчас, уже нет. Если это так, то начнется обработка IdleHandler, иначе просто будет обработано следующее сообщение.

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

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

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

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

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

syncBarrier

syncBarrier нужен для того чтобы остановить выполнение очереди сообщений по какой-либо причине.

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

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

Работает этот механизм очень просто. Для того чтобы исполнение очереди сообщений приостановилось, в очередь сообщений добавляется особо промаркированное сообщение.

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

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

Но ведь не все задачи главного потока связаны с отрисовкой View. Зачем останавливать все сообщения? Разработчики Android SDK подумали так же. Вы можете пометить ваше сообщение как асинхронное, с помощью метода Message.setAsynchronous(true). На такие сообщения syncBarrier не распространяется и они продолжат выполняться в обычном режиме.

Message

Важное примечание. Класс Message и Handler мы будем рассматривать только в контексте главного цикла. Другие их особенности связанные с возможностью передачи сообщений между потоками и между разными узлами приложения — сейчас опустим.

Pool, obtain, recycle

У Message имеется private конструктор. Для чего это сделано? Так как, за время работы процесса в нем генерируется и пересылается огромное количество сообщений, то каждый раз создавать новый объект Message будет весьма затратно. Даже такая простая вещь как создание объекта при большом количестве вызовов может иметь значение. Поэтому используются особый pool сообщений. В него будут складываться уже ставшие ненужными объекты Message и когда нам понадобится новое сообщение мы вместо создания нового объекта просто будем переиспользовать старый ненужный объект.

Так же, как и в случае с очередью сообщений, pool представляет из себя односвязный список, ссылка на начало которого хранится в поле sPool. Отдельным полем sPoolSize хранится размер этого списка, он нам понадобится чтобы наш pool не слишком разрастался и мы могли контролировать его размер.

Так как конструктор приватный, то новое сообщение создается через метод obtain. Рассмотрим его подробнее:

Первое что нас ждёт — блок синхронизации, внутри него мы смотрим — есть ли у нас сообщения в sPool. Если есть, то забираем первое сообщение из pool и возвращаем его, при этом не забывая поменять ссылку на начало списка и уменьшить значение sPoolSize.

Если же в sPool сообщений нет, то создаем новое сообщение через приватный конструктор. Но как объекты попадают в sPool? Для этого, после того как MessageQueue выполняет действие сообщения, оно вызывает у него метод recycle.

Внутри этого метода сначала проверяется — используется ли сейчас сообщение, если да, то бросается исключение, ведь в sPool должны попадать уже ненужные сообщения. Иначе вызывается приватный метод recycleUnchecked.

Внутри recycleUnchecked во все поля сообщения выставляются значения по умолчанию, а затем если наш pool ещё не заполнен, то в него добавляется наше сообщение, при этом значение sPoolSize увеличивается.

Handler

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

Помимо Looper, Message и MessageQueue в главном цикле Android SDK присутствует ещё один класс — Handler. Для чего же он нужен? Дело в том, что что с точки зрения безопасности и стабильности кода давать программистам прямой доступ к очереди сообщений может быть опасно. Помимо того, что кто-то может напакостить поменяв очередь, так ещё и такие изменения будет очень сложно отследить. Для решения этой проблемы и нужен Handler, он является фасадом для логики работы с очередью сообщений.

Если мы захотим из кода приложения добавить новое сообщение в очередь, то мы должны делать это через Handler, напрямую это сделать никак не получится, так как большинство методов MessageQueue имеют видимость package-local, а не public.

post и postDelayed

Итак, мы захотели добавить новое сообщение в очередь. Как нам это сделать? Для добавления нового сообщения в очередь у Handler есть методы post и postDelayed. Эти методы есть не только у Handler, но и например у view: post, postDelayed, есть аналог и у Activity: runOnUiThread, но все они так или иначе в итоге сводятся к вызову Handler.

Метод post просто добавляет новое сообщение в конец очереди.

Метод postDelayed добавляет отложенное сообщение, которое выполнится через определенный промежуток времени. Для этого в поле when класса Message записывается время с момента старта JVM + время через которое надо выполнить сообщение, таким образом MessageQueue понимает когда надо выполнить сообщение.

Стоит заметить, что с postDelayed стоит быть аккуратными если вы используете их в объектах с коротким жизненным циклом. Иначе может сложится ситуация когда ваш объект уже готов быть собран сборщиком мусора, но сообщение которое он отправил ещё не успело выполнится. В случае с post беда не велика и я бы даже назвал это микроутечкой памяти, но в случае с postDelayed это уже может быть скорее миниутечка, ведь объект утечет на тот период времени, что вы указали.

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

Источник

Оцените статью