Hurricane, la reacción química de Lifecycle, coroutine y Flow

prefacio

Artículos de la serie Coroutine:

Originalmente, la primera parte de la serie coroutine ha terminado, y algunos amigos luego sugirieron que podemos hablar sobre el uso real. Siento que no puedo parar, así que terminemos con algunos artículos más. Sabemos que un tema importante que no se puede evitar en el desarrollo de Android es el ciclo de vida ¿Cómo deberían cooperar los dos después de la introducción de las rutinas?
A través de este artículo, aprenderá:

  1. El pasado y el presente del ciclo de vida.
  2. La combinación de actividad y rutina.
  3. La cooperación de ViewModel y coroutine
  4. La aplicación crea un ámbito de rutina global
  5. La relación triangular de Flujo, corrutina y ciclo de vida.

1. Pasado y presente del ciclo vital

Breve descripción del ciclo de vida.

El diseño del sistema actual se centra más en la separación de la interfaz de usuario y los datos. Qué datos se necesitan para la visualización de la interfaz de usuario actual y cuándo mostrar los datos, todo esto debe ser controlado por los propios desarrolladores. Si no se controla adecuadamente, pueden producirse fugas de memoria y desperdicio de recursos.
Android proporciona cuatro componentes principales, de los cuales Actividad se utiliza para mostrar la interfaz de usuario. Su creación hasta su destrucción es su ciclo de vida completo. Entre los cuatro componentes principales, prestamos más atención al ciclo de vida de Actividad y Servicio, especialmente Actividad es el más importante Lo más importante, y el ciclo de vida del Fragmento depende de la Actividad, por lo que mientras comprenda el ciclo de vida de la Actividad, otras cosas no son un problema.

Inquietudes sobre el ciclo de vida de la actividad

Fuga de memoria de actividad

Tome la típica adquisición de datos de fondo y Toast to the UI como ejemplo:

        binding.btnStartLifecycle.setOnClickListener {
    
    
            thread {
    
    
                //模拟网络获取数据
                Thread.sleep(5000)
                runOnUiThread {
    
    
                    //线程持有Activity实例
                    Toast.makeText(this@ThirdActivity, "hello world", Toast.LENGTH_SHORT).show()
                }
            }
        }

Inicie el hilo en segundo plano, simule la solicitud de red, espere 5 segundos y abra el Toast.
No hay ningún problema en escenarios normales. ¿Qué pasará si sales de la Actividad antes de que aparezca el Toast en este momento?
Obviamente, por supuesto que habrá una fuga de memoria , porque la instancia de Actividad está en manos del subproceso y no se puede reciclar, y la Actividad se pierde.

desperdicio de recursos

Tome los datos obtenidos del fondo y muéstrelos en la Actividad como ejemplo:

        binding.btnStartGetInfo.setOnClickListener {
    
    
            thread {
    
    
                //模拟获取数据
                var count = 0
                while (true) {
    
    
                    Thread.sleep(2000)
                    runOnUiThread {
    
    
                        binding.count.text = "计算值:${
      
      count++}"
                        println("${
      
      binding.count.text}")
                    }
                }
            }
        }

Inicie el hilo en segundo plano, simule la solicitud de red y actualice TextView después de esperar 5 segundos.
No hay problema en escenarios normales. Si volvemos al escritorio o cambiamos a otras aplicaciones en este momento, no necesitamos actualizar la interfaz de usuario y no necesitamos obtener datos de red. En este caso, habrá ser un desperdicio de recursos, y este método debe evitarse.

Los dos fenómenos anteriores existen porque no se presta atención al ciclo de vida de la Actividad en el proceso de implementación de la función, en definitiva, se presta atención al ciclo de vida de la Actividad para resolver dos tipos de problemas:
imagen.png

La solución también es muy simple, ya sea que la actividad salga o regrese al fondo, habrá devoluciones de llamada para cada etapa del ciclo de vida. Por lo tanto, siempre que se monitoree el ciclo de Actividad, los problemas anteriores se pueden solucionar realizando la protección en el lugar correspondiente.
Para obtener más información, vaya a: Explicación detallada y seguimiento del ciclo de vida de la actividad de Android

2. La combinación de actividad y rutina

Uso de corrutinas sin ciclo de vida asociado

Mira la demostración primero:

        val scope = CoroutineScope(Job())
        binding.btnStartUnlifecyleCoroutine.setOnClickListener {
    
    
            scope.launch {
    
    
                delay(5000)
                scope.launch(Dispatchers.Main) {
    
    
                    Toast.makeText(this@ThirdActivity, "协程还在运行中", Toast.LENGTH_SHORT).show()
                }
            }
        }

Como se indicó anteriormente, se construye el alcance de la rutina y se inicia la rutina a través de él, y se imprimirá en segundo plano después de 5 segundos.
Cuando se hace clic en el botón, salimos de la Actividad y finalmente encontramos que Toast seguirá apareciendo, lo que indica que se ha producido una fuga.

El uso de corrutinas con ciclos de vida asociados.

arreglar la fuga

La aparición de corrutinas simplifica nuestra estructura de programación, pero mientras exista una relación con Activity, no podemos dejar de prestar atención a su ciclo de vida.
Afortunadamente, la rutina está asociada activamente con el ciclo de vida y los desarrolladores no necesitan manejarla manualmente. Veamos cómo usarla.

        binding.btnStartWithlifecyleCoroutine.setOnClickListener {
    
    
            lifecycleScope.launch {
    
    
                delay(5000)
                lifecycleScope.launch(Dispatchers.Main) {
    
    
                    Toast.makeText(this@ThirdActivity, "协程还在运行中", Toast.LENGTH_SHORT).show()
                }
                //假设有网络请求
                println("协程还在运行中")
            }
        }

La diferencia con la demostración anterior es la elección del alcance de la rutina. Esta vez, se usa lifecycleScope, que es un atributo extendido de LifecycleOwner.
Después de hacer clic en el botón, salga de la actividad. En este momento, no puede ver el brindis y no puede ver la impresión. Significa que el alcance de la corrutina detecta que la actividad sale y se destruye a sí misma, y ​​la instancia de la actividad no ser referenciado Por supuesto, el problema de pérdida de memoria está resuelto.

Evite desperdiciar recursos

Si tienes cuidado, te habrás dado cuenta: si haces clic en el botón y vuelves al escritorio en este momento, te darás cuenta de que la impresión todavía está en curso. De hecho, no queremos que sigan ejecutándose para ahorrar recursos ¿Qué debemos hacer?
Por supuesto, las corrutinas también consideran este escenario y brindan varias funciones convenientes.

        binding.btnStartPauseLifecyleCoroutine.setOnClickListener {
    
    
            lifecycleScope.launchWhenResumed {
    
    
                delay(5000)
                lifecycleScope.launch(Dispatchers.Main) {
    
    
                    Toast.makeText(this@ThirdActivity, "协程还在运行中", Toast.LENGTH_SHORT).show()
                }
                println("协程还在运行中")
            }
        }

Después de hacer clic en el botón, regrese al escritorio, y después de esperar unos segundos, no se encontró ninguna impresión.Después de regresar a la aplicación desde el escritorio, descubrí que aparecían Toast e impresión.
Esto también cumple con nuestros requisitos: cuando la aplicación está en primer plano, la rutina funciona, y cuando la aplicación está en segundo plano, la rutina deja de funcionar, lo que evita el desperdicio innecesario de recursos.
La función launchWhenResumed(), como su nombre lo indica, activa la rutina cuando la actividad está en estado de reanudación y suspende la rutina cuando no está en estado de reanudación. Similares son launchWhenCreated y launchWhenStarted.

El principio de corrutinas con ciclos de vida asociados.

El principio de resolver fugas de memoria.

Sabiendo cómo usarlo, es hora de explorar el principio nuevamente, centrándonos en el alcance de las rutinas.

#LifecycleOwner.kt
//扩展属性
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

#Lifecycle.kt
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
    
    
        while (true) {
    
    
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
    
    
                return existing
            }
            //构造新的协程作用域,默认在主线程执行协程
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
    
    
                //协程作用域关联生命周期
                newScope.register()
                return newScope
            }
        }
    }

fun register() {
    
    
    launch(Dispatchers.Main.immediate) {
    
    
        if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
    
    
            //监听生命周期变化
            lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
        } else {
    
    
            //如果已经处在destroy状态,直接取消协程
            coroutineContext.cancel()
        }
    }
}

Se puede ver en lo anterior:

  1. LifecycleOwner tiene un atributo extendido lifecycleScope y LifecycleOwner tiene Lifecycle, por lo que el lifecycleScope de LifecycleOwner proviene del atributo extendido coroutineScope de Lifecycle.
  2. Dado que es un atributo extendido de Lifecycle, por supuesto es posible monitorear los cambios de estado de Lifecycle

lifecycleScope monitorea los cambios de estado de Lifecycle, solo mire el procesamiento de su devolución de llamada:

#Lifecycle.kt
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
    
    
    if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
    
    
        //如果处于Destroy状态,也就是Activity被销毁了,那么移除监听者
        lifecycle.removeObserver(this)
        //取消协程
        coroutineContext.cancel()
    }
}

Hasta ahora es relativamente claro:

Cada instancia de actividad es un LifecycleOwner, y cada actividad está asociada con un objeto lifecycleScope, que puede monitorear el ciclo de vida de la actividad y cancelar la rutina cuando se destruye la actividad.

Principio de Evitar el Desperdicio de Recursos

En comparación con el principio de resolver las fugas de memoria, el principio de evitar el desperdicio de recursos es más complicado, así que echemos un vistazo breve.
Tome la función launchWhenResumed como ejemplo, que es una función en LifecycleCoroutineScope:

#Lifecycle.kt
public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
    
    
    //启动了协程
    lifecycle.whenResumed(block)
}

#PausingDispatcher.kt
public suspend fun <T> Lifecycle.whenResumed(block: suspend CoroutineScope.() -> T): T {
    
    
    return whenStateAtLeast(Lifecycle.State.RESUMED, block)
}

public suspend fun <T> Lifecycle.whenStateAtLeast(
    minState: Lifecycle.State,
    block: suspend CoroutineScope.() -> T
): T = withContext(Dispatchers.Main.immediate) {
    
    
    //切换协程,在主线程执行
    val job = coroutineContext[Job] ?: error("when[State] methods should have a parent job")
    //协程分发器
    val dispatcher = PausingDispatcher()
    //关联了生命周期
    val controller =
        LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)
    try {
    
    
        //在新的协程里执行block
        withContext(dispatcher, block)
    } finally {
    
    
        controller.finish()
    }
}

Lo anterior reveló tres piezas de información:

  1. launchWhenResumed no es una función de suspensión, inicia una nueva rutina internamente
  2. El cierre de launchWhenResumed es enviado por PausingDispatcher
  3. LifecycleController está asociado con un ciclo de vida

Centrarse en el punto 3:

#LifecycleController.kt
private val observer = LifecycleEventObserver {
    
     source, _ ->
    if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {
    
    
        //取消协程
        handleDestroy(parentJob)
    } else if (source.lifecycle.currentState < minState) {
    
    
        //小于目标状态,比如非Resume,则挂起协程
        dispatchQueue.pause()
    } else {
    
    
        //继续分发协程
        dispatchQueue.resume()
    }
}

init {
    
    
    if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
    
    
        handleDestroy(parentJob)
    } else {
    
    
        //LifecycleController 初始化时监听生命周期
        lifecycle.addObserver(observer)
    }
}

El ciclo de vida todavía está asociado con el ciclo de vida.

Se estima que la combinación de los códigos anteriores es un poco confusa y un poco enrevesada. No importan las reglas antiguas. Puedes verlo en la imagen: el punto
imagen.png
clave es el juicio de si se puede distribuir, que es basado en el estado en DispatchQueue:

    fun canRun() = finished || !paused

Cuando no está en estado Reanudar, pausado=verdadero, no se puede distribuir;
cuando está en estado Reanudar, pausado=falso, se puede distribuir.
Cuando la actividad sale, terminó = verdadero.

3. La cooperación entre ViewModel y coroutine

Uso de corrutinas sin ciclo de vida asociado

En la arquitectura MVVM, el enfoque recomendado es solicitar datos en ViewModel, como:

    val liveData = MutableLiveData<String>()
    fun getStuInfo() {
    
    
        thread {
    
    
            //模拟网络请求
            Thread.sleep(2000)
            liveData.postValue("hello world")
        }
    }

Luego escuche los cambios de datos en la Actividad:

        //监听数据变化
        val vm  by viewModels<MyVM>()
        vm.liveData.observe(this) {
    
    
            Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
        }
        vm.getStuInfo()

Por supuesto, el método de abrir directamente un subproceso para solicitar datos no es elegante. Ahora que hay una corrutina, solo use la corrutina para cambiar a la solicitud de subproceso.

    val scope = CoroutineScope(Job())
    fun getStuInfoV2() {
    
    
        scope.launch {
    
    
            //模拟网络请求
            delay(4000)
            liveData.postValue("hello world")
            println("hello world")
        }
    }

Los mismos pasos de prueba que arriba:
después de salir de la actividad, la impresión de corrutina en el modelo de vista continúa. Aunque la actividad no se filtra en este momento, también sabemos que el modelo de vista sirve a la actividad, la actividad se destruye y el modelo de vista no existir Es necesario, por lo que sus corrutinas asociadas también deben cancelarse para ahorrar recursos.

El uso de corrutinas con ciclos de vida asociados.

    fun getInfo() {
    
    
        viewModelScope.launch {
    
    
            //模拟网络请求
            delay(4000)
            liveData.postValue("hello world")
            println("hello world")
        }
    }

Esta forma de escribir es más concisa que la anterior.
Después de salir de la Actividad, la rutina se cancela y, por supuesto, no aparecerá la impresión.

El principio de corrutinas con ciclos de vida asociados.

El foco está en el objeto viewModelScope, que es una propiedad de extensión de ViewModel:

#ViewModel.kt
public val ViewModel.viewModelScope: CoroutineScope
    get() {
        //查缓存
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        //加入到缓存里
        return setTagIfAbsent(
            JOB_KEY,
            //构造协程作用域
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

ViewModel construye un atributo extendido: viewModelScope, que se usa para representar el alcance de la corrutina del ViewModel actual y almacenar el objeto de alcance en el mapa.
Puede llamar a viewModelScope en el lugar donde desea usar la rutina en ViewModel, lo que mejora enormemente la conveniencia.
A continuación, veamos cómo cancela la rutina después de que se destruye la Actividad.

    final void clear() {
    
    
        mCleared = true;
        if (mBagOfTags != null) {
    
    
            synchronized (mBagOfTags) {
    
    
                //从缓存取出协程作用域
                for (Object value : mBagOfTags.values()) {
    
    
                //取消协程
                closeWithRuntimeException(value);
            }
            }
        }
    }

Todo el proceso está representado por un diagrama:
imagen.png

El proceso anterior involucra el principio de ViewModel, si está interesado, puede pasar a: Jetpack ViewModel

4. La aplicación crea un ámbito de rutina global

Ya sea lifecycleScope en la actividad o viewModelScope en ViewModel, todos están relacionados con la página y no es necesario que existan después de que se destruye la página. Y a veces necesitamos usar rutinas en otros lugares fuera de la página, no se ven afectadas por la creación y destrucción de la página, generalmente pensamos en usar rutinas globales.
imagen.png

Propiedades de extensión de aplicación personalizada

val Application.scope: CoroutineScope
get() {
    
    
    return CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
//使用
application.scope.launch {
    
    
    delay(5000)
    println("协程在全局状态运行1")
}

Se construye un ámbito de rutina global y se puede acceder a este atributo extendido cuando otros módulos obtienen la instancia de la aplicación.
La ventaja de este enfoque: puede personalizar fácilmente el contexto de la rutina.

Alcance global

Generalmente se usa durante las pruebas y no se recomienda para proyectos formales.

GlobalScope.launch {
    
    
    delay(5000)
    println("协程在全局状态运行2")
}

ProcessLifecycleOwnerProcessLifecycleOwner

Producido oficialmente, se usa más a menudo para monitorear el estado de la aplicación en la parte delantera y trasera. El principio es monitorear el ciclo de vida. Dado que existe el ciclo de vida, por supuesto, hay un alcance de rutinas:

ProcessLifecycleOwner.get().lifecycleScope.launch {
    
    
    delay(5000)
    println("协程在全局状态运行3")
}

5. Relación triangular entre flujo, corrutina y ciclo de vida

concepto claro

Desde la perspectiva del desarrollo de Android, los tres tienen las siguientes diferencias:

  1. El ciclo de vida se refiere principalmente al ciclo de vida de la interfaz de usuario
  2. El flujo y las rutinas pertenecen a la categoría de lenguaje Kotlin, y Kotlin es multiplataforma.
  3. El flujo debe usarse en las corrutinas.
  4. Al combinar los dos puntos en 1.2, encontramos que el alcance de la rutina asociado con el ciclo de vida existe en forma de atributos extendidos.Después de todo, es posible que otras plataformas no necesiten asociar el ciclo de vida.

Flujo y ciclo de vida

Ciclo de vida de la asociación LiveData

Flow afirma ser una implementación mejorada de LiveData. Sabemos que LiveData puede detectar el ciclo de vida, como por ejemplo:

        binding.btnStartLifecycleLivedata.setOnClickListener {
    
     
            vm.liveData.observe(this) {
    
    
                //接收数据
                println("hello world")
            }
            vm.getInfo()
        }

Cuando la aplicación regresa al escritorio, incluso si ViewModel continúa asignando valores a LiveData, la devolución de llamada de LiveData no se activará. Cuando la aplicación vuelve al primer plano, se activará la devolución de llamada de LiveData.
Este diseño es para evitar el desperdicio innecesario de recursos.

Flujo combinado con launchWhenXX

En este punto, puedes pensar: en lugar de pasar LiveData para pasar datos, usa Flow para reemplazarlo ¿Cómo asociar el ciclo de vida?
De acuerdo con la experiencia anterior, es fácil escribir de la siguiente manera:

        binding.btnStartLifecycleFlowWhen.setOnClickListener {
    
    
            lifecycleScope.launchWhenResumed {
    
    
                MyFlow().flow.collect {
    
    
                    println("collect when $it")
                }
            }
        }

    val flow = flow {
    
    
        var count = 0
        while (true) {
    
    
            kotlinx.coroutines.delay(1000)
            println("emit hello world $count")
            emit(count++)
        }
    }

Construya un flujo frío, inicie la rutina a través de launchWhenResumed en la actividad y llame al operador de terminal de recopilación en la rutina. recopilar desencadena la ejecución del código en el cierre de flujo, emite datos continuamente y la impresión en el cierre de recopilación también continuará.
En este momento, regrese la aplicación al escritorio y descubra que la impresión no aparece, luego regrese la aplicación al primer plano y la impresión continúa. De esta manera, se puede lograr el mismo efecto que LiveData.
De los resultados impresos, también encontramos fenómenos interesantes:

Cuando se imprime el número 5, volvemos al escritorio y esperamos unos segundos antes de volver al primer plano. En este momento, la impresión comienza desde 6, lo que indica que
la función launchWhenXX no termina el trabajo aguas arriba del flujo cuando el La actividad no está activa, solo suspende la rutina.

Flujo combinado con repeatOnLifecycle

Y más a menudo, cuando la Actividad no está activa, no queremos que el flujo siga funcionando. En este momento, se introduce otra API: repeatOnLifecycle

        binding.btnStartLifecycleFlowRepeat.setOnClickListener {
    
    
            lifecycleScope.launch {
    
    
                repeatOnLifecycle(Lifecycle.State.RESUMED) {
    
    
                    MyFlow().flow.collect {
    
    
                        println("collect repeat $it")
                    }
                }
                println("repeatOnLifecycle over")
            }
        }

Encontrado por impresión:

Cuando se imprime el número 5, volvemos al escritorio, esperamos unos segundos y luego volvemos al primer plano. En este momento, la impresión comienza desde 0, lo que indica que la función repeatOnLifecycle finaliza
el trabajo aguas arriba del flujo cuando la Actividad no está activo, porque la rutina está cancelada. Cuando la actividad está activa, la corrutina se reinicia y el flujo de trabajo se reinicia

Es posible que aún tenga dudas: la demostración anterior no prueba directamente la diferencia entre los dos, porque la impresión en el cierre del flujo no aparece después de que la actividad se retira al escritorio.
Con una ligera modificación a la Demo, el resultado será obvio:

    val flow = flow {
    
    
        var count = 0
        while (true) {
    
    
            kotlinx.coroutines.delay(1000)
            println("emit hello world $count")
            emit(count++)
        }
    }.flowOn(Dispatchers.IO)

Cuando se usa repeatOnLifecycle, después de que la actividad vuelve al escritorio, la impresión desaparece, lo que indica que el flujo deja de funcionar.
Cuando se usa launchWhenXX, después de que la actividad vuelve al escritorio, la impresión continúa, lo que indica que el flujo está funcionando.

El principio de repetir en el ciclo de vida

repeatOnLifecycle es una función de extensión de LifecycleOwner, y luego una función de extensión de ciclo de vida, por lo que tiene un ciclo de vida.
La función repeatOnLifecycle abre una nueva rutina y monitorea los cambios en el ciclo de vida:

//监听生命周期
observer = LifecycleEventObserver {
    
     _, event ->
    if (event == startWorkEvent) {
    
    
        //大于目标生命状态,则启动协程
        launchedJob = this@coroutineScope.launch {
    
    
            // Mutex makes invocations run serially,
            // coroutineScope ensures all child coroutines finish
            mutex.withLock {
    
    
                coroutineScope {
    
    
                    block()
                }
            }
        }
        return@LifecycleEventObserver
    }
    if (event == cancelWorkEvent) {
    
    
        //小于目标生命状态,则取消协程
        launchedJob?.cancel()
        launchedJob = null
    }
    if (event == Lifecycle.Event.ON_DESTROY) {
    
    
        //Activity退出,则唤醒挂起的协程
        cont.resume(Unit)
    }
}
this@repeatOnLifecycle.addObserver(observer as LifecycleEventObserver)

Hay otra forma de usar repeatOnLifecycle:

                MyFlow().flow.flowWithLifecycle(this@ThirdActivity.lifecycle, Lifecycle.State.RESUMED)
                    .collectLatest {
    
    
                        println("collect repeat $it")
                    }

Tiene el mismo efecto que repeatOnLifecycle, excepto que el flujo generado de esta manera es seguro para subprocesos.

Diferencias y escenarios de aplicación entre launchWhenXX y repeatOnLifecycle

imagen.png

Finalmente, resuma la relación entre los tres.
imagen.png

Flow es muy poderoso y fácil de usar. La clave es cómo usarlo, cómo elegir entre muchos operadores de Flow para el desarrollo comercial y cómo distinguir sus funciones de un vistazo. La siguiente parte revelará el misterio de los operadores comunes de Flow, así que estad atentos.
Este artículo está basado en Kotlin 1.5.3, haga clic aquí para ver la demostración experimental completa

Si te gusta, dale me gusta, sigue y marca, tu aliento es mi motivación.

Actualización continua, sistema paso a paso conmigo, estudio en profundidad de Android/Kotlin

1. El pasado y el presente de los diversos contextos de Android
2. Android DecorView debe saber y saber
3. Cosas que debe saber Window/WindowManager
4. View Measure/Layout/Draw realmente entiende
5. Distribución de eventos de Android conjunto completo de servicios
6. Android invalida /postInvalidate /requestLayout completamente aclarado
7. Cómo determinar el tamaño de la ventana de Android/la razón de múltiples ejecuciones de onMeasure()
8. Análisis de controlador-mensaje-Looper basado en eventos de Android
9. El teclado de Android se puede hacer con un truco
10. Las diversas coordenadas de Android se entienden completamente
11. Actividad de Android/Ventana/Fondo de vista
12. Actividad de Android creada para ver mostrada
13. Serie IPC de Android
14. Serie de almacenamiento de Android
15. Serie de concurrencia de Java No más dudas
16. Serie de grupos de subprocesos de Java
17. Serie prebásica de Android Jetpack
18. Serie fácil de aprender y comprender de Android Jetpack
19. Serie de entrada fácil de Kotlin
20. Interpretación integral de la serie de rutinas de Kotlin

Supongo que te gusta

Origin blog.csdn.net/wekajava/article/details/129173528
Recomendado
Clasificación