Статьи

Android с нуля: фоновые операции

Помимо самых основных приложений Android, все, что вы создаете, потребует, по крайней мере, некоторого использования фонового потока для выполнения операции. Это связано с тем, что в Android есть время, известное как тайм-аут ANR (приложение не используется), которое возникает, когда операция занимает пять или более секунд в потоке пользовательского интерфейса, предотвращая ввод пользователя и вызывая зависание пользователя. приложение.

Чтобы избежать этого, вы должны перенести более длительные операции, такие как сетевые запросы или медленные запросы к базе данных, в другой поток, чтобы не мешать пользователю продолжать использовать ваше приложение. Несмотря на то, что всесторонний охват многопоточности является крупным и сложным предметом в области компьютерных наук, этот учебник познакомит вас с основными понятиями многопоточности в Android и с некоторыми из доступных инструментов, которые помогут вам создавать приложения, которые работают лучше с использованием фоновых процессов.

Тебе легче учиться с видео? Почему бы не проверить наш курс:

  • Android
    Начало работы с Android

Когда приложение запускается, запускается новый процесс Linux с одним основным потоком выполнения. Это поток, который имеет доступ к инструментарию пользовательского интерфейса Android, прослушивает вводимые пользователем данные и обрабатывает рисование на экране устройства Android. Из-за этого это также обычно называют потоком пользовательского интерфейса .

Все компоненты приложения выполняются в одном потоке и обрабатываются по умолчанию, хотя могут быть созданы дополнительные потоки для перемещения задач из потока пользовательского интерфейса и предотвращения ANR. Когда дело доходит до многопоточности в Android, нужно помнить о двух простых правилах, чтобы приложение работало должным образом:

  1. Не блокируйте поток пользовательского интерфейса.
  2. Не пытайтесь получить доступ к компонентам пользовательского интерфейса Android вне потока пользовательского интерфейса.

Хотя вы можете соблюдать первое правило, просто создав новый Thread и Runnable , обработка второго правила становится немного сложнее. Рассмотрим следующий фрагмент кода:

01
02
03
04
05
06
07
08
09
10
11
new Thread(new Runnable() {
    public void run() {
        try {
            Thread.sleep(6000);
        } catch( InterruptedException e ) {
 
        }
 
        mTextView.setText(«test»);
    }
}).start();

Хотя этот код не останавливает поток пользовательского интерфейса, пока поток спит после истечения времени ожидания ANR, попытка установить текст TextView приведет к тому, что приложение выдаст следующую ошибку:

1
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

К счастью, есть несколько простых способов обойти это. Вы можете использовать метод Android runOnUiThread(Runnable) чтобы выполнить код обратно в главном потоке приложения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
mTextView = (TextView) findViewById(R.id.text);
new Thread(new Runnable() {
    public void run() {
        try {
            Thread.sleep(6000);
        } catch( InterruptedException e ) {
 
        }
 
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mTextView.setText(«test»);
            }
        });
    }
}).start();

Или вы можете взять стандартный объект View и post на нем Runnable .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
new Thread(new Runnable() {
    public void run() {
        try {
            Thread.sleep(6000);
        } catch( InterruptedException e ) {
 
        }
 
        mTextView.post(new Runnable() {
            @Override
            public void run() {
                mTextView.setText(«test»);
            }
        });
    }
}).start();

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

AsyncTaskAsyncTask один из инструментов, предоставляемых Android для управления сложностью фоновых потоков. AsyncTask предоставляет рабочий поток для блокирования операций, а затем AsyncTask результат обратно в поток пользовательского интерфейса с помощью предварительно созданного метода обратного вызова, что позволяет легко выполнять задачи без необходимости использовать потоки и обработчики.

Прежде чем начать использовать класс AsyncTask , вам необходимо понять жизненный цикл по сравнению с выполнением операции в главном потоке.

AsyncTask Lifecycle

Первый метод, который вызывается AsyncTask это onPreExecute() . Этот метод выполняется в потоке пользовательского интерфейса и предназначен для настройки любых компонентов интерфейса, которые должны дать пользователю знать, что что-то происходит.

После onPreExecute() вызывается doInBackground(T) . Общий параметр здесь — это любая информация, которую вам нужно передать методу, чтобы он мог выполнить свою задачу. Например, если вы пишете задачу, которая извлекает JSON из URL-адреса, вы передадите URL-адрес этому методу в виде String . Когда операция выполняется в doInBackground() , вы можете вызвать onProgressUpdate(T) чтобы обновить ваш пользовательский интерфейс (например, индикатор выполнения на экране). Здесь универсальный является значением, представляющим прогресс, такой как Integer .

После завершения doInBackground() он может вернуть объект, переданный в onPostExecute(T) , такой как JSONObject который был загружен с нашего исходного URL-адреса. onPostExecute(T) работает в потоке пользовательского интерфейса.

Когда вы создаете класс AsyncTask , вы должны переопределить эти обобщения как в объявлении класса, так и в описанных выше методах. Пример AsyncTask который обновляет ProgressBar каждую секунду, можно увидеть здесь:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected class DemoAsyncTask extends AsyncTask<Integer, Void, String> {
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mProgress.setProgress(0);
        mProgress.setVisibility(View.Visible);
    }
 
    @Override
    protected void onProgressUpdate(Integer… values) {
        super.onProgressUpdate(values);
        mProgress.setProgress(values[0]);
    }
 
    @Override
    protected Void doInBackground(Void… params) {
 
        for( int i = 0; i < 100; i++ ) {
            try {
                Thread.sleep(1000);
            } catch( InterruptedException e ) {}
            publishProgress(i);
        }
 
       return «All done!»;
 
    }
 
    @Override
    protected void onPostExecute(String result) {
        super.onPostExecute(aVoid);
        if( isCancelled() ) {
            return;
        }
 
        mProgress.setVisibility(View.GONE);
        Toast.makeText(context, result, Toast.LENGTH_SHORT).show();
    }
}

Возможно, вы заметили, что onPostExecute(T) проверяет isCancelled() . Это потому, что есть одна большая проблема с AsyncTasks : они поддерживают ссылку на Context даже после того, как этот Context был уничтожен.

Это легче всего увидеть при запуске AsyncTask а затем при вращении экрана. Если вы попытаетесь сослаться на элемент Context (например, View или действие) после уничтожения исходного Context , будет Exception . Самый простой способ обойти это — вызвать метод cancel(true) для AsyncTask в методе onDestroy() вашего Activity или Fragment , а затем проверить, что задача не была отменена в onPostExecute(T) .

Как и в любом AsyncTask программировании, ответ на вопрос, когда следует использовать AsyncTask : это зависит. Хотя AsyncTasks просты в использовании, они не являются AsyncTasks и конечным решением для многопоточности, и их лучше всего использовать для коротких операций, длящихся не более нескольких секунд. Если у вас есть операция, которая может длиться дольше, я рекомендую вам изучить использование ThreadPoolExecutor , Service или GcmNetworkManager (обратно совместимая версия JobScheduler ).

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

Запущенная Service запускается компонентом в вашем приложении и остается активной в фоновом режиме устройства, даже если исходный компонент уничтожен. Когда задача, которую выполняет запущенная Service , завершена, Service остановится сама. Стандартная запущенная Service обычно используется для длительных фоновых задач, которым не требуется связь с остальной частью приложения.

Связанная Service похожа на запущенную Service и предоставляет обратные вызовы для различных компонентов приложения, которые могут с ней связываться. Когда все связанные компоненты отсоединятся от Service , он остановится сам. Важно отметить, что эти два способа запуска Service не являются взаимоисключающими: вы можете запустить Service которая будет работать неограниченное время и иметь привязанные к ней компоненты.

Одна из самых больших проблем со стандартной Service заключается в том, что она не может обрабатывать несколько запросов одновременно, поскольку это было бы многопоточным кошмаром. Одним из способов решения этой IntentService является расширение IntentService , которое расширяет стандартную Service . IntentService создает рабочий поток по умолчанию для выполнения всех намерений, полученных в onStartCommand() , поэтому все операции могут выполняться вне основного потока. Затем он создает рабочую очередь для отправки каждого намерения в onHandleIntent() одному, чтобы вам не onHandleIntent() беспокоиться о проблемах с многопоточностью.

Помимо обработки потоков, IntentService также автоматически останавливается после обработки всех запросов на запуск. Поскольку все детали реализации обрабатываются в IntentService , работа для вас, как для разработчика, довольно проста.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class ExampleIntentService extends IntentService {
 
  //required constructor with a name for the service
  public ExampleIntentService() {
      super(«ExampleIntentService»);
  }
 
  @Override
  protected void onHandleIntent(Intent intent) {
    //Perform your tasks here
      try {
          Thread.sleep(5000);
      } catch (InterruptedException e) {}
  }
}

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