Development for Android
25 January 2012

Выполнение задач в бэкграунде

На Stackoverflow часто встречаются вопросы по выполнению на Android фоновых задач, в т.ч. и повторяющихся с заданным промежутком времени. Как правило, первое, что используется, это Service.

Такой подход в некоторых случаях может привести к тормозам и низкой скорости ответа пользовательского интерфейса. Расскажу когда так бывает и как с этим бороться…

Почему может «тормозить» Service


Каждое Android-приложение по-умолчанию запускается в отдельном процессе. В каждом процессе запускаются потоки (Thread). По-умолчанию все компоненты приложения (Activity, Service, BroadcastReceiver) запускаются в одном «main» потоке (он же UI-thread). Если внутри сервиса запустить, например, долгий сетевой вызов или какую-то тяжелую инициализацию, мы получим тормоза всего приложения, его интерфейса и, скорее всего, предложение сделать Force close… Впрочем, работа службы в том же потоке, что и остальное приложение, имеет свои плюсы — у вас есть доступ к элементам интерфейса. Если бы служба работала в другом потоке, доступ к UI у вас бы отсутствовал.

Что делать?


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

Для запуска задачи в отдельном потоке можно воспользоваться следующими средствами SDK:
  • создать Thread
  • использовать AsyncTask

Запускаем свой поток


Поток создать просто…

Thread myThread = new Thread(new Runnable() {
    @Override
    pubic void run() {
        doLongAndComplicatedTask();
    }
});

myThread.start(); // запускаем

Все просто, но проблемы начинаются, когда после выполнения длинного задания нам захочется обновить UI.

final TextView txtResult = (TextView)findViewById(R.id.txtResult);
Thread myThread = new Thread(new Runnable() {
    @Override
    public void run() {
        txtResult.setText(doLongAndComplicatedTask());
    }
});

myThread.start();

В результате выполнения получим ошибку. «Чужой» поток попытался обратиться к UI! Как вылечить? Надо использовать Handler. Доработаем код…

final Handler myHandler = new Handler(); // автоматически привязывается к текущему потоку.
final TextView txtResult = (TextView)findViewById(R.id.txtResult);
Thread myThread = new Thread(new Runnable() {
    final String result = doLongAndComplicatedTask();
    myHandler.post(new Runnable() {  // используя Handler, привязанный к UI-Thread
        @Override
        public void run() {
            txtResult.setText(result);         // выполним установку значения
        }
    });
});

myThread.start();

AsyncTask — все проще


Для реализации подобных задач в Android SDK имеет встроенное средство — AsyncTask. Данный класс позволяет не думать о том, в каком потоке выполняется ваш код, все происходит автоматически. Рассмотрим пример выше переписанный на AsyncTask.

class LongAndComplicatedTask extends AsyncTask<Void, Void, String> {
    
    @Override
    protected String doInBackground(Void... noargs) {
        return doLongAndComplicatedTask();
    }

    @Override
    protected void onPostExecute(String result) {
        txtResult.setText(result);
    }
}

LongAndComplicatedTask longTask = new LongAndComplicatedTask(); // Создаем экземпляр
longTask.execute(); // запускаем

Метод doInBackground будет выполнен в отдельном потоке, результат его выполнения будет передан в метод onPostExecute, который, в свою очередь будет выполнен на UI-Thread'е
Следует помнить, что:
  • AsyncTask может выполняться лишь раз. Для повторного запуска нужно пересоздать класс;
  • execute() должен быть выполнен на UI-Thread'е.

А что делать, если задачу нужно выполнять регулярно, через определенные промежутки времени…

Таймер. Самый простой подход к периодическому запуску.


Java предоставляет Timer для запуска повторяющихся задач. Сделаем так, чтобы AsyncTask из предыдущего примера выполнялся раз в минуту…

Timer myTimer = new Timer(); // Создаем таймер
final Handler uiHandler = new Handler();
final TextView txtResult = (TextView)findViewById(R.id.txtResult);
myTimer.schedule(new TimerTask() { // Определяем задачу
    @Override
    public void run() {
        final String result = doLongAndComplicatedTask();
        uiHandler.post(new Runnable() {
            @Override
            public void run() {
                txtResult.setText(result);
            }
        });
    });
}, 0L, 60L * 1000); // интервал - 60000 миллисекунд, 0 миллисекунд до первого запуска.


Стоит заметить, что все упрощается если «долгоиграющая» задача не требует доступ к UI. В этом случае не требуются ни Handler'ы, ни AsyncTask'и.

Кстати, у таймера есть еще метод scheduleAtFixedRate(). Различия между ним и schedule() описаны в документации.

Более гибкий способ. ScheduledThreadPoolExecutor.


Класс ScheduledThreadPoolExecutor указан как рекомендуемая альтернатива использованию Timer. Данный класс позволяет организовать пул потоков и планировать выполняемые задачи относительно него. Т.е. класс для организации очереди заданий один, но тем не менее он может выполнять одновременно несколько заданий, если это позволяет имеющееся количество доступных потоков. Более подробно о преимуществах — в документации.

Для каждого заплаированного задания доступен его «дескриптор» — ScheduledFuture с помощью которого можно, например, отменить выполнения одного конкретного задания не трогая весь остальной пул.

Задание со звездочкой. Велосипед. Thread, Looper, Handler.


А еще можно собрать свой велосипед поток с очередью.

public class LoopingThread extends Thread {
    private CountdownLatch syncLatch = new CountdownLatch(1);
    private Handler handler;

    public LoopingThread() {
        super();
        start();
    } 

    @Override
    public void run() {
        try {
            Looper.prepare();
            handler = new Handler();
            syncLatch.countDown();
            Looper.loop();
        } catch(Exception e) {
            Log.d("LoopingThread", e.getMessage());
        }
    }

    public Handler getHandler() {
        syncLatch.await();
        return handler;
    }
}

Thread loopThread = new LoopingThread(); // будет выполняться вечно
loopThread.getHandler().post(new Runnable() {
    @Override
    public void run() {
        doLongAndComplicatedTask();
    }
});

Данный поток, будучи единожды запущенным, выполняется вечно. Для общения с ним (отправки заданий) можно использовать метод getHandler() для получения хандлера и дальнейшей отправкой «событий» в него. CountdownLatch используется для синхронизации, чтобы поток, желающий получить Handler, не получил его ранее того момента, когда поток-работник запустится и Handler буде создан.

+38
107.6k 319
Comments 29