2. Looper-потоки. Работа с очередью сообщений с помощью Handler
Как мы знаем, процесс состоит из одного или нескольких потоков. Также мы знаем, что как только поток выполняет все входящие в него инструкции, он закрывается.
Но тогда каким образом главный поток (он же MainThread или UIThread) не закрывается после выполнения всех инструкций, а ждет действий пользователя, чтобы на них определенным образом отреагировать?
Для этого основной поток приложения модифицируется с помощью специального класса Looper.
Looper – который ещё иногда ещё называют «цикл обработки событий» используется для реализации бесконечного цикла который может получать задания.
Класс Looper позволяет подготовить Thread для обработки повторяющихся действий. Такой Thread, как показано на рисунке ниже, часто называют LooperThread. Главный поток в приложении является этим самым Looper Thread. Объект Looper уникален для каждого потока.
Message – сообщение, которое представляет собой контейнер для набора инструкций которые будут выполнены в другом потоке.
Handler – данный класс обеспечивает взаимодействие с LooperThread. С помощью Handler можно отправлять Message в Looper.
Поток может иметь только один Looper и, следовательно, один поток сообщений.
С помощью этой информации мы можем реализовать следующий функционал:
Передавать сообщения из потока в поток, запускать код в другом потоке;
Реализовать отложенное по времени выполнение кода.
Начнем с базовых вещей. Можно ли создать рабочий поток с использованием Looper`а и очереди сообщений? Да, это сделать очень просто, рассмотрим следующий код
Отлично, мы запустили поток, но каким образом нам добавлять сообщения в очередь и как нам эти сообщения обрабатывать?
Для этого существует специальный класс, который называется Handler. При создании объекта класса Handler, он привязывается («биндится», bind) к конкретному объекту Looper`у. Модифицируем класс LooperThread
Теперь у нас есть созданный объект Handler и мы можем с помощью метода getHandler() получить ссылку на объект Handler, который привязан к объекту Looper`у созданного потока.
Таким образом, у нас есть основной поток и рабочий Looper-поток, причем оба потока имеют ссылки на объект Handler другого потока. Теперь осталось изучить, как передавать сообщения и выполнять код в другой поток с помощью объектов Handler.
Рассмотрим небольшой пример, который позволит нам рассмотреть использование объектов класса Handler. Программа будет работать следующим образом – пользователь вводит некоторое целое число, после чего программа в отдельном потоке вычисляет факториал числа и показывает результат в текстовом поле.
Создадим новый класс потока. В классе предусмотрим два поля для ссылок на Handler текущего потока и на Handler основного потока, с которым будет происходить взаимодействие.
Теперь нам необходимо закодировать следующие действия:
необходимо передать в объект Handler целое число;
внутри объекта Handler необходимо выполнить код для подсчета факториала;
в Handler основного потока необходимо передать изначальное число и результат вычисления факториала этого числа.
Работать с Handler можно двумя способами:
в Handler можно передать объект Runnable с необходимым кодом для исполнения в looper-потоке;
можно создать класс-наследник класса Handler и переопределить метод handleMessage(), в котором описать необходимый для исполнения код.
В рамках нашего примера мы воспользуемся вторым способом. Давайте сразу посмотрим на код, а ниже его прокомментируем.
LooperThread.java
publicclassLooperThreadextendsThread {privatestaticHandler mHandler =newMyHandler();privatestaticHandler mainHandler;publicvoidsetMainThreadHandler(Handler mainThreadHandler) {this.mainHandler= mainThreadHandler; }privatestaticclassMyHandlerextendsHandler {privateBigIntegerfactorial(BigInteger n) {BigInteger result =BigInteger.ONE;while (!n.equals(BigInteger.ZERO)) { result =result.multiply(n); n =n.subtract(BigInteger.ONE); }return result; } @OverridepublicvoidhandleMessage(Message msg) {// Извлекаем данные из сообщенияBundle b =msg.getData();int number =b.getInt("number");// Подсчет результатаBigInteger result =factorial(BigInteger.valueOf(number));// Формируем bundleBundle bundle =newBundle();bundle.putInt("initial_number", number);bundle.putString("result",result.toString());// Формируем сообщениеMessage ret_message =Message.obtain();ret_message.setData(bundle);// Отсылаем сообщениеmainHandler.sendMessage(ret_message); } }publicHandlergetHandler() {return mHandler; } @Overridepublicvoidrun() {// Подготовка объекта LooperLooper.prepare();// Запуск бесконечного циклаLooper.loop(); }}
Как мы видим, в обработчике нажатия на кнопку мы выполняем следующие действия:
получаем введенное целое число из поля ввода;
формируем Bundle, куда упаковываем целое число;
создаем объект некоторого класса Message с помощью статического метода obtain(), куда добавляем созданный Bundle;
получаем объект Handler и вызываем метод sendMessage() и передаем этому методу созданный нами объект типа Message.
Класс Message служит для обмена сообщениями между различными объектами Handler. Он предоставляет много различных возможностей для передачи данных и взаимодействия между объектами Handler (подробнее читайте здесь - https://developer.android.com/reference/android/os/Message).
На данном этапе мы видим, что в Message можно добавить Bundle с нужным набором различных данных. Метод obtain() позволяет нам не создавать объект Message, а повторно использовать уже имеющиеся объекты Message, которые больше не нужны.
Теперь давайте рассмотрим, как нам обработать это сообщение в объекте Handler.
Обратите внимание – мы переопределяем метод handleMessage(), в котором совершаем следующие действия:
извлекаем из пришедшего объекта Message нужные данные;
вычисляем факториал с помощью метода factorial(), код которого будет приведен ниже;
в демонстрационных целях устанавливаем задержку в 3 секунды;
готовим ответное сообщение с исходным значением и результатом;
имея ссылку на Handler основного потока, высылаем ему сообщение.
Теперь нам осталось прописать код для Handler основного потока, чтобы обработать сообщение с результатами вычислений
MainActivity.java
publicclassMainActivityextendsAppCompatActivity { @SuppressLint("HandlerLeak") @OverrideprotectedvoidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);LooperThread thread =newLooperThread();thread.start();// Передаем Handler основного потока в LooperThreadthread.setMainThreadHandler(newHandler() { @OverridepublicvoidhandleMessage(Message msg) {Bundle bundle =msg.getData();String txt ="Ответ: "+bundle.getInt("initial_number")+"! = "+bundle.getString("result");TextView tv =findViewById(R.id.result);tv.append(String.valueOf(txt +"\n")); } });Button button =findViewById(R.id.button);button.setOnClickListener(v -> {EditText et =findViewById(R.id.et_fact);int number =Integer.parseInt(et.getText().toString());Bundle b =newBundle();b.putInt("number", number);Message message =Message.obtain();message.setData(b);thread.getHandler().sendMessage(message);et.getText().clear(); }); }}
Здесь мы опять извлекаем данные из Message, формируем строку с сообщением и добавляем ее в текстовое поле. Запустим приложение и посмотрим на результат
Обратите внимание, что при нажатии кнопки «Вычислить», главный поток не блокируется. Также обратите внимание, что если вы будете добавлять новые сообщения быстрее, чем они будут обрабатываться (попробуйте очень быстро добавлять новые числа для вычисления), обработка сообщений будет происходить в режиме очереди. Фактически, в MessageQueue потока будут добавляться новые сообщения, которые потом, одна за одной, будут обрабатываться Handler`ом.