Android Threading: все, что вам нужно знать
Опубликовано: 2022-03-11Каждому разработчику Android в тот или иной момент приходится иметь дело с потоками в своем приложении.
Когда приложение запускается в Android, оно создает первый поток выполнения, известный как «основной» поток. Основной поток отвечает за отправку событий соответствующим виджетам пользовательского интерфейса, а также за взаимодействие с компонентами из набора инструментов пользовательского интерфейса Android.
Чтобы поддерживать отзывчивость вашего приложения, важно избегать использования основного потока для выполнения любой операции, которая может привести к его блокировке.
Сетевые операции и вызовы базы данных, а также загрузка определенных компонентов — типичные примеры операций, которых следует избегать в основном потоке. Когда они вызываются в основном потоке, они вызываются синхронно, а это означает, что пользовательский интерфейс не будет отвечать до тех пор, пока операция не завершится. По этой причине они обычно выполняются в отдельных потоках, что позволяет избежать блокировки пользовательского интерфейса во время их выполнения (т. е. они выполняются асинхронно из пользовательского интерфейса).
Android предоставляет множество способов создания потоков и управления ими, и существует множество сторонних библиотек, которые делают управление потоками намного более приятным. Тем не менее, с таким количеством разных подходов выбор правильного может быть довольно запутанным.
В этой статье вы узнаете о некоторых распространенных сценариях разработки под Android, где многопоточность становится необходимой, а также о некоторых простых решениях, которые можно применить к этим сценариям, а также о многом другом.
Многопоточность в Android
В Android вы можете разделить все компоненты многопоточности на две основные категории:
- Потоки, прикрепленные к действию/фрагменту: эти потоки привязаны к жизненному циклу действия/фрагмента и завершаются, как только действие/фрагмент уничтожается.
- Потоки, которые не привязаны ни к какому действию/фрагменту: эти потоки могут продолжать работать после окончания срока действия действия/фрагмента (если есть), из которого они были созданы.
Компоненты потоков, которые присоединяются к действию/фрагменту
Асинтаск
AsyncTask
— это самый простой компонент Android для многопоточности. Он прост в использовании и может быть полезен для базовых сценариев.
Пример использования:
public class ExampleActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); new MyTask().execute(url); } private class MyTask extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... params) { String url = params[0]; return doSomeWork(url); } @Override protected void onPostExecute(String result) { super.onPostExecute(result); // do something with result } } }
AsyncTask
, однако, терпит неудачу, если вам нужно, чтобы ваша отложенная задача выполнялась за пределами времени жизни действия/фрагмента. Стоит отметить, что даже такая простая вещь, как поворот экрана, может привести к уничтожению активности.
Погрузчики
Загрузчики являются решением проблемы, упомянутой выше. Загрузчики могут автоматически останавливаться при уничтожении активности, а также перезапускаться после повторного создания активности.
В основном есть два типа загрузчиков: AsyncTaskLoader
и CursorLoader
. Вы узнаете больше о CursorLoader
позже в этой статье.
AsyncTaskLoader
похож на AsyncTask
, но немного сложнее.
Пример использования:
public class ExampleActivity extends Activity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getLoaderManager().initLoader(1, null, new MyLoaderCallbacks()); } private class MyLoaderCallbacks implements LoaderManager.LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { return new MyLoader(ExampleActivity.this); } @Override public void onLoadFinished(Loader loader, Object data) { } @Override public void onLoaderReset(Loader loader) { } } private class MyLoader extends AsyncTaskLoader { public MyLoader(Context context) { super(context); } @Override public Object loadInBackground() { return someWorkToDo(); } } }
Потоковые компоненты, которые не присоединяются к действию/фрагменту
Оказание услуг
Service
— это компонент, который полезен для выполнения длительных (или потенциально длительных) операций без какого-либо пользовательского интерфейса.
Service
работает в основном потоке хост-процесса; служба не создает свой собственный поток и не запускается в отдельном процессе, если вы не укажете иное.
Пример использования:
public class ExampleService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { doSomeLongProccesingWork(); stopSelf(); return START_NOT_STICKY; } @Nullable @Override public IBinder onBind(Intent intent) { return null; } }
В случае с Service
вы обязаны остановить его, когда его работа будет завершена, вызвав либо метод stopSelf()
, либо метод stopService()
.
ИнтентСервис
Как и Service
, IntentService
работает в отдельном потоке и автоматически останавливается после завершения своей работы.
IntentService
обычно используется для коротких задач, которые не нужно привязывать к какому-либо пользовательскому интерфейсу.
Пример использования:
public class ExampleService extends IntentService { public ExampleService() { super("ExampleService"); } @Override protected void onHandleIntent(Intent intent) { doSomeShortWork(); } }
Семь шаблонов многопоточности в Android
Вариант использования № 1: выполнение запроса по сети без запроса ответа от сервера
Иногда вам может понадобиться отправить запрос API на сервер, не беспокоясь о его ответе. Например, вы можете отправить токен принудительной регистрации на серверную часть вашего приложения.
Поскольку это включает в себя выполнение запроса по сети, вы должны делать это из потока, отличного от основного потока.
Вариант 1: AsyncTask или загрузчики
Вы можете использовать AsyncTask
или загрузчики для вызова, и это сработает.
Однако и AsyncTask
, и загрузчики зависят от жизненного цикла действия. Это означает, что вам нужно либо дождаться выполнения вызова и попытаться предотвратить выход пользователя из активности, либо надеяться, что он выполнится до того, как активность будет уничтожена.
Вариант 2: Сервис
Service
может лучше подходить для этого варианта использования, поскольку она не привязана ни к какому действию. Таким образом, он сможет продолжить сетевой вызов даже после уничтожения активности. Кроме того, поскольку ответ от сервера не требуется, служба также не будет ограничивать здесь.
Однако, поскольку служба начнет работать в потоке пользовательского интерфейса, вам все равно придется управлять потоками самостоятельно. Вам также необходимо убедиться, что служба остановлена после завершения сетевого вызова.
Это потребует больше усилий, чем должно быть необходимо для такого простого действия.
Вариант 3: Служба намерений
Это, на мой взгляд, будет лучшим вариантом.
Поскольку IntentService
не присоединяется к какой-либо активности и работает в потоке, отличном от пользовательского интерфейса, здесь он идеально подходит для наших нужд. Более того, IntentService
останавливается автоматически, поэтому нет необходимости управлять им вручную.
Вариант использования № 2: выполнение сетевого вызова и получение ответа от сервера
Этот вариант использования, вероятно, немного более распространен. Например, вы можете вызвать API в серверной части и использовать его ответ для заполнения полей на экране.
Вариант 1: Сервис или IntentService
Хотя Service
или IntentService
хорошо зарекомендовали себя в предыдущем случае, использовать их здесь было бы не очень хорошей идеей. Попытка получить данные из Service
или IntentService
в основной поток пользовательского интерфейса может сильно усложнить ситуацию.
Вариант 2: AsyncTask или загрузчики
На первый взгляд, AsyncTask
или загрузчики кажутся здесь очевидным решением. Они просты в использовании — просты и понятны.
Однако при использовании AsyncTask
или загрузчиков вы заметите, что необходимо написать шаблонный код. Более того, с этими компонентами обработка ошибок становится основной задачей. Даже при простом сетевом звонке вам необходимо знать о потенциальных исключениях, перехватывать их и действовать соответствующим образом. Это заставляет нас обернуть наш ответ в пользовательский класс, содержащий данные, с возможной информацией об ошибке, а флаг указывает, было ли действие успешным или нет.
Это довольно много работы для каждого отдельного звонка. К счастью, теперь доступно гораздо лучшее и простое решение: RxJava.
Вариант 3: RxJava
Возможно, вы слышали о RxJava, библиотеке, разработанной Netflix. Это почти волшебство в Java.
RxAndroid позволяет использовать RxJava в Android и упрощает работу с асинхронными задачами. Вы можете узнать больше о RxJava для Android здесь.
RxJava предоставляет два компонента: Observer
и Subscriber
.
Наблюдатель — это компонент, который содержит некоторое действие. Он выполняет это действие и возвращает результат в случае успеха или ошибку в случае сбоя.
Подписчик , с другой стороны, — это компонент, который может получить результат (или ошибку) от наблюдаемого, подписавшись на него.
С RxJava вы сначала создаете наблюдаемое:
Observable.create((ObservableOnSubscribe<Data>) e -> { Data data = mRestApi.getData(); e.onNext(data); })
После создания наблюдаемого объекта вы можете подписаться на него.
С библиотекой RxAndroid вы можете управлять потоком, в котором вы хотите выполнить действие в наблюдаемом объекте, и потоком, в котором вы хотите получить ответ (то есть результат или ошибку).
Вы связываете наблюдаемое с этими двумя функциями:
.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()
Планировщики — это компоненты, которые выполняют действие в определенном потоке. AndroidSchedulers.mainThread()
— это планировщик, связанный с основным потоком.
Учитывая, что наш вызов API — это mRestApi.getData()
и он возвращает объект Data
, базовый вызов может выглядеть следующим образом:
Observable.create((ObservableOnSubscribe<Data>) e -> { try { Data data = mRestApi.getData(); e.onNext(data); } catch (Exception ex) { e.onError(ex); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(match -> Log.i(“rest api, "success"), throwable -> Log.e(“rest api, "error: %s" + throwable.getMessage()));
Даже не вдаваясь в другие преимущества использования RxJava, вы уже видите, как RxJava позволяет нам писать более зрелый код, абстрагируясь от сложности многопоточности.
Вариант использования № 3: объединение сетевых вызовов в цепочку
Для сетевых вызовов, которые должны выполняться последовательно (т. е. когда каждая операция зависит от ответа/результата предыдущей операции), необходимо быть особенно осторожным при создании спагетти-кода.
Например, может потребоваться выполнить вызов API с токеном, который необходимо сначала получить с помощью другого вызова API.
Вариант 1: AsyncTask или загрузчики
Использование AsyncTask
или загрузчиков почти наверняка приведет к спагетти-коду. Всю функциональность будет сложно реализовать правильно, и для всего проекта потребуется огромное количество избыточного шаблонного кода.
Вариант 2: RxJava с использованием flatMap
В flatMap
оператор flatMap берет испускаемое значение из исходного наблюдаемого объекта и возвращает другой наблюдаемый объект. Вы можете создать наблюдаемое, а затем создать другое наблюдаемое, используя испускаемое значение из первого, что в основном свяжет их.
Шаг 1. Создайте наблюдаемую, которая извлекает токен:
public Observable<String> getTokenObservable() { return Observable.create(subscriber -> { try { String token = mRestApi.getToken(); subscriber.onNext(token); } catch (IOException e) { subscriber.onError(e); } }); }
Шаг 2. Создайте наблюдаемую, которая получает данные с помощью токена:
public Observable<String> getDataObservable(String token) { return Observable.create(subscriber -> { try { Data data = mRestApi.getData(token); subscriber.onNext(data); } catch (IOException e) { subscriber.onError(e); } }); }
Шаг 3. Соедините две наблюдаемые вместе и подпишитесь:
getTokenObservable() .flatMap(new Function<String, Observable<Data>>() { @Override public Observable<Data> apply(String token) throws Exception { return getDataObservable(token); } }) .subscribe(data -> { doSomethingWithData(data) }, error -> handleError(e));
Обратите внимание, что использование этого подхода не ограничивается сетевыми вызовами; он может работать с любым набором действий, которые необходимо выполнять последовательно, но в отдельных потоках.
Все вышеперечисленные варианты использования довольно просты. Переключение между потоками происходило только после того, как каждый завершил свою задачу. Более сложные сценарии, например, когда два или более потока должны активно взаимодействовать друг с другом, также могут поддерживаться этим подходом.
Вариант использования № 4: связь с потоком пользовательского интерфейса из другого потока
Рассмотрим сценарий, в котором вы хотели бы загрузить файл и обновить пользовательский интерфейс после его завершения.
Поскольку загрузка файла может занять много времени, нет необходимости заставлять пользователя ждать. Вы можете использовать службу и, возможно, IntentService
для реализации этой функциональности здесь.
Однако в этом случае более сложной задачей является возможность вызова метода в потоке пользовательского интерфейса после завершения загрузки файла (которая выполнялась в отдельном потоке).
Вариант 1: RxJava внутри сервиса
RxJava, как сама по себе, так и внутри IntentService
, может быть не идеальной. Вам нужно будет использовать механизм обратного вызова при подписке на Observable
, а IntentService
создан для выполнения простых синхронных вызовов, а не обратных вызовов.

С другой стороны, с помощью Service
вам нужно будет вручную остановить службу, что требует дополнительной работы.
Вариант 2: широковещательный приемник
Android предоставляет этот компонент, который может прослушивать глобальные события (например, события батареи, сетевые события и т. д.), а также пользовательские события. Вы можете использовать этот компонент для создания пользовательского события, которое запускается после завершения загрузки.
Для этого вам нужно создать собственный класс, расширяющий BroadcastReceiver
, зарегистрировать его в манифесте и использовать Intent
и IntentFilter
для создания пользовательского события. Для запуска события вам понадобится метод sendBroadcast
.
Манифест:
<receiver android:name="UploadReceiver"> <intent-filter> <action android:name="com.example.upload"> </action> </intent-filter> </receiver>
Получатель:
public class UploadReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getBoolean(“success”, false) { Activity activity = (Activity)context; activity.updateUI(); } }
Отправитель:
Intent intent = new Intent(); intent.setAction("com.example.upload"); sendBroadcast(intent);
Такой подход является жизнеспособным вариантом. Но, как вы заметили, это требует некоторой работы, а слишком большое количество трансляций может замедлить работу.
Вариант 3: Использование обработчика
Handler
— это компонент, который можно присоединить к потоку, а затем заставить выполнять какое-либо действие в этом потоке с помощью простых сообщений или задач Runnable
. Он работает совместно с другим компонентом, Looper
, отвечающим за обработку сообщений в конкретном потоке.
Когда Handler
создан, он может получить объект Looper
в конструкторе, который указывает, к какому потоку привязан обработчик. Если вы хотите использовать обработчик, прикрепленный к основному потоку, вам нужно использовать петлитель, связанный с основным потоком, вызвав Looper.getMainLooper()
.
В этом случае, чтобы обновить пользовательский интерфейс из фонового потока, вы можете создать обработчик, прикрепленный к потоку пользовательского интерфейса, а затем опубликовать действие как Runnable
:
Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { // update the ui from here } });
Этот подход намного лучше первого, но есть еще более простой способ сделать это…
Вариант 3: Использование EventBus
EventBus
, популярная библиотека GreenRobot, позволяет компонентам безопасно взаимодействовать друг с другом. Поскольку в нашем случае использования мы хотим обновить только пользовательский интерфейс, это может быть самым простым и безопасным выбором.
Шаг 1. Создайте класс событий. например, UIEvent
.
Шаг 2. Подпишитесь на мероприятие.
@Subscribe(threadMode = ThreadMode.MAIN) public void onUIEvent(UIEvent event) {/* Do something */}; register and unregister eventbus : @Override public void onStart() { super.onStart(); EventBus.getDefault().register(this); } @Override public void onStop() { super.onStop(); EventBus.getDefault().unregister(this); }
Шаг 3. Опубликуйте событие: EventBus.getDefault().post(new UIEvent());
С помощью параметра ThreadMode
в аннотации вы указываете поток, на который вы хотели бы подписаться на это событие. В нашем примере здесь мы выбираем основной поток, так как мы хотим, чтобы получатель события мог обновлять пользовательский интерфейс.
Вы можете структурировать свой класс UIEvent
, чтобы он содержал дополнительную информацию по мере необходимости.
В сервисе:
class UploadFileService extends IntentService { // … Boolean success = uploadFile(File file); EventBus.getDefault().post(new UIEvent(success)); // ... }
В действии/фрагменте:
@Subscribe(threadMode = ThreadMode.MAIN) public void onUIEvent(UIEvent event) {//show message according to the action success};
С EventBus library
общение между потоками становится намного проще.
Вариант использования № 5: Двусторонняя связь между потоками на основе действий пользователя
Предположим, вы создаете медиаплеер и хотите, чтобы он мог продолжать воспроизводить музыку, даже когда экран приложения закрыт. В этом сценарии вам нужно, чтобы пользовательский интерфейс мог взаимодействовать с медиапотоком (например, воспроизведение, пауза и другие действия), а также чтобы медиапоток обновлял пользовательский интерфейс на основе определенных событий (например, ошибки, состояния буферизации). , так далее).
Полный пример медиаплеера выходит за рамки этой статьи. Однако вы можете найти хорошие уроки здесь и здесь.
Вариант 1: Использование EventBus
Вы можете использовать EventBus
здесь. Однако, как правило, небезопасно публиковать событие из потока пользовательского интерфейса и получать его в службе. Это связано с тем, что у вас нет возможности узнать, запущена ли служба, когда вы отправили сообщение.
Вариант 2: Использование BoundService
BoundService
— это Service
, привязанная к активности/фрагменту. Это означает, что активность/фрагмент всегда знает, запущена служба или нет, и, кроме того, получает доступ к публичным методам службы.
Для его реализации необходимо создать собственный Binder
внутри службы и создать метод, возвращающий службу.
public class MediaService extends Service { private final IBinder mBinder = new MediaBinder(); public class MediaBinder extends Binder { MediaService getService() { // Return this instance of LocalService so clients can call public methods return MediaService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } }
Чтобы привязать действие к службе, вам нужно реализовать ServiceConnection
, который является классом, отслеживающим состояние службы, и использовать метод bindService
для выполнения привязки:
// in the activity MediaService mService; // flag indicates the bound status boolean mBound; @Override protected void onStart() { super.onStart(); // Bind to LocalService Intent intent = new Intent(this, MediaService.class); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { MediaBinder binder = (MediaBinder) service; mService = binder.getService(); mBound = true; } @Override public void onServiceDisconnected(ComponentName arg0) { mBound = false; } };
Вы можете найти полный пример реализации здесь.
Для связи со службой, когда пользователь нажимает кнопку «Воспроизведение» или «Пауза», вы можете выполнить привязку к службе, а затем вызвать соответствующий общедоступный метод службы.
Когда есть медиа-событие, и вы хотите сообщить об этом активности/фрагменту, вы можете использовать один из более ранних методов (например, BroadcastReceiver
, Handler
или EventBus
).
Вариант использования № 6: параллельное выполнение действий и получение результатов
Допустим, вы создаете туристическое приложение и хотите показать достопримечательности на карте, полученной из нескольких источников (разных поставщиков данных). Поскольку не все источники могут быть надежными, вы можете проигнорировать те из них, в которых произошел сбой, и в любом случае продолжить рендеринг карты.
Чтобы распараллелить процесс, каждый вызов API должен выполняться в отдельном потоке.
Вариант 1: Использование RxJava
В RxJava вы можете объединить несколько наблюдаемых объектов в один, используя операторы merge()
или concat()
. Затем вы можете подписаться на «объединенные» наблюдаемые и дождаться всех результатов.
Однако этот подход не будет работать должным образом. Если один вызов API завершится неудачно, объединенная наблюдаемая сообщит об общей ошибке.
Вариант 2. Использование собственных компонентов Java
ExecutorService
в Java создает фиксированное (настраиваемое) количество потоков и выполняет в них задачи одновременно. Служба возвращает объект Future
, который в конечном итоге возвращает все результаты с помощью invokeAll()
.
Каждая задача, которую вы отправляете в ExecutorService
, должна содержаться в интерфейсе Callable
, который является интерфейсом для создания задачи, которая может вызвать исключение.
Как только вы получите результаты от invokeAll()
, вы можете проверить каждый результат и действовать соответствующим образом.
Предположим, например, что у вас есть три типа привлечения, поступающие с трех разных конечных точек, и вы хотите сделать три параллельных вызова:
ExecutorService pool = Executors.newFixedThreadPool(3); List<Callable<Object>> tasks = new ArrayList<>(); tasks.add(new Callable<Object>() { @Override public Integer call() throws Exception { return mRest.getAttractionType1(); } }); // ... try { List<Future<Object>> results = pool.invokeAll(tasks); for (Future result : results) { try { Object response = result.get(); if (response instance of AttractionType1... {} if (response instance of AttractionType2... {} ... } catch (ExecutionException e) { e.printStackTrace(); } } } catch (InterruptedException e) { e.printStackTrace(); }
Таким образом, вы выполняете все действия параллельно. Таким образом, вы можете проверять наличие ошибок в каждом действии отдельно и при необходимости игнорировать отдельные сбои.
Этот подход проще, чем использование RxJava. Это проще, короче и не приводит к сбою всех действий из-за одного исключения.
Пример использования № 7: Запрос к локальной базе данных SQLite
При работе с локальной базой данных SQLite рекомендуется использовать базу данных из фонового потока, поскольку вызовы базы данных (особенно с большими базами данных или сложными запросами) могут занимать много времени, что приводит к зависанию пользовательского интерфейса.
При запросе данных SQLite вы получаете объект Cursor
, который затем можно использовать для получения фактических данных.
Cursor cursor = getData(); String name = cursor.getString(<colum_number>);
Вариант 1: Использование RxJava
Вы можете использовать RxJava и получать данные из базы данных так же, как мы получаем данные из нашего бэкэнда:
public Observable<Cursor> getLocalDataObservable() { return Observable.create(subscriber -> { Cursor cursor = mDbHandler.getData(); subscriber.onNext(cursor); }); }
Вы можете использовать наблюдаемую, возвращенную getLocalDataObservable()
, следующим образом:
getLocalDataObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(cursor -> String name = cursor.getString(0), throwable -> Log.e(“db, "error: %s" + throwable.getMessage()));
Хотя это, безусловно, хороший подход, есть и еще лучший, поскольку есть компонент, созданный именно для этого сценария.
Вариант 2: Использование CursorLoader + ContentProvider
Android предоставляет CursorLoader
, собственный компонент для загрузки данных SQLite и управления соответствующим потоком. Это Loader
, который возвращает Cursor
, который мы можем использовать для получения данных, вызывая простые методы, такие как getString()
, getLong()
и т. д.
public class SimpleCursorLoader extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor> { public static final String TAG = SimpleCursorLoader.class.getSimpleName(); private static final int LOADER_ID = 0x01; private TextView textView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_cursor_loader); textView = (TextView) findViewById(R.id.text_view); getSupportLoaderManager().initLoader(LOADER_ID, null, this); } public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { return new CursorLoader(this, Uri.parse("content://com.github.browep.cursorloader.data") , new String[]{"col1"}, null, null, null); } public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { if (cursor != null && cursor.moveToFirst()) { String text = textView.getText().toString(); while (cursor.moveToNext()) { text += "<br />" + cursor.getString(1); cursor.moveToNext(); } textView.setText(Html.fromHtml(text) ); } } public void onLoaderReset(Loader<Cursor> cursorLoader) { } }
CursorLoader
работает с компонентом ContentProvider
. Этот компонент предоставляет множество функций базы данных в режиме реального времени (например, уведомления об изменениях, триггеры и т. д.), которые позволяют разработчикам гораздо проще реализовать лучший пользовательский интерфейс.
Нет универсального решения для многопоточности в Android
Android предоставляет множество способов обработки потоков и управления ими, но ни один из них не является панацеей.
Выбор правильного подхода к многопоточности, в зависимости от вашего варианта использования, может иметь решающее значение для упрощения реализации и понимания общего решения. Нативные компоненты подходят для некоторых случаев, но не для всех. То же самое относится и к модным сторонним решениям.
Я надеюсь, что вы найдете эту статью полезной при работе над вашим следующим проектом Android. Поделитесь с нами своим опытом работы с потоками в Android или любым случаем использования, в котором вышеуказанные решения работают хорошо или не работают, если на то пошло, в комментариях ниже.