А теперь давайте посмотрим на то как происходит не блокирование потока и отсутствие колбэков «под капотом». Если честно, то колбэки все же есть=) Не все так просто, но и не сложно! Корутины используют специальный механизм Continuation.
Continuation можно рассматривать как колбэк. Continuation — похож на огромную стейт-машину, где на каждый кусок Корутин-кода(Suspend-функции) есть свой Switch-Case.
Давайте смотреть код, который мы уже использовали:
launch {
val url = "http://myurl.com/file.pdf"
// long synchronous function
suspendedGetFile(url)
println("File is downloaded")
}
Давайте посмотрим на Java класс, который получится в результате преобразования Kotlin кода корутины.
class GeneratedContinuationClass extends SuspendLambda {
int label;
String url;
void invokeSuspend() {
switch (label) {
case 0: { // шаг 1-ый
url = buildUrl();
label = 1;
suspendedGetFile(url, this); // Обратите внимание на "this". Его не было в Kotlin версии
return;
} // <= Примерно тут происходит водораздел нашего кода. До старта suspend-функции и после
case 1: { // шаг 2-ой
println("File is downloaded");
return;
}
}
}
}
Заметьте, что наша корутина трансформировалась в отдельный класс. Обратите внимание на метод invokeSuspend(), переменную label и на то как она используется в switch-е. Как вы успели заметить, основной код нашей Корутины(buildUrl(),suspendedGetFile(), println()) находится в методе invokeSuspend(). Он вызывается при запуске Корутины.
Continuation нужен для того чтобы код расположенный после suspendedGetFile() — а это println() — был исполнен только после завершения supendedGetFile(). Для этого код делится switch-ем на две части и добавляется переменная label. Точка разделения кода на две части — это suspend-функция.
Основная задача Continuation — сделать так, чтобы код, расположенный после вызова функции download(), был выполнен только когда метод download() завершил работу. Для этого код делится switch-ем на две части. И добавляется переменная label. Точка разделения кода на две части — это suspend-функция. От значения переменной label зависит, какая из двух частей будет выполнена при вызове invokeSuspend(). В нашем примере invokeSuspend() будет вызван 2 раза. Ключевые слова «case» в данном случае стоит понимать как «шаги»
Итак, первый раз invokeSuspend() будет вызван при старте Корутины. Он выполнит блок кода в case0: поменяет значение label на 1 и запустит suspend-функцию. Второй раз invokeSuspend() будет вызван уже из нашей suspend-функции suspendedGetFile(), когда она завершит свою работу. Как так?=) Suspend-функция при преобразовании в Java получает дополнительный входной параметр с типом Continuation. Т.е. в suspend-функцию suspendedGetFile() мы передаем инстанс текущего Continuation-а — this.
Когда suspend-функция suspendedGetFile() закончит загрузку файла, она возьмет Continuation, который ей передали и вызовет его invokeSuspend() метод. label был установлен в 1 (еще до запуска suspend функции), поэтому switch пойдет во вторую ветку и метод toast будет выполнен.
Таким образом Continuation является колбэком для suspend-функции. Если в корутине есть несколько suspend-функций, то Continuation будет колбэком для всех них.
Возврат значения suspend-функцией
Suspend-функция может возвращать какое-либо значение, как результат своей работы. Давайте посмотрим, как эта ситуация обрабатывается в Continuation.
Немного обновим код:
launch {
val url = "http://myurl.com/file.pdf"
// long synchronous function
val file = suspendedGetFile(url)
println("File is downloaded")
val size = suspendUnzip(file)
println("Size is $size")
}
Мы добавили еще одну suspend-функцию suspendUnzip(). При трансформации кода в Java, код будет разделен на три части:
1) suspendedGetFile() и все, что перед ней
2) suspendUnzip() и все, что между ней и suspendedGetFile()
3) все, что после suspendUnzip()
Точки разделения — это suspend-функции.
File file;
Long size;
void invokeSuspend(Object result) {
switch (label) {
case 0: {
url = buildUrl();
label = 1;
suspendedGetFile(url, this);
return;
}
case 1: {
file = (File) result;
println("File is downloaded");
label = 2;
suspendUnzip(file, this);
return;
}
case 2: {
size = (Long) result
toast("File is unzipped, size = " + size);
return;
}
}
}
Как мы ранее уже обсудили, suspend-функция по завершении своей работы вызывает метод invokeSuspend(). И именно сюда же она и передает результат своей работы. Для этого у invokeSuspend() есть входной параметр типа Object.
Suspend-функция suspendedGetFile() при вызове invokeSuspend() передаст файл (File). А в ветке case 1 будет выполнено приведение типа Object к типу File.
Suspend-функция suspendUnzip() при вызове invokeSuspend() передаст размер (Long). А в ветке case 2 будет выполнено приведение типа Object к типу Long.