2. Looper-потоки. Работа с очередью сообщений с помощью Handler

Как мы знаем, процесс состоит из одного или нескольких потоков. Также мы знаем, что как только поток выполняет все входящие в него инструкции, он закрывается.

Но тогда каким образом главный поток (он же MainThread или UIThread) не закрывается после выполнения всех инструкций, а ждет действий пользователя, чтобы на них определенным образом отреагировать?

Для этого основной поток приложения модифицируется с помощью специального класса Looper.

Looper – который ещё иногда ещё называют «цикл обработки событий» используется для реализации бесконечного цикла который может получать задания.

Класс Looper позволяет подготовить Thread для обработки повторяющихся действий. Такой Thread, как показано на рисунке ниже, часто называют Looper Thread. Главный поток в приложении является этим самым Looper Thread. Объект Looper уникален для каждого потока.

Message – сообщение, которое представляет собой контейнер для набора инструкций которые будут выполнены в другом потоке.

Handler – данный класс обеспечивает взаимодействие с Looper Thread. С помощью Handler можно отправлять Message в Looper.

Поток может иметь только один Looper и, следовательно, один поток сообщений.

С помощью этой информации мы можем реализовать следующий функционал:

  1. Передавать сообщения из потока в поток, запускать код в другом потоке;

  2. Реализовать отложенное по времени выполнение кода.

Начнем с базовых вещей. Можно ли создать рабочий поток с использованием Looper`а и очереди сообщений? Да, это сделать очень просто, рассмотрим следующий код

LooperThread.java
class LooperThread extends Thread {

    @Override
    public void run() {
        // Подготовка объекта Looper
        Looper.prepare();
        
        // Запуск бесконечного цикла
        Looper.loop();
    }
}

Запускается такой поток как обычный поток

MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ...
    
        LooperThread thread = new LooperThread();
        thread.start();
    }
}

Отлично, мы запустили поток, но каким образом нам добавлять сообщения в очередь и как нам эти сообщения обрабатывать?

Для этого существует специальный класс, который называется Handler. При создании объекта класса Handler, он привязывается («биндится», bind) к конкретному объекту Looper`у. Модифицируем класс LooperThread

LooperThread.java
public class LooperThread extends Thread {

    private static Handler mHandler = new MyHandler();

    private static class MyHandler extends Handler {

        @Override
        public void handleMessage(Message msg) {
            // Обработчик сообщения
        }
    }
    
    public Handler getHandler() {
        return mHandler;
    }
    
    @Override
    public void run() {
        // Подготовка объекта Looper
        Looper.prepare();

        // Запуск бесконечного цикла
        Looper.loop();
    }
}

Теперь у нас есть созданный объект Handler и мы можем с помощью метода getHandler() получить ссылку на объект Handler, который привязан к объекту Looper`у созданного потока.

MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ...
    
        LooperThread thread = new LooperThread();
        thread.start();
        
        Handler looperThreadHandler = thread.getHandler();
    }
}

Таким же образом мы можем передать ссылку на Handler основного потока в другой поток.

MainActivity.java
public class MainActivity extends AppCompatActivity {

    private static class MainHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            // Обработчик Handler основного потока
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LooperThread thread = new LooperThread();
        thread.start();

        // Получаем Handler из LooperThread
        Handler looperThreadHandler = thread.getHandler();
        
        // Передаем Handler основного потока в LooperThread
        thread.setMainThreadHandler(new MainHandler());
    }
}

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

Рассмотрим небольшой пример, который позволит нам рассмотреть использование объектов класса Handler. Программа будет работать следующим образом – пользователь вводит некоторое целое число, после чего программа в отдельном потоке вычисляет факториал числа и показывает результат в текстовом поле.

Сначала создадим макет приложения

Создадим новый класс потока. В классе предусмотрим два поля для ссылок на Handler текущего потока и на Handler основного потока, с которым будет происходить взаимодействие.

Теперь нам необходимо закодировать следующие действия:

  • необходимо передать в объект Handler целое число;

  • внутри объекта Handler необходимо выполнить код для подсчета факториала;

  • в Handler основного потока необходимо передать изначальное число и результат вычисления факториала этого числа.

Работать с Handler можно двумя способами:

  1. в Handler можно передать объект Runnable с необходимым кодом для исполнения в looper-потоке;

  2. можно создать класс-наследник класса Handler и переопределить метод handleMessage(), в котором описать необходимый для исполнения код.

В рамках нашего примера мы воспользуемся вторым способом. Давайте сразу посмотрим на код, а ниже его прокомментируем.

LooperThread.java
public class LooperThread extends Thread {

    private static Handler mHandler = new MyHandler();
    private static Handler mainHandler;

    public void setMainThreadHandler(Handler mainThreadHandler) {
        this.mainHandler = mainThreadHandler;
    }

    private static class MyHandler extends Handler {

        private BigInteger factorial(BigInteger n) {
            BigInteger result = BigInteger.ONE;

            while (!n.equals(BigInteger.ZERO)) {
                result = result.multiply(n);
                n = n.subtract(BigInteger.ONE);
            }
            return result;
        }

        @Override
        public void handleMessage(Message msg) {

            // Извлекаем данные из сообщения
            Bundle b = msg.getData();
            int number = b.getInt("number");

            // Подсчет результата
            BigInteger result = factorial(BigInteger.valueOf(number));

            // Формируем bundle
            Bundle bundle = new Bundle();
            bundle.putInt("initial_number", number);
            bundle.putString("result", result.toString());

            // Формируем сообщение
            Message ret_message = Message.obtain();
            ret_message.setData(bundle);

            // Отсылаем сообщение
            mainHandler.sendMessage(ret_message);
        }
    }

    public Handler getHandler() {
        return mHandler;
    }

    @Override
    public void run() {
        // Подготовка объекта Looper
        Looper.prepare();

        // Запуск бесконечного цикла
        Looper.loop();
    }
}

Как мы видим, в обработчике нажатия на кнопку мы выполняем следующие действия:

  1. получаем введенное целое число из поля ввода;

  2. формируем Bundle, куда упаковываем целое число;

  3. создаем объект некоторого класса Message с помощью статического метода obtain(), куда добавляем созданный Bundle;

  4. получаем объект 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
public class MainActivity extends AppCompatActivity {

    @SuppressLint("HandlerLeak")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LooperThread thread = new LooperThread();
        thread.start();

        // Передаем Handler основного потока в LooperThread
        thread.setMainThreadHandler(new Handler() {
            @Override
            public void handleMessage(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 = new Bundle();
            b.putInt("number", number);

            Message message = Message.obtain();
            message.setData(b);

            thread.getHandler().sendMessage(message);
            et.getText().clear();

        });
    }
}

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

Обратите внимание, что при нажатии кнопки «Вычислить», главный поток не блокируется. Также обратите внимание, что если вы будете добавлять новые сообщения быстрее, чем они будут обрабатываться (попробуйте очень быстро добавлять новые числа для вычисления), обработка сообщений будет происходить в режиме очереди. Фактически, в MessageQueue потока будут добавляться новые сообщения, которые потом, одна за одной, будут обрабатываться Handler`ом.

Last updated