На Stackoverflow часто встречаются вопросы по выполнению на Android фоновых задач, в т.ч. и повторяющихся с заданным промежутком времени. Как правило, первое, что используется, это Service.
Такой подход в некоторых случаях может привести к тормозам и низкой скорости ответа пользовательского интерфейса. Расскажу когда так бывает и как с этим бороться…
Каждое Android-приложение по-умолчанию запускается в отдельном процессе. В каждом процессе запускаются потоки (Thread). По-умолчанию все компоненты приложения (Activity, Service, BroadcastReceiver) запускаются в одном «main» потоке (он же UI-thread). Если внутри сервиса запустить, например, долгий сетевой вызов или какую-то тяжелую инициализацию, мы получим тормоза всего приложения, его интерфейса и, скорее всего, предложение сделать Force close… Впрочем, работа службы в том же потоке, что и остальное приложение, имеет свои плюсы — у вас есть доступ к элементам интерфейса. Если бы служба работала в другом потоке, доступ к UI у вас бы отсутствовал.
Для решения данной проблемы стоит для тяжелых задач использовать отдельный поток. На самом деле его даже не обязательно создавать внутри службы…
Для запуска задачи в отдельном потоке можно воспользоваться следующими средствами SDK:
Поток создать просто…
Все просто, но проблемы начинаются, когда после выполнения длинного задания нам захочется обновить UI.
В результате выполнения получим ошибку. «Чужой» поток попытался обратиться к UI! Как вылечить? Надо использовать Handler. Доработаем код…
Для реализации подобных задач в Android SDK имеет встроенное средство — AsyncTask. Данный класс позволяет не думать о том, в каком потоке выполняется ваш код, все происходит автоматически. Рассмотрим пример выше переписанный на AsyncTask.
Метод doInBackground будет выполнен в отдельном потоке, результат его выполнения будет передан в метод onPostExecute, который, в свою очередь будет выполнен на UI-Thread'е
Следует помнить, что:
А что делать, если задачу нужно выполнять регулярно, через определенные промежутки времени…
Java предоставляет Timer для запуска повторяющихся задач. Сделаем так, чтобы AsyncTask из предыдущего примера выполнялся раз в минуту…
Стоит заметить, что все упрощается если «долгоиграющая» задача не требует доступ к UI. В этом случае не требуются ни Handler'ы, ни AsyncTask'и.
Кстати, у таймера есть еще метод scheduleAtFixedRate(). Различия между ним и schedule() описаны в документации.
Класс ScheduledThreadPoolExecutor указан как рекомендуемая альтернатива использованию Timer. Данный класс позволяет организовать пул потоков и планировать выполняемые задачи относительно него. Т.е. класс для организации очереди заданий один, но тем не менее он может выполнять одновременно несколько заданий, если это позволяет имеющееся количество доступных потоков. Более подробно о преимуществах — в документации.
Для каждого заплаированного задания доступен его «дескриптор» — ScheduledFuture с помощью которого можно, например, отменить выполнения одного конкретного задания не трогая весь остальной пул.
А еще можно собрать свойвелосипед поток с очередью.
Данный поток, будучи единожды запущенным, выполняется вечно. Для общения с ним (отправки заданий) можно использовать метод getHandler() для получения хандлера и дальнейшей отправкой «событий» в него. CountdownLatch используется для синхронизации, чтобы поток, желающий получить Handler, не получил его ранее того момента, когда поток-работник запустится и Handler буде создан.
Такой подход в некоторых случаях может привести к тормозам и низкой скорости ответа пользовательского интерфейса. Расскажу когда так бывает и как с этим бороться…
Почему может «тормозить» 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 буде создан.