Многопоточность в Android. Looper, Handler, HandlerThread. Часть 1.

Что вы знаете о многопоточности в андроид? Вы скажете:  «Я могу использовать AsynchTask для выполнения задач в бэкграунде». Отлично, это популярный ответ, но что ещё? «О, я слышал что-то о Handler’ах, и даже как то приходилось их использовать для вывода Toast’ов или для выполнения задач с задержкой…» — добавите Вы. Это уже гораздо лучше, и в этой статье мы рассмотрим как и для чего используют многопоточность в Android.

Для начала давайте взглянем на хорошо известный нам класс AsyncTask, я уверен что каждый андроид-разработчик использовал его. Прежде всего, стоит заметить, что есть отличное описание этого класса в официальной документации. Это хороший и удобный класс для управления задачами в фоне, он подойдёт для выполнения простых задач, если вы не хотите тратить впустую время на изучение того как можно эффективно управлять потоками в андроид. Самая главная вещь о которой вы должны знать – только метод doInBackground выполняется в другом потоке! Остальные его методы выполняются в главном UI потоке. Рассмотрим пример типичного использования AsyncTask:

public class MyActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        MyAsyncTask myTask = new MyAsyncTask(this);
        myTask.execute("http://javaway.info/");
    }
}

public class MyAsyncTask extends AsyncTask<String, Void, Integer> {
    private Context mContext;

    public MyAsyncTask(Context context) {
        mContext = context.getApplicationContext();
    }

    @Override
    protected void onPreExecute() {
        Toast.makeText(mContext, "Let's start!", Toast.LENGTH_LONG).show();
    }

    @Override
    protected Integer doInBackground(String... params) {
        HttpURLConnection connection;
        try {
            connection = (HttpURLConnection) new URL(params[0]).openConnection();
            return connection.getResponseCode();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return -1;
    }

    @Override
    protected void onPostExecute(Integer integer) {
        if (integer != -1) {
            Toast.makeText(mContext, "Got the following code: " + integer, Toast.LENGTH_LONG).show();
        }
    }
}

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:gravity="center"
              android:orientation="vertical">

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

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

Здесь мы используем AsyncTask, потому что приложению потребуется некоторое время для получения ответа от сервера и мы не хотим что бы нам интерфейс завис в ожидании этого ответа, поэтому мы поручаем выполнить сетевую задачу другому потоку. Есть много постов о том, почему использование AsyncTask – это плохая затея(например: если это внутренний класс вашего активити/фрагмента, то он будет удерживать внутреннюю ссылку на него, что является плохой практикой, потому что активити/фрагмент могут быть уничтожены при смене конфигурации, но они будут висеть в памяти пока работает фоновый поток; если объявлен отдельным статическим внутренним классом и вы используете ссылку на Context для обновления вьюшек, вы должны всегда проверять их на null).



Все задачи в главном потоке выполняются последовательно, делая тем самым код более предсказуемым – вы не рискуете попасть в ситуацию изменения данных несколькими потоками. Значит если какая-то задача работает слишком долго, Вы получите неотвечающее приложени, или ANR(Application Not Responding) ошибку. AsyncTask является одноразовым решением. Класс не может быть повторно использован при повторном вызове execute метода на одном экземпляре – вы непременно должны создать новый экземпляр AsyncTask для новой работы.

Любопытно то, что если вы попытаетесь показать Toast из метода doInBackground, то получите ошибку, содержащую что то вроде:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() at 
android.os.Handler.<init>(Handler.java:121) at 
android.widget.Toast$TN.<init>(Toast.java:322) at 
android.widget.Toast.<init>(Toast.java:91) at 
android.widget.Toast.makeText(Toast.java:238) at 
com.example.testapp.MyActivity$MyAsyncTask.doInBackground(MyActivity.java:25) at 
com.example.testapp.MyActivity$MyAsyncTask.doInBackground(MyActivity.java:21)

Из-за чего же мы получили ошибку? Ответ прост: потому что Toast является частью интерфейса и может быть показан только  из UI потока, а правильный ответ: потому что он может быть показан только из потока с Looper’ом! Вы спросите, что такое Looper?

Хорошо, пришло время копнуть глубже. AsyncTask отличный класс, но что если его функциональности недостаточно для ваших действий? Если мы заглянем под капот AsyncTask, то обнаружим устройство с крепко связанными компонентами: Handler, Runnable и Thread. Каждый из вас знаком с потоками в Java, но в андройде вы обнаружите ещё один класс HandlerThread, произошедший от Thread. Единственное существенное отличие между HandlerThread и Thread заключается в том что первый содержит внутри себя Looper, Thread и MessageQueue. Looper трудится, обслуживая looperMessageQueue для текущего потока. MessageQueue это очередь которая содержит в себе задачи, называющиеся сообщениями, которые нужно обработать. Looper перемещается по этой очереди и отправляет сообщения в соответствующие обработчики для выполнения. Любой поток может иметь единственный уникальлный Looper, это ограничение достигается с помощью концепции ThreadLocal хранилища. Связка Looper+MessageQueue выглядит как конвейер с коробками.  Задачи в очередь помещают Handler‘ы.

Вы можете спросить: «Для чего вся эта сложность, если задачи всё равно обрабатываются их создателями – Handler‘ами?». Мы получаем как минимум 2 преимущества:

— это помогает избежать состояния гонки (race conditions), когда работа приложения становится зависимой от порядка выполнения потоков;

— Thread не может быть повторно использован после завершения работы. А если поток работает с Looper’ом, то вам не нужно повторно создавать экземпляр Thread каждый раз для работы в бэкграунде.

Вы можете сами создавать и управлять Thread’ами и Looper’ами, но я рекомендую воспользоваться HandlerThread (Google решили использовать HandlerThread вместо LooperThread) : В нём уже есть встроенный Looper и всё настроено для работы.

А что о  Handler? Это класс с двумя главными функциями: отправлять задачи в очередь сообщений (MessageQueue) и выполнять их. По умолчанию Handler неявно связывается с потоком в котором он был создан с помощью Looper’a, но вы можете связать его явно с другим потоком, если предоставите ему другой Looper в конструкторе. Наконец-то пришло время собрать все куски теории вместе и взглянуть на примере как всё это работает! Представим себе Activity в которой мы хотим отправлять задачи(в моей статье задачи представлены экземплярами Runnable интерфейса, что такое на самом деле задача(или сообщение) я расскажу во второй части статьи) в очередь сообщений(все активити и фрагменты существуют в главном UI потоке), но они должны выполняться с некоторой задержкой:

public class MyActivity extends Activity {
    private Handler mUiHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Thread myThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (i == 2) {
                        mUiHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(MyActivity.this, "I am at the middle of background task", Toast.LENGTH_LONG).show();
                            }
                        });
                    }
                }
                mUiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MyActivity.this, "Background task is completed", Toast.LENGTH_LONG).show();
                    }
                });
            }
        });
        myThread.start();
    }
}

Так как mUiHandler связан с главным потоком(он получил Looper главного потока в конструкторе по умолчанию) и он является членом класса, мы можем получить к нему доступ из внутреннего анонимного класса и поэтому можем отправлять задачи-сообщения в главный поток. Мы используем Thread в примере выше и не можем использовать его повторно, если хотим выполнить новую задачу. Для этого нам придется создать новый экземпляр. Есть другое решение? Да! Мы можем использовать поток с Looper’ом. Давайте немного модифицируем пример выше с целью заменить Thread на HandlerThread для демонстрации того, какой прекрасной способностью к повторному использованию он обладает:

public class MyActivity extends Activity {

    private Handler mUiHandler = new Handler();
    private MyWorkerThread mWorkerThread;

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

        mWorkerThread = new MyWorkerThread("myWorkerThread");
        Runnable task = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (i == 2) {
                        mUiHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(MyActivity.this,
                                        "I am at the middle of background task",
                                        Toast.LENGTH_LONG)
                                        .show();
                            }
                        });
                    }
                }
                mUiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MyActivity.this,
                                "Background task is completed",
                                Toast.LENGTH_LONG)
                                .show();
                    }
                });
            }
        };
        mWorkerThread.start();
        mWorkerThread.prepareHandler();
        mWorkerThread.postTask(task);
        mWorkerThread.postTask(task);
    }

    @Override
    protected void onDestroy() {
        mWorkerThread.quit();
        super.onDestroy();
    }
}


public class MyWorkerThread extends HandlerThread {

    private Handler mWorkerHandler;

    public MyWorkerThread(String name) {
        super(name);
    }

    public void postTask(Runnable task){
        mWorkerHandler.post(task);
    }

    public void prepareHandler(){
        mWorkerHandler = new Handler(getLooper());
    }
}

Я использую HandlerThread в этом примере, потому что я не хочу управлять Looper’ом сам, HandlerThread сам справится с этим. Один раз мы стартуем HandlerThread, после чего можем отправлять задачи в любое время, но помните о вызове метода quit когда вы хотите остановить HandlerThread. mWorkerHandler связан с MyWorkerThread с помощью его Looper. Вы не сможете инициализировать mWorkerHandler за пределами конструктора HandlerThread , так как getLooper будет возвращать null, потому что поток ещё не существует. Порой вам может встретится следующий способ инициализации Handler:

private class MyWorkerThread extends HandlerThread {
    private Handler mWorkerHandler;

    public MyWorkerThread(String name) {
        super(name);
    }

    @Override
    protected void onLooperPrepared() {
        mWorkerHandler = new Handler(getLooper());
    }

    public void postTask(Runnable task) {
        mWorkerHandler.post(task);
    }
}

Порой это отлично работает, но иногда вы будете выхватывать NPE после вызова
postTask, с сообщением о том, что ваш mWorkerHandler  — null. Сюрприз!

u71ms

Почему это произошло? Хитрость здесь в нативном вызове, необходимом для создании нового потока. Если мы взглянем на кусок кода, где вызывается onLooperPrepared, мы найдём следующий фрагмент в HandlerThread классе:

public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }

Хитрость заключается в том, что run метод будет вызван только после того, как новый поток создаётся и запускается. И этот вызов может иногда произойти после вызова postTask(можете проверить это самостоятельно, просто поместите точку останова внутри postTask и onLooperPreparerd методов и взгляните, какой из них будет первым), таким образом вы можете стать жертвой состояния гонки между двух потоков (main и background).

Во второй части разберем как на самом деле внутри MessageQueue работают задачи.

Подготовлено по материалам сайта blog.nikitaog.me

комментариев 5

  1. Я не уверен, но вроде в статье есть опечатка.

    mWorkerHandler связан с MyWorkerThread с помощью его Looper. Вы не сможете инициализировать mWorkerHandler за пределами конструктора HandlerThread , так как getLooper будет возвращать null, потому что поток ещё не существует.

    Насколько я понял, mWorkerHandler мы не сможем инициализировать В ПРЕДЕЛАХ конструктора HandlerThread, так как поток ещё не будет создан.
    Поправьте меня, если я ошибаюсь.

  2. ПО личному делу. Необходимо построить здание (автосервис), скорее всего из металлоконструкций (но еще думаем). Надо подрядчик. Но только чтобы ответственный был человек, а то был опыт так скажем «не очень». Может есть здесь у кого знакомые?

Добавить комментарий для ИгорьОтменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *