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

    На 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 буде создан.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 29
    • +5
      Об этом уже писали
      • 0
        А правда, что кто-то для сетевых соединений запускает сервисы?)
        • +1
          Кстати, запуск повторяющихся задача можно реализовать, если в Runnable, соответствующий задаче, добавить handler.postDelayed(this, TIMEOUT);
          • 0
            Это немного другое. Это отложенный запуск а не честный запуск с повтором. Но с помощью этого метода конечно же можно сделать повторение, да.
          • 0
            Если сетевое соединение должно некоторое время поддерживаться даже если пользователь выйдет из приложения (из его UI), то нужен сервис.
            • 0
              cовсем не обязательно, можно в отдельном потоке, просто уровень приложения поднять и в бекграунде система его не будет убивать часто
              • 0
                Само собой, но тогда приложение будет висеть в памяти всегда, а не только когда это необходимо. Сервис — самый разумный способ реализовать фоновую задачу, которая должна быть завершена даже в случае закрытия приложения.
              • 0
                Например, в моем проекте критично быть онлайн и получать обновления. Информация об обновлениях показывается в области уведомлений. Приложению при этом совсем не нужно быть запущенным.

                Для всех сетевых взаимодействий запущен один отдельный поток с блокирующей очередью запросов. Такой подход себя зарекомендовал в нескольких проектах (Android и Java ME).
                • +1
                  Парни для этих целей есть IntentService — очередь + отдельный поток выполнения. Сервис автоматом остановится как только не будет тасков.

                  Для переодичных задач пользуйтес alarm менеджером. Говорите ему что бы кинул вам интент в нужный сервис и выполняете таску.
                  • +1
                    Спасибо, скажу парням чтобы глянули)
            • +2
              В 3-м и наверно 4-м андроиде сетевой вызов в UI потоке и не сделаешь — вылетит исключение.
            • –1
              Спасибо за статью!
              Как раз гуглил инфу по данной теме, потом зашёл на хабр, а тут свежеиспечённая, готовая к употреблению статья :)
            • 0
              Возможно это дело вкуса, но я очень не люблю эти вложенные классы. В своей задаче у меня несколько сетевых потоков, вызывающих отдельные классы реализующие Runnable и получающих в качестве аргумента конструктора экземпляр Handler. Обмен с потоком осуществляется через Message. Если в общих чертах, это выглядит так:

              pastebin.com/s8FsGsEa
              • +1
                Разве в 1ом варианте нельзя использовать context#runOnUiThread(Runnable)?
              • +1
                Спасибо за статью, но вы помимо AsyncTask и Handler забыли упомянуть о методе runOnUiThread(… )
                • 0
                  Есть еще нюанс, я по такой схеме сделал приложение, которое поллит сервер и шлет данные в гуи. Но вот гуевый компонент передавал в сервис через статическое поле, что, вероятно приводило к проблемам. Таймер (да и экзекутор) отказывался второй раз выполнять асинк таск, без каких либо эксепшинов или записей в логах.

                  Помучавшись пару вечеров, я сдался и написал вопрос на stackoverflow и мне порекомендовали сделать по человечески. В смысле передавать данные от сервиса к клиенту через Intent и Listener'a.

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

                  • 0
                    CommonsWare — крутой чел, да.
                  • 0
                    Для повторяющихся задач Timer не рекомендуют использовать, для этого лучше подходит Handler.
                    Также можно использовать AlarmManager. Тут пример.
                    • 0
                      AlarmManager слишком тяжелый для простых случаев. Он больше подойдут стартовать приложение в указанное время + будить телефон из спячки + держать его не спящим.
                    • 0
                      Взгляните на WakefullIntentService — очень красивое решение бекграунд-вопросов. Не спит, выполняется в отдельном потоке, живет ровно столько, сколько нужно на выполнение поставленных в очередь задач.
                      • 0
                        Автор забыл упомянуть о IntentService, который предназначен именно для выполнения задач в бэкграунде
                        • 0
                          А что если в приложении нужно реализовать например счетчик подсчета СМС например через BroadcastReceiver? Если запустить его в Main Activity то в какой то момент андроид может освободить апликацию.

                          Была мысль сделать его через Service: Main Activity запускает сервис который запускает BroadcastReceiver.
                          Но почитав тут уже не уверен…

                          Буду рад услышать ваше мнение.
                          • 0
                            А почему бы не объявить Receiver в манифесте? Он не будет зависеть ни от Activity, ни от Service.
                            • 0
                              Написал в приват дабы не расплодить оффтоп :)
                          • 0
                            Сделаем так, чтобы AsyncTask из предыдущего примера выполнялся раз в минуту…

                            И после Вы привели код выполняющий не AsyncTask.

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