El jefe de Ali P7 te enseña a descifrar la rutina de Kotlin (3) - programación de rutinas

Cracking Kotlin Coroutine (3) - Programación Coroutine

1. Contexto de la rutina

El planificador es esencialmente la implementación de un contexto de corrutina. Primero, presentemos el contexto.

Mencionamos anteriormente que launchla función tiene tres parámetros, el primer parámetro se llama contexto , y su tipo de interfaz es CoroutineContext, generalmente el tipo de contexto que vemos es CombinedContexto EmptyCoroutineContext, uno representa una combinación de contextos y el otro no representa nada. Veamos los métodos de interfaz CoroutineContextde :

@SinceKotlin("1.3")
public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...
    public fun minusKey(key: Key<*>): CoroutineContext

    public interface Key<E : Element>

    public interface Element : CoroutineContext {
        public val key: Key<*>
        ...
    }
}

No sé si te has dado cuenta, es simplemente un índice Keybasado List:

|
CoroutineContext

|

Lista

|
| — | — |
|

obtener la clave)

|

obtener (int)

|
|

más (contexto de rutina)

|

más (Lista)

|
|

minusKey(Clave)

|

removeAt(Int)

|

en la tabla List.plus(List)en realidad se refiere al método de extensiónCollection<T>.plus(elements:Iterable<T>):List<T>

CoroutineContextComo colección, sus elementos son los que se ven en el código fuente Element, cada uno Elementtiene uno key, por lo que puede aparecer como un elemento, y también es CoroutineContextuna subinterfaz de, por lo que también puede aparecer como una colección.

Hablando de esto, todos entenderán que CoroutineContextresulta ser una estructura de datos. Si está familiarizado con la definición recursiva Listde , entonces esCombinedContext fácil de entender y , por ejemplo, la de scala se define así:EmptyCoroutineContextList

sealed abstract class List[+A] extends ... {
    ...
    def head: A
    def tail: List[A]
    ...
}

Cuando el patrón coincide, List(1,2,3,4)puede x::ycoincidir , xy es 1, ypor lo que es List(2,3,4).

CombinedContextLa definición de también es muy similar:

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
    ...
}

Es solo que está invertido, el frente es una colección y la parte posterior es un solo elemento. coroutineContextLa mayor parte de lo que accedemos en el cuerpo de la rutina es de este CombinedContexttipo, lo que significa que hay muchos conjuntos de implementaciones de contexto específico.Si queremos encontrar una implementación de contexto específico, necesitamos usar el correspondiente Keypara encontrarlo, por ejemplo:

suspend fun main(){
    GlobalScope.launch {
        println(coroutineContext[Job]) // "coroutine#1":StandaloneCoroutine{Active}@1ff62014
    }
    println(coroutineContext[Job]) // null,suspend main 虽然也是协程体,但它是更底层的逻辑,因此没有 Job 实例
}

Aquí hay Joben realidad companionobjectuna referencia a su

public interface Job : CoroutineContext.Element {
    /**
     * Key for [Job] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<Job> { ... }
    ...
}

Entonces también podemos imitar Thread.currentThread()para encontrar un Jobmétodo para obtener la corriente:

suspend inline fun Job.Key.currentJob() = coroutineContext[Job]

suspend fun coroutineJob(){
    GlobalScope.launch {
        log(Job.currentJob())
    }
    log(Job.currentJob())
}

Podemos agregar algunas características a la corrutina especificando el contexto. Un buen ejemplo es agregar un nombre a la corrutina para facilitar la depuración:

GlobalScope.launch(CoroutineName("Hello")) {
    ...
}

Copiar

Si hay varios contextos para agregar, +solo :

GlobalScope.launch(Dispatchers.Main + CoroutineName("Hello")) {
    ...
}

Dispatchers.Maines una implementación del planificador, no te preocupes, lo conoceremos en breve.

2. Interceptor de rutina

Después de pasar mucho tiempo hablando sobre el contexto, aquí hay un interceptor de existencia especial.

public interface ContinuationInterceptor : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>

    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
    ...
}

Interceptor es también la dirección de implementación de un contexto. Interceptor puede controlar la ejecución de su coroutine. Al mismo tiempo, para garantizar la corrección de su función, la colección de contexto de coroutine siempre lo colocará al final. Este es realmente el elegido. .

Su método de interceptación de rutinas también es muy simple, porque la esencia de las rutinas es devolución de llamada + "magia negra", y esta devolución de llamada es Continuationinterceptada . Los amigos que han usado OkHttp se emocionarán de inmediato. A menudo uso interceptores. OkHttp usa interceptores para almacenar en caché, registrar y simular solicitudes. Lo mismo es cierto para los interceptores de rutina. El planificador se implementa en función del interceptor, en otras palabras, el planificador es una especie de interceptor.

Podemos definir un interceptor y ponerlo en nuestro contexto de rutina para ver qué sucede.

class MyContinuationInterceptor: ContinuationInterceptor{
    override val key = ContinuationInterceptor
    override fun <T> interceptContinuation(continuation: Continuation<T>) = MyContinuation(continuation)
}

class MyContinuation<T>(val continuation: Continuation<T>): Continuation<T> {
    override val context = continuation.context
    override fun resumeWith(result: Result<T>) {
        log("<MyContinuation> $result" )
        continuation.resumeWith(result)
    }
}

Acabamos de llegar a una línea de registro en la devolución de llamada. A continuación sacamos el caso de uso:

suspend fun main() {
    GlobalScope.launch(MyContinuationInterceptor()) {
        log(1)
        val job = async {
            log(2)
            delay(1000)
            log(3)
            "Hello"
        }
        log(4)
        val result = job.await()
        log("5. $result")
    }.join()
    log(6)
}

Este es probablemente el ejemplo más complicado que hemos dado hasta ahora, pero no se deje intimidar por él, sigue siendo bastante simple. launchComenzamos una corrutina a través de , especificamos nuestro propio interceptor como contexto para ella, y luego asynccomenzamos una corrutina asyncdentro de ella, que es el mismo tipo de función launchde la función, y todas se llaman la función constructora de la corrutina, la diferencia es que asyncel activado Jobes el real Deferredpuede tener un resultado de retorno, que se puede awaitobtener a través del método.

Es concebible que resultel valor de sea Hola. Entonces, ¿cuál es el resultado de ejecutar este programa?

15:31:55:989 [main] <MyContinuation> Success(kotlin.Unit)  // ①
15:31:55:992 [main] 1
15:31:56:000 [main] <MyContinuation> Success(kotlin.Unit) // ②
15:31:56:000 [main] 2
15:31:56:031 [main] 4
15:31:57:029 [kotlinx.coroutines.DefaultExecutor] <MyContinuation> Success(kotlin.Unit) // ③
15:31:57:029 [kotlinx.coroutines.DefaultExecutor] 3
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] <MyContinuation> Success(Hello) // ④
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] 5. Hello
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] 6

"// ①" no es el contenido generado por el programa, solo está marcado para facilitar las explicaciones posteriores.

Todos pueden preguntarse, ¿no dijiste Continuationque es una devolución de llamada, y solo hay una devolución de llamada ( awaitallí), por qué el registro se imprime cuatro veces?

No se asuste, le presentaremos en orden.

En primer lugar, cuando se inicien todas las corrutinas, habrá Continuation.resumeWithuna operación. Esta operación es una oportunidad de programación para el programador. Esta es la clave para que nuestras corrutinas tengan la oportunidad de programar otros subprocesos. Este es el caso tanto en ① como en ②.

En segundo lugar, delayes el punto de suspensión.Después de 1000 ms, la rutina debe programarse y ejecutarse, por lo que hay un registro en ③.

Finalmente, el registro en ④ es fácil de entender, es nuestro resultado devuelto.

Es posible que algunos amigos todavía tengan preguntas. No cambié los hilos en el interceptor. ¿Por qué hay una operación de cambio de hilo a partir de ③? La lógica de cambiar de subprocesos proviene del hecho delayde que en la JVM delay, ScheduledExcecutoren realidad se agrega una tarea retrasada en un , por lo que se producirá el cambio de subprocesos, mientras que en el entorno de JavaScript, se basa en setTimeout. Si se ejecuta en Nodejs, no lo hará delay. Corta el hilo, después de todo, las personas son de un solo hilo.

Si manejamos el cambio de subprocesos nosotros mismos en el interceptor, entonces hemos implementado nuestro propio planificador simple. Si está interesado, puede probarlo usted mismo.

Pensando: ¿Puede haber más de un interceptor?

3. Programador

3.1 Resumen

Con la base anterior, nuestra introducción al planificador se convierte en algo natural.

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    ...
    public abstract fun dispatch(context: CoroutineContext, block: Runnable)
    ...
}

Es en sí mismo una subclase del contexto coroutine y, al mismo tiempo, implementa la interfaz del interceptor, y dispatchel método se llamará interceptContinuationen , realizando así la programación de la coroutine. Entonces, si queremos implementar nuestro propio programador, podemos heredar esta clase, pero generalmente usamos las listas para usar, que se definen Dispatchersen :

val Default: CoroutineDispatcher
val Main: MainCoroutineDispatcher
val Unconfined: CoroutineDispatcher

La definición de esta clase implica el soporte de Kotlin MPP, por lo que también lo verás en la versión Jvm val IO:CoroutineDispatcher. En js y native, solo existen los tres mencionados anteriormente (soy parcial a Jvm).

|
|

jmv

|

js

|

Nativo

|
| — | — | — | — |
|

Por defecto

|

Grupo de subprocesos

|

bucle de hilo principal

|

bucle de hilo principal

|
|

Principal

|

Subproceso de interfaz de usuario

|

Igual que el predeterminado

|

Igual que el predeterminado

|
|

no confinado

|

ejecución directa

|

ejecución directa

|

ejecución directa

|
|

IO

|

Grupo de subprocesos

|

|

|

  • IO solo se define en Jvm. Se basa en el grupo de subprocesos detrás del programador predeterminado e implementa colas y límites independientes. Por lo tanto, cambiar el programador coroutine de Predeterminado a IO no activa el cambio de subprocesos.
  • Main se usa principalmente para programas relacionados con la interfaz de usuario, incluidos Swing, JavaFx y Android en Jvm, y puede enviar corrutinas a sus respectivos subprocesos de interfaz de usuario.
  • Js en sí mismo es un bucle de eventos de un solo subproceso, que es similar al programa de interfaz de usuario en Jvm.

3.2 Escribir programas relacionados con la interfaz de usuario

La gran mayoría de los usuarios de Kotlin son desarrolladores de Android, y todos tienen una demanda relativamente grande de desarrollo de UI. Tomemos un escenario muy común, haga clic en un botón para realizar algunas operaciones asincrónicas y luego vuelva a llamar para actualizar la interfaz de usuario:

getUserBtn.setOnClickListener { 
    getUser { user ->
        handler.post {
            userNameView.text = user.name
        }
    }
}

Simplemente damos la declaración de getUserla función :

typealias Callback = (User) -> Unit

fun getUser(callback: Callback){
    ...
}

Dado que getUserla función debe cambiarse a otros subprocesos para su ejecución, la devolución de llamada generalmente se llama en este subproceso que no es de la interfaz de usuario, por lo que para garantizar que la interfaz de usuario se actualice correctamente, debemos usar handler.postpara cambiar al subproceso de la interfaz de usuario. El método de escritura anterior es nuestro método de escritura más antiguo.

Luego vino RxJava, y las cosas empezaron a ponerse interesantes:

fun getUserObservable(): Observable<User> {
    return Observable.create<User> { emitter ->
        getUser {
            emitter.onNext(it)
        }
    }
}

Entonces, el evento de clic de botón se puede escribir de la siguiente manera:

getUserBtn.setOnClickListener {
    getUserObservable()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { user ->
                userNameView.text = user.name
            }
}

De hecho, el rendimiento de RxJava en el cambio de subprocesos es muy bueno, y es exactamente el mismo. ¡Mucha gente incluso lo usa solo por la comodidad del cambio de subprocesos!

Así que ahora hacemos la transición de este código al método de escritura coroutine:

suspend fun getUserCoroutine() = suspendCoroutine<User> {
    continuation ->
    getUser {
        continuation.resume(it)
    }
}

Al hacer clic en el botón, podemos:

getUserBtn.setOnClickListener {
    GlobalScope.launch(Dispatchers.Main) {
        userNameView.text = getUserCoroutine().name
    }
}

También puede usar la extensión View.onClick en anko-coroutines, por lo que no necesitamos launchiniciar . Con respecto al soporte de Anko para las rutinas, organizaremos un artículo para presentarlas más adelante.

Aquí hay algo que no has visto antes. suspendCoroutineEste método no nos ayuda a iniciar la corrutina. Se ejecuta en la corrutina y nos ayuda a obtener Continuationla instancia , es decir, obtener la devolución de llamada, que es conveniente para nosotros llamar. resumeo resumeWithExceptionpara devolver un resultado o lanzar una excepción.

Si llama repetidamente resumeo resumeWithExceptionrecibe una moneda IllegalStateException, piense por qué.

En comparación con el enfoque anterior de RxJava, encontrará que este código es realmente muy fácil de entender, e incluso encontrará que los escenarios de uso de coroutines son muy similares a RxJava. Aquí usamos Dispatchers.Mainpara asegurarnos launchde que la rutina iniciada por siempre esté programada para el subproceso de la interfaz de usuario al programar, así que echemos un vistazo a la implementación específica Dispatchers.Mainde .

En Jvm, Mainla implementación de también es más interesante:

internal object MainDispatcherLoader {
    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

    private fun loadMainDispatcher(): MainCoroutineDispatcher {
        return try {
            val factories = MainDispatcherFactory::class.java.let { clz ->
                ServiceLoader.load(clz, clz.classLoader).toList()
            }
            factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
                ?: MissingMainCoroutineDispatcher(null)
        } catch (e: Throwable) {
            MissingMainCoroutineDispatcher(e)
        }
    }
}

En Android, el marco coroutine se registra AndroidDispatcherFactorypara que Mainfinalmente se le asigne una instancia HandlerDispatcherde .Si está interesado, puede consultar la implementación del código fuente de kotlinx-coroutines-android.

Tenga en cuenta que en la implementación anterior de RxJava y coroutines, no consideramos las excepciones y cancelaciones. El tema de las excepciones y la cancelación se tratará en detalle en un artículo posterior.

3.3 Programadores vinculados a subprocesos arbitrarios

El propósito del programador es cortar hilos. No creas que lo llamaré aleatoriamente según mi estado de ánimo cuando esté dispatchen , entonces te estás haciendo daño (no tengas miedo de tus bromas, realmente escribí ese código, solo por entretenimiento). Entonces, el problema es simple, siempre que proporcionemos hilos, el programador debe crearse fácilmente:

suspend fun main() {
    val myDispatcher= Executors.newSingleThreadExecutor{ r -> Thread(r, "MyThread") }.asCoroutineDispatcher()
    GlobalScope.launch(myDispatcher) {
        log(1)
    }.join()
    log(2)
}

La información de salida indica que la rutina se está ejecutando en nuestro propio subproceso.

16:10:57:130 [MyThread] 1
16:10:57:136 [MyThread] 2

Pero tenga en cuenta que, dado que este grupo de subprocesos lo creamos nosotros mismos, debemos cerrarlo en el momento adecuado; de lo contrario:

Podemos cerrar activamente el grupo de subprocesos o llamar:

myDispatcher.close()

Para finalizar su ciclo de vida, ejecute el programa nuevamente y saldrá normalmente.

Por supuesto, algunas personas dirán que los subprocesos en el grupo de subprocesos que creó no son daemon, por lo que Jvm no dejará de ejecutarse cuando finalice el subproceso principal. Tienes razón, pero lo que debe ser liberado debe ser liberado a tiempo, si solo usas este planificador por un corto tiempo en todo el ciclo de vida del programa, ¿no habrá fugas de hilo si no cierras su hilo correspondiente? piscina todo el tiempo? Eso es vergonzoso.

Los diseñadores de rutinas de Kotlin también tienen mucho miedo de que las personas no se den cuenta de esto, y abandonaron deliberadamente dos API y abrieron un problema que decía que queríamos rehacer este conjunto de API. ¿Quiénes son estos dos pobres?

Dos API abandonadas para crear planificadores basados ​​en grupos de subprocesos

fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher
fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher

Estos dos pueden ser muy convenientes para crear un programador vinculado a un hilo específico, pero la API demasiado concisa parece hacer que las personas olviden su riesgo. A Kotlin nunca le ha gustado hacer cosas tan poco claras, por lo que debe construir el grupo de subprocesos usted mismo como en el ejemplo de esta sección, para que se olvide de cerrarlo de todos modos y no pueda culpar a otros (jajaja).

De hecho, ejecutar corrutinas en múltiples subprocesos, los subprocesos siempre se cortan de esta manera, no es muy ligero.Por ejemplo, el siguiente ejemplo es bastante aterrador:

Executors.newFixedThreadPool(10)
        .asCoroutineDispatcher().use { dispatcher ->
            GlobalScope.launch(dispatcher) {
                log(1)
                val job = async {
                    log(2)
                    delay(1000)
                    log(3)
                    "Hello"
                }
                log(4)
                val result = job.await()
                log("5. $result")
            }.join()
            log(6)
        }

Excepto delaypor un cambio de hilo inevitable, las operaciones de continuación ( Continuation.resume) en los puntos de suspensión de otras corrutinas cortarán hilos:

16:28:04:771 [pool-1-thread-1] 1
16:28:04:779 [pool-1-thread-1] 4
16:28:04:779 [pool-1-thread-2] 2
16:28:05:790 [pool-1-thread-3] 3
16:28:05:793 [pool-1-thread-4] 5. Hello
16:28:05:794 [pool-1-thread-4] 6

Si nuestro grupo de subprocesos solo abre 1 subproceso, toda la salida aquí se imprimirá en este único subproceso:

16:40:14:685 [pool-1-thread-1] 1
16:40:14:706 [pool-1-thread-1] 4
16:40:14:710 [pool-1-thread-1] 2
16:40:15:723 [pool-1-thread-1] 3
16:40:15:725 [pool-1-thread-1] 5. Hello
16:40:15:725 [pool-1-thread-1] 6

Comparando los dos, en el caso de 10 subprocesos, la cantidad de cambios de subprocesos es al menos 3 veces, mientras que en el caso de 1 subproceso, solo debe ejecutarse delayuna vez después de 1000 ms. Solo dos cambios de hilo más, ¿cuánto impacto tendrá? Ejecuté el ciclo 100 veces para dos casos diferentes en mi propio 2015 mbp y obtuve los siguientes tiempos promedio:

|
Número de hilos

|

10

|

1

|
| — | — | — |
|

MS que consume mucho tiempo

|

1006.00

|

1004.97

|

Tenga en cuenta que para que la prueba sea justa, se ha realizado un calentamiento antes de ejecutar el bucle 100 veces para garantizar que se hayan cargado todas las clases. Los resultados de las pruebas son solo para referencia.

Es decir, dos cambios de hilo más pueden tomar un promedio de 1 ms más de tiempo. El código en el entorno de producción, por supuesto, será más complicado.Si utiliza el grupo de subprocesos para programar de esta manera, se puede imaginar el resultado.

De hecho, por lo general, solo necesitamos procesar nuestra propia lógica comercial en un subproceso, y solo algunas E/S que consumen mucho tiempo deben cambiarse al subproceso de E/S para su procesamiento, por lo que una buena práctica puede referirse al programador correspondiente a la interfaz de usuario, y defina el planificador a través del grupo de subprocesos usted mismo No hay nada malo con el enfoque en sí mismo, pero es mejor usar solo un subproceso, porque los subprocesos múltiples tienen problemas de seguridad de subprocesos además de la sobrecarga del cambio de subprocesos mencionado anteriormente.

3.4 Temas de seguridad de subprocesos

El modelo de concurrencia de Js y Native es diferente de Jvm. Jvm expone la API de subprocesos a los usuarios, lo que también hace que la programación de rutinas sea más flexible para que los usuarios elijan. Más libertad significa más costo Lo que debemos entender al escribir código de corrutina en JVM es que aún existen problemas de seguridad de subprocesos entre diferentes corrutinas del programador.

Una buena práctica, como mencionamos en la sección anterior, es tratar de controlar su propia lógica dentro de un subproceso, lo que ahorra el costo de cambiar de subproceso por un lado y evita los problemas de seguridad del subproceso por otro lado, que es lo mejor. de ambos mundos.

Si usa herramientas de concurrencia, como bloqueos en el código coroutine, aumentará la complejidad del código. Mi sugerencia para esto es que intente evitar hacer referencia a variables variables en el ámbito externo al escribir código coroutine. Use el paso de parámetros en lugar de referencias. a las variables globales.

El siguiente es un ejemplo de un error, que es fácil de entender para todos:

suspend fun main(){
    var i = 0
    Executors.newFixedThreadPool(10)
            .asCoroutineDispatcher().use { dispatcher ->
                List(1000000) {
                    GlobalScope.launch(dispatcher) {
                        i++
                    }
                }.forEach {
                    it.join()
                }
            }
    log(i)
}

Resultado de salida:

16:59:28:080 [main] 999593

4. ¿Cómo programar la función principal de suspensión?

En el artículo anterior, mencionamos que suspend main iniciará una corrutina. Las corrutinas en nuestro ejemplo son todas sus subrutinas, pero ¿cómo surgió esta corrutina más externa?

Primero pongamos un ejemplo:

suspend fun main() {
    log(1)
    GlobalScope.launch {
        log(2)
    }.join()
    log(3)
}

Es equivalente a escribir lo siguiente:

fun main() {
    runSuspend {
        log(1)
        GlobalScope.launch {
            log(2)
        }.join()
        log(3)
    }
}

Entonces, ¿por qué runSuspenddices es sagrado? Es un método de la biblioteca estándar de Kotlin, tenga en cuenta que no está en kotlinx.coroutines, en realidad pertenece a una API de nivel inferior.

internal fun runSuspend(block: suspend () -> Unit) {
    val run = RunSuspend()
    block.startCoroutine(run)
    run.await()
}

Y RunSuspendaquí está Continuationla implementación de:

private class RunSuspend : Continuation<Unit> {
    override val context: CoroutineContext
        get() = EmptyCoroutineContext

    var result: Result<Unit>? = null

    override fun resumeWith(result: Result<Unit>) = synchronized(this) {
        this.result = result
        (this as Object).notifyAll()
    }

    fun await() = synchronized(this) {
        while (true) {
            when (val result = this.result) {
                null -> (this as Object).wait()
                else -> {
                    result.getOrThrow() // throw up failure
                    return
                }
            }
        }
    }
}

Su contexto está vacío, por lo que la corrutina iniciada por suspend main no tiene ningún comportamiento de programación.

A través de este ejemplo, podemos saber que en realidad iniciar una corrutina solo necesita una expresión lambda. Cuando se lanzó Kotlin 1.1 por primera vez, escribí una serie de tutoriales basados ​​en la API de la biblioteca estándar. Más tarde, descubrí que la API de la biblioteca estándar puede realmente no será utilizado por nosotros, así que solo eche un vistazo.

Los códigos anteriores están decorados en la biblioteca estándar internal, por lo que no podemos usarlos directamente. Sin embargo, puede copiar el contenido de RunSuspend.kt a su proyecto, para que pueda usarlo directamente, y varresult:Result<Unit>?=nullpuede informar un error, no importa, privatevarresult:Result<Unit>?=nullsimplemente cámbielo a .

5. Resumen

En este artículo, presentamos el contexto de la corrutina, presentamos el interceptor y finalmente condujimos a nuestro planificador. Hasta ahora, no hemos hablado sobre el manejo de excepciones, la cancelación de la corrutina, el soporte de Anko para las corrutinas, etc. Sí, si tiene algún tema relacionado. a las corrutinas que quieras saber, puedes dejar un mensaje~

¡Se pueden escanear más materiales de aprendizaje de Kotlin de forma gratuita!

La guía de tutorial de introducción a Kotlin

Capítulo 1 Guía del tutorial de introducción a Kotlin

​ ● Prefacio

imagen

Capítulo 2 Descripción general

​ ● Desarrollo del lado del servidor con Kotlin

​ ● Desarrollo Android con Kotlin

​ ● Descripción general de JavaScript de Kotlin

​ ● Kotlin/Native para desarrollo nativo

​ ● Corrutinas para escenarios como la programación asíncrona

​ ● Nuevas funciones en Kotlin 1.1

​ ● Nuevas funciones en Kotlin 1.2

​ ● Nuevas funciones en Kotlin 1.3

imagen

comienza el capitulo 3

​ ● Sintaxis básica

​ ● Modismos

​ ● Estándares de codificación

imagen

Capítulo 4 Fundamentos

​ ● Tipos básicos

​ ● paquete

​ ● Flujo de control: si, cuando, para, mientras

​ ● Retrocede y salta

imagen

Capítulo 5 Clases y objetos

​ ● Clases y Herencia

​ ● Atributos y campos

​ ● interfaz

​ ● Modificadores de visibilidad

​ ● Extensión

​ ● clase de datos

​ ● Sellado

​ ● Genéricos

​ ● Clases anidadas y clases internas

​ ● Clase de enumeración

​ ● Expresiones de objetos y declaraciones de objetos

​ ● Clases en línea

​ ● comisión

propiedad delegada

imagen

Supongo que te gusta

Origin blog.csdn.net/Android_XG/article/details/130007935
Recomendado
Clasificación