Все приложения Android используют основной поток для обработки операций пользовательского интерфейса. Вызов длительных операций из основного потока может привести к зависаниям и отсутствию ответа. Например, если приложение отправляет сетевой запрос из основного потока, его интерфейс зависает до получения ответа. Если вы используете Java, то можете создать дополнительные фоновые потоки для обработки продолжительных операций, пока основной поток продолжает обрабатывать обновления интерфейса.
В этом руководстве рассказывается, как разработчики, использующие язык программирования Java, могут применять пул потоков для настройки и использования нескольких потоков в приложении Android. В этом руководстве также показано, как определить код для запуска в потоке и как организовать связь между одним из этих потоков и основным потоком.
Библиотеки для параллельного программирования
Важно понимать основы работы с цепочками и механизмы, лежащие в их основе. Однако существует множество популярных библиотек, которые предлагают более высокоуровневые абстракции над этими концепциями и готовые к использованию утилиты для передачи данных между потоками. К ним относятся Guava и RxJava для пользователей языка программирования Java, а также Coroutines, которые мы рекомендуем для пользователей Kotlin.
На практике вы должны выбрать тот, который лучше всего подходит для вашего приложения и вашей команды разработчиков, хотя правила работы с потоками остаются прежними.
Обзор примеров
В соответствии с Руководством по архитектуре приложений в примерах из этой статьи выполняется сетевой запрос, а результат возвращается в основной поток, где приложение может показать его на экране.
В частности, ViewModel вызывает уровень данных в основном потоке, чтобы инициировать сетевой запрос. Слой данных отвечает за перенос выполнения сетевого запроса из основного потока и отправку результата обратно в основной поток с помощью обратного вызова.
Чтобы перенести выполнение сетевого запроса из основного потока, нам нужно создать другие потоки в нашем приложении.
Создать несколько цепочек
Пул потоков – это управляемый набор потоков, которые параллельно выполняют задачи из очереди. Новые задачи выполняются в существующих потоках, когда они становятся свободными. Чтобы отправить задачу в пул потоков, используйте интерфейс ExecutorService. Обратите внимание, что ExecutorService не имеет ничего общего с сервисами, компонентом приложения для Android.
Создание потоков – ресурсоемкий процесс, поэтому пул потоков следует создавать только один раз при инициализации приложения. Сохраните экземпляр ExecutorService в классе Application или в контейнере внедрения зависимостей.
В следующем примере создается пул из четырех потоков, которые можно использовать для выполнения фоновых задач.
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
Пул потоков можно настроить и другими способами в зависимости от ожидаемой рабочей нагрузки. Подробнее о настройке пула потоков…
Выполнение в фоновом потоке
Сетевой запрос в основном потоке заставляет его ждать ответа, то есть блокирует его. Поскольку поток заблокирован, ОС не может вызвать onDraw(), и приложение зависает, что может привести к появлению диалогового окна "Приложение не отвечает". Вместо этого давайте запустим эту операцию в фоновом потоке.
Отправка запроса
Сначала давайте посмотрим на наш класс LoginRepository и узнаем, как он делает сетевой запрос:
// Result.java
public abstract class Result<T> {
private Result() {}
public static final class Success<T> extends Result<T> {
public T data;
public Success(T data) {
this.data = data;
}
}
public static final class Error<T> extends Result<T> {
public Exception exception;
public Error(Exception exception) {
this.exception = exception;
}
}
}
// LoginRepository.java
public class LoginRepository {
private final String loginUrl = "https://example.com/login";
private final LoginResponseParser responseParser;
public LoginRepository(LoginResponseParser responseParser) {
this.responseParser = responseParser;
}
public Result<LoginResponse> makeLoginRequest(String jsonBody) {
try {
URL url = new URL(loginUrl);
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestMethod("POST");
httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
httpConnection.setRequestProperty("Accept", "application/json");
httpConnection.setDoOutput(true);
httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));
LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
return new Result.Success<LoginResponse>(loginResponse);
} catch (Exception e) {
return new Result.Error<LoginResponse>(e);
}
}
}
makeLoginRequest() является синхронным и блокирует вызывающий поток. Для моделирования ответа на сетевой запрос у нас есть собственный класс Result.
Активируйте запрос.
ViewModel активирует сетевой запрос, когда пользователь нажимает, например, на кнопку:
public class LoginViewModel {
private final LoginRepository loginRepository;
public LoginViewModel(LoginRepository loginRepository) {
this.loginRepository = loginRepository;
}
public void makeLoginRequest(String username, String token) {
String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
loginRepository.makeLoginRequest(jsonBody);
}
}
В предыдущем примере кода LoginViewModel блокирует основной поток при выполнении сетевого запроса. Мы можем использовать пул потоков, экземпляр которого мы создали, чтобы перенести выполнение в фоновый поток.
Обработка внедрения зависимостей
Во-первых, следуя принципам внедрения зависимостей, LoginRepository принимает экземпляр Executor, а не ExecutorService, поскольку он выполняет код, а не управляет потоками:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
Метод execute() объекта Executor принимает объект Runnable. Runnable – это интерфейс Single Abstract Method (SAM) с методом run(), который выполняется в потоке при вызове.
Выполнять в фоновом режиме
Давайте создадим ещё одну функцию под названием makeLoginRequest(), которая переносит выполнение в фоновый поток и пока игнорирует ответ:
public class LoginRepository {
...
public void makeLoginRequest(final String jsonBody) {
executor.execute(new Runnable() {
@Override
public void run() {
Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
}
});
}
public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
... // HttpURLConnection logic
}
...
}
Внутри метода execute() мы создаем новый объект Runnable с блоком кода, который хотим выполнить в фоновом потоке. В нашем случае это метод синхронного сетевого запроса. Внутренний компонент ExecutorService управляет объектом Runnable и выполняет его в доступном потоке.
Что нужно учесть
Любой поток в приложении может выполняться параллельно с другими потоками, включая основной, поэтому необходимо убедиться, что ваш код потокобезопасен. Обратите внимание, что в нашем примере мы избегаем записи в переменные, общие для потоков, вместо этого передавая неизменяемые данные. Это хорошая практика, потому что каждый поток работает со своим собственным экземпляром данных, и мы избегаем сложностей синхронизации.
Если вам нужно передавать состояние между потоками, необходимо тщательно управлять доступом из потоков с помощью механизмов синхронизации, таких как блокировки. Это выходит за рамки данного руководства. В целом, по возможности следует избегать совместного использования изменяемого состояния между потоками.
Обмен данными с основным потоком
На предыдущем шаге мы проигнорировали ответ на сетевой запрос. Чтобы показать результат на экране, LoginViewModel должна знать о нем. Для этого можно использовать обратные вызовы.
Функция makeLoginRequest() должна принимать обратный вызов в качестве параметра, чтобы возвращать значение асинхронно. Обратный вызов с результатом выполняется, когда сетевой запрос завершается или происходит сбой. В Kotlin мы можем
использовать функцию высшего порядка. Однако в Java для получения той же функциональности
нам нужно создать новый интерфейс обратного вызова:
interface RepositoryCallback<T> {
void onComplete(Result<T> result);
}
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
callback.onComplete(result);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
callback.onComplete(errorResult);
}
}
});
}
...
}
Теперь ViewModel должен реализовать обратный вызов. Он может выполнять разные
логические действия в зависимости от результата:
public class LoginViewModel {
...
public void makeLoginRequest(String username, String token) {
String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
@Override
public void onComplete(Result<LoginResponse> result) {
if (result instanceof Result.Success) {
// Happy path
} else {
// Show error in UI
}
}
});
}
}
В этом примере обратный вызов выполняется в вызывающем потоке, который является фоновым. Это означает, что вы не можете изменять уровень пользовательского интерфейса или напрямую взаимодействовать с ним, пока не переключитесь обратно на основной поток.
Как использовать обработчики
Вы можете использовать Handler, чтобы поставить в очередь действие, которое будет выполняться в другом потоке. Чтобы указать поток, в котором должно выполняться действие, создайте Handler, используя Looper для потока. Looper – это объект, который запускает цикл обработки сообщений для связанного потока. Создав объект Handler, вы можете использовать метод post(Runnable) для выполнения блока кода в соответствующем потоке.
В классе Looper есть вспомогательная функция getMainLooper(), которая получает объект Looper основного потока. Вы можете запустить код в основном потоке, используя Looper для создания Handler. Поскольку это действие может выполняться довольно часто, вы также можете сохранить экземпляр Handler в том же месте, где вы сохранили ExecutorService:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
Рекомендуется внедрить обработчик в репозиторий, так как это дает вам больше гибкости. Например, в будущем вы можете передать другой Handler, чтобы запланировать задачи в отдельном потоке. Если вы всегда общаетесь с одним и тем же потоком, вы можете передать Handler в конструктор репозитория, как показано в следующем примере.
public class LoginRepository {
...
private final Handler resultHandler;
public LoginRepository(LoginResponseParser responseParser, Executor executor,
Handler resultHandler) {
this.responseParser = responseParser;
this.executor = executor;
this.resultHandler = resultHandler;
}
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
...
}
Если вам нужно больше гибкости, вы можете передать в каждую функцию объект Handler:
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler,
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback, resultHandler);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback, resultHandler);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
}
В этом примере обратный вызов, переданный в вызов makeLoginRequest репозитория, выполняется в основном потоке. Это означает, что вы можете напрямую изменять пользовательский интерфейс из обратного вызова или использовать LiveData.setValue() для связи с пользовательским интерфейсом.
Как настроить пул потоков
Вы можете создать пул потоков, используя одну из вспомогательных функций Executor с предварительно заданными настройками, как показано в предыдущем примере кода. Если вы хотите настроить пул потоков, создайте экземпляр с помощью ThreadPoolExecutor. Вы можете настроить следующие параметры:
- Начальный и максимальный размер пула.
- Keep alive time и единицу измерения времени. Keep alive time – это максимальное время, в течение которого поток может бездействовать, прежде чем он будет закрыт.
- Очередь ввода, в которой содержится
Runnableзадач. Эта очередь должна реализовывать интерфейс BlockingQueue. Вы можете выбрать реализацию очереди, которая соответствует требованиям вашего приложения. Подробную информацию вы найдете в обзоре класса ThreadPoolExecutor.
Ниже приведен пример, в котором размер пула потоков определяется на основе общего количества ядер процессора, время поддержания активности составляет одну секунду и используется входная очередь.
public class MyApplication extends Application {
/*
* Gets the number of available cores
* (not always the same as the maximum number of cores)
*/
private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
// Instantiates the queue of Runnables as a LinkedBlockingQueue
private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
// Sets the amount of time an idle thread waits before terminating
private static final int KEEP_ALIVE_TIME = 1;
// Sets the Time Unit to seconds
private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
// Creates a thread pool manager
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
NUMBER_OF_CORES, // Initial pool size
NUMBER_OF_CORES, // Max pool size
KEEP_ALIVE_TIME,
KEEP_ALIVE_TIME_UNIT,
workQueue
);
...
}