Сопрограммы Kotlin позволяют вам писать чистый, упрощенный асинхронный код, который обеспечивает быстроту реагирования вашего приложения при управлении длительными задачами, такими как сетевые вызовы или дисковые операции.
В этом разделе представлен подробный обзор сопрограмм на Android. Если вы не знакомы с сопрограммами, обязательно прочитайте сопрограммы Kotlin для Android, прежде чем читать этот раздел.
Управляйте долго выполняющимися задачами
Сопрограммы основываются на обычных функциях, добавляя две операции для обработки долго выполняющихся задач. Помимо invoke
(или call
) и return
, сопрограммы добавляют suspend
и resume
:
-
suspend
приостанавливает выполнение текущей сопрограммы, сохраняя все локальные переменные. -
resume
продолжает выполнение приостановленной сопрограммы с того места, где она была приостановлена.
Вы можете вызывать функции suspend
только из других функций suspend
или с помощью построителя сопрограммы, такого как launch
для запуска новой сопрограммы.
В следующем примере показана простая реализация сопрограммы для гипотетической долго выполняющейся задачи:
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
В этом примере get()
по-прежнему выполняется в основном потоке, но приостанавливает сопрограмму перед запуском сетевого запроса. Когда сетевой запрос завершается, get
возобновляет приостановленную сопрограмму вместо использования обратного вызова для уведомления основного потока.
Котлин использует фрейм стека для управления тем, какая функция выполняется вместе с любыми локальными переменными. При приостановке сопрограммы текущий кадр стека копируется и сохраняется для последующего использования. При возобновлении кадр стека копируется обратно из того места, где он был сохранен, и функция снова запускается. Хотя код может выглядеть как обычный запрос последовательной блокировки, сопрограмма гарантирует, что сетевой запрос не блокирует основной поток.
Используйте сопрограммы для обеспечения основной безопасности
Сопрограммы Kotlin используют диспетчеры, чтобы определить, какие потоки используются для выполнения сопрограммы. Чтобы запустить код вне основного потока, вы можете указать сопрограммам Kotlin выполнять работу либо с диспетчером по умолчанию , либо с диспетчером ввода-вывода . В Kotlin все сопрограммы должны выполняться в диспетчере, даже если они выполняются в основном потоке. Сопрограммы могут приостанавливать себя, а за их возобновление отвечает диспетчер.
Чтобы указать, где должны запускаться сопрограммы, Kotlin предоставляет три диспетчера, которые вы можете использовать:
- Dispatchers.Main — используйте этот диспетчер для запуска сопрограммы в основном потоке Android. Его следует использовать только для взаимодействия с пользовательским интерфейсом и выполнения быстрой работы. Примеры включают вызов функций
suspend
, выполнение операций платформы пользовательского интерфейса Android и обновление объектовLiveData
. - Dispatchers.IO — этот диспетчер оптимизирован для выполнения дискового или сетевого ввода-вывода вне основного потока. Примеры включают использование компонента Room , чтение или запись файлов, а также выполнение любых сетевых операций.
- Dispatchers.Default — этот диспетчер оптимизирован для выполнения ресурсоемкой работы за пределами основного потока. Примеры использования включают сортировку списка и анализ JSON.
Продолжая предыдущий пример, вы можете использовать диспетчеры для переопределения функции get
. Внутри тела get
вызовите withContext(Dispatchers.IO)
чтобы создать блок, который выполняется в пуле потоков ввода-вывода. Любой код, который вы помещаете внутрь этого блока, всегда выполняется через диспетчер IO
. Поскольку withContext
сама по себе является функцией приостановки, функция get
также является функцией приостановки.
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
С помощью сопрограмм вы можете отправлять потоки с детальным контролем. Поскольку withContext()
позволяет вам управлять пулом потоков любой строки кода без введения обратных вызовов, вы можете применять его к очень небольшим функциям, таким как чтение из базы данных или выполнение сетевого запроса. Хорошей практикой является использование withContext()
чтобы убедиться, что каждая функция безопасна для основного потока , что означает, что вы можете вызвать функцию из основного потока. Таким образом, вызывающему абоненту никогда не придется думать о том, какой поток следует использовать для выполнения функции.
В предыдущем примере fetchDocs()
выполняется в основном потоке; однако он может безопасно вызвать get
, который выполняет сетевой запрос в фоновом режиме. Поскольку сопрограммы поддерживают suspend
и resume
, выполнение сопрограммы в основном потоке возобновляется с результатом get
, как только блок withContext
завершается.
Производительность withContext()
withContext()
не добавляет дополнительных затрат по сравнению с эквивалентной реализацией на основе обратного вызова. Более того, в некоторых ситуациях можно оптимизировать вызовы withContext()
за пределами эквивалентной реализации на основе обратного вызова. Например, если функция выполняет десять вызовов в сети, вы можете указать Kotlin переключать потоки только один раз, используя внешний withContext()
. Тогда, даже если сетевая библиотека использует withContext()
несколько раз, она остается в том же диспетчере и избегает переключения потоков. Кроме того, Kotlin оптимизирует переключение между Dispatchers.Default
и Dispatchers.IO
, чтобы по возможности избегать переключения потоков.
Запустить сопрограмму
Вы можете запустить сопрограммы одним из двух способов:
-
launch
запускает новую сопрограмму и не возвращает результат вызывающей стороне. Любую работу, которая считается «выстрелил и забыл», можно начать с помощьюlaunch
. -
async
запускает новую сопрограмму и позволяет вам вернуть результат с помощью функции приостановки, называемойawait
.
Обычно новую сопрограмму следует launch
из обычной функции, поскольку обычная функция не может вызывать await
. Используйте async
только внутри другой сопрограммы или внутри функции приостановки и выполнения параллельной декомпозиции.
Параллельная декомпозиция
Все сопрограммы, запускаемые внутри функции suspend
, должны быть остановлены, когда эта функция возвращается, поэтому вам, вероятно, необходимо гарантировать, что эти сопрограммы завершатся перед возвратом. Благодаря структурированному параллелизму в Kotlin вы можете определить coroutineScope
, который запускает одну или несколько сопрограмм. Затем, используя await()
(для одной сопрограммы) или awaitAll()
(для нескольких сопрограмм), вы можете гарантировать, что эти сопрограммы завершатся до возврата из функции.
В качестве примера давайте определим coroutineScope
, который асинхронно извлекает два документа. Вызывая await()
для каждой отложенной ссылки, мы гарантируем, что обе async
операции завершатся до возврата значения:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
Вы также можете использовать awaitAll()
для коллекций, как показано в следующем примере:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
Несмотря на то, что fetchTwoDocs()
запускает новые сопрограммы с помощью async
, функция использует awaitAll()
для ожидания завершения запущенных сопрограмм перед возвратом. Однако обратите внимание, что даже если бы мы не вызвали awaitAll()
, построитель coroutineScope
не возобновляет работу сопрограммы, вызвавшей fetchTwoDocs
, до тех пор, пока не будут завершены все новые сопрограммы.
Кроме того, coroutineScope
перехватывает любые исключения, которые выдают сопрограммы, и направляет их обратно вызывающему объекту.
Дополнительные сведения о параллельной декомпозиции см. в разделе Составление приостанавливающих функций .
Концепции сопрограмм
CoroutineScope
CoroutineScope
отслеживает любую сопрограмму, которую он создает с помощью launch
или async
. Текущую работу (т. е. запущенные сопрограммы) можно отменить, scope.cancel()
в любой момент времени. В Android некоторые библиотеки KTX предоставляют собственные CoroutineScope
для определенных классов жизненного цикла. Например, у ViewModel
есть viewModelScope
, а у Lifecycle
— lifecycleScope
. Однако, в отличие от диспетчера, CoroutineScope
не запускает сопрограммы.
viewModelScope
также используется в примерах, приведенных в разделе Фоновая обработка потоков на Android с помощью Coroutines . Однако если вам нужно создать собственный CoroutineScope
для управления жизненным циклом сопрограмм на определенном уровне вашего приложения, вы можете создать его следующим образом:
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
Отмененная область не может создавать больше сопрограмм. Поэтому вам следует вызыватьscope.cancel scope.cancel()
только тогда, когда уничтожается класс, управляющий его жизненным циклом. При использовании viewModelScope
класс ViewModel
автоматически отменяет область видимости в методе onCleared()
ViewModel.
Работа
Job
— это дескриптор сопрограммы. Каждая сопрограмма, которую вы создаете с помощью launch
или async
возвращает экземпляр Job
, который однозначно идентифицирует сопрограмму и управляет ее жизненным циклом. Вы также можете передать Job
в CoroutineScope
для дальнейшего управления его жизненным циклом, как показано в следующем примере:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
СопрограммаКонтекст
CoroutineContext
определяет поведение сопрограммы, используя следующий набор элементов:
-
Job
: Управляет жизненным циклом сопрограммы. -
CoroutineDispatcher
: Отправляет работу в соответствующий поток. -
CoroutineName
: имя сопрограммы, полезное для отладки. -
CoroutineExceptionHandler
: обрабатывает неперехваченные исключения.
Для новых сопрограмм, созданных в определенной области, новой сопрограмме назначается новый экземпляр Job
, а другие элементы CoroutineContext
наследуются из содержащейся области. Вы можете переопределить унаследованные элементы, передав новый CoroutineContext
в функцию launch
или async
функцию. Обратите внимание, что передача Job
для launch
или async
не имеет никакого эффекта, поскольку новый экземпляр Job
всегда назначается новой сопрограмме.
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
Дополнительные ресурсы сопрограмм
Дополнительные ресурсы по сопрограммам можно найти по следующим ссылкам: