Многозадачность в Android. Часть 2 - 7 шаблонов использования многопоточности в Android
Случай 1. Выполнение запроса по сети, без необходимости ответа от сервера
Иногда вы можете отправить запрос API на сервер, не беспокоясь о его ответе. Например, вы можете отправлять токен устройства для пуш-уведомлений на ваш сервер.
Т.к. это включает в себя создание запроса по сети, вы должны сделать это из потока, отличного от основного.
Вариант 1. AsyncTask или загрузчики
Вы можете использовать AsyncTask
или загрузчики для выполнения вызова, и это будет работать.
Однако AsyncTask
и загрузчики зависят от жизненного цикла активности. Это означает, что вам нужно будет либо дождаться завершения вызова, либо попытаться помешать пользователю покинуть активность или надеяться, что он будет выполнен до того, как активность будет уничтожена.
Вариант 2. Service
Service
может лучше подходить для этого варианта использования, поскольку он не привязан к какой-либо активности. Поэтому он сможет продолжить сетевой вызов даже после уничтожения активности. Плюс, так как ответ с сервера не нужен, сервис здесь тоже не ограничивается.
Однако, поскольку сервис начнет работать в потоке пользовательского интерфейса, вам все равно нужно будет управлять потоками. Вам также необходимо убедиться, что сервис остановлен после завершения сетевого вызова.
Это потребует больше усилий, чем это необходимо для такого простого действия.
Вариант 3. IntentService
Это, на мой взгляд, было бы лучшим вариантом.
Поскольку IntentService
не привязан к какой-либо активности и работает на потоке, отличном от UI, он отлично удовлетворяет нашим потребностям. Кроме того, IntentService
автоматически останавливается, поэтому нет необходимости вручную управлять им.
Случай 2. Выполнение сетевого вызова и получение ответа от сервера
Этот вариант использования, вероятно, более распространен. Например, вы можете вызвать API в фоновом режиме и использовать его ответ для заполнения полей на экране.
Вариант 1. Service или 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
В RxJava оператор 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. Общение c UI потоком из другого потока
Рассмотрим сценарий, в котором вы хотите загрузить файл и обновить пользовательский интерфейс после его завершения.
Поскольку загрузка файла может занять много времени, нет необходимости держать пользователя в ожидании. Вы можете использовать сервис и, возможно, IntentService
для реализации функциональности в нем.
Однако, в этом случае, более сложная задача заключается в возможности вызвать метод в потоке пользовательского интерфейса после завершения загрузки файла (который был выполнен в отдельном потоке).
Вариант 1. RxJava внутри сервиса
RxJava, как самостоятельно, так и внутри IntentService
, может быть не идеальным решением. Вам нужно будет использовать механизм обратного вызова при подписке на Observable
, а IntentService
построен для выполнения простых синхронных, а не обратных вызовов.
С другой стороны, с помощью Service
вам необходимо будет вручную остановить службу, что потребует дополнительной работы.
Вариант 2. BroadcastReceiver
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
Handler
- это компонент, который может быть присоединен к потоку, а затем быть использованным для выполнения некоторых действий в этом потоке с помощью простых сообщений или Runnable задач. Он работает совместно с другим компонентом - Looper
, который отвечает за обработку сообщений в конкретном потоке.
Когда Handler
создается, он может получить объект Looper
в конструкторе, который указывает, к какому потоку он прикреплен. Если вы хотите использовать Handler, прикрепленный к основному потоку, вам нужно использовать looper, связанный с основным потоком, вызывая Looper.getMainLooper()
.
В этом случае для обновления пользовательского интерфейса из фонового потока вы можете создать Handler, подключенный к потоку пользовательского интерфейса, а затем опубликовать действие как Runnable
:
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// обновить UI отсюда
}
});
Этот подход намного лучше, чем первый, но есть еще более простой способ сделать это…
Вариант 3. Использование EventBus
EventBus
- это популярная библиотека от GreenRobot, позволяет компонентам безопасно связываться друг с другом. Поскольку наш вариант использования - это тот, где мы хотим обновить интерфейс, это может быть самым простым и безопасным способом.
Шаг 1. Создайте класс событий. Например, UIEvent
.
Шаг 2. Подпишитесь на событие.
@Subscribe(threadMode = ThreadMode.MAIN)
public void onUIEvent(UIEvent event) {/* делаем что-то */};
// регистрация и разрегистрация 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 (новый 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) {
//показать сообщение в соответствии с успешным действием
}
Используя библиотеку EventBus
, связь между потоками становится намного проще.
Случай 5. Двусторонняя связь между потоками на основе действий пользователя
Предположим, вы создаете медиаплеер и хотите, чтобы он продолжал воспроизводить музыку, даже когда экран приложения закрыт. В этом случае вы хотите, чтобы пользовательский интерфейс мог взаимодействовать с медиа-потоком (например, играть, приостанавливать и другие действия), а также требовать, чтобы медиа-поток обновлял пользовательский интерфейс на основе определенных событий (например, ошибка, состояние буферизации, и т.д).
Полный пример медиаплеера выходит за рамки этой статьи. Однако вы можете найти хорошие материалы здесь и здесь.
Вариант 1. Использование EventBus
Здесь вы можете использовать EventBus
. Однако, как правило, не безопасно отправлять событие из потока пользовательского интерфейса и получать его в сервисе. Это связано с тем, что вы не можете узнать, работает ли сервис, когда вы отправили сообщение.
Вариант 2. Использование BoundService
BoundService
- это служба, связанная с активностью / фрагментом. Это означает, что активность / фрагмент всегда знает, работает служба или нет и получает доступ к публичным методам службы.
Чтобы реализовать его, вам необходимо создать настраиваемый Binder
внутри службы и создать метод, который возвращает службу.
public class MediaService extends Service {
private final IBinder mBinder = new MediaBinder();
public class MediaBinder extends Binder {
MediaService getService() {
// Верните экземпляр LocalService, чтобы клиенты могли вызвать публичные методы
return MediaService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
Чтобы связать активность со службой, вам необходимо реализовать ServiceConnection
, который является классом, контролирующим статус службы, и использовать метод bindService
для привязки:
// в активности
MediaService mService;
// флаг указывает статус привязки
boolean mBound;
@Override
protected void onStart() {
super.onStart();
// привязать к 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()
. Затем вы можете подписаться на «merged» наблюдатели и ждать всех результатов.
Но, этот подход не будет работать должным образом. Если один вызов к API завершится неудачно, объединенный (merged) наблюдатель сообщит об общем сбое.
Вариант 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 и получать данные из базы данных, так же, как мы получаем данные из нашего back-end:
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, перевод