Kotlin协程介绍

前言


前言

介绍Kotlin协程之前,我们先来看看广义上的协程是啥。

  • 用于在程序中处理并发任务的一种方案
  • 比线程更加轻量级的存在,线程的上下文切换都需要内核(操作系统的核心)参与,而协程的上下文切换,完全由用户去控制,避免了大量的中断参与,减少了线程上下文切换与调度消耗的资源。线程上下文切换具体如下所示:

挂起一个线程,将这个线程在 CPU 中的状态(上下文)存储于内存中的某处;
恢复一个线程,在内存中检索下一个线程的上下文并将其在 CPU 的寄存器中恢复;
跳转到程序计数器所指向的位置(即跳转到线程被中断时的代码行),以恢复该线程。

线程上下文:线程Id + 线程状态 + 堆栈 + 寄存器状态等

 

1. Kotlin协程是啥?

  • Kotlin 的协程和⼴义的协程不是⼀种东⻄,Kotlin 的协程是⼀个线程框架,本质上只是一套基于原生Java Thread API 的封装,类似于Android 的 Handler 系列 API。
  • Android官方文档中提到,协程是一种并发设计模式,可以在 Android 平台上使用Kotlin协程来简化异步执行的代码。

2. Kotlin协程具体介绍

  2.0 Kotlin协程的优点

  • 协程最大的优点是可以帮我们自动切线程,用看起来同步的方式写出异步的代码,不用再写回调了。
  • 利用线程发起两个并行请求很困难,一般可能就会选择先后请求,而如果是协程的话利用两个async()函数开启两个协程同步请求就很容易了。
  • 使用协程作用域统一管理协程可以减少内存泄漏
  • 许多 Jetpack 库都包含提供全面协程支持的扩展。

  2.1 添加依赖

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

  2.2 启动协程

  • launch()

     需要在CoroutineScope中启动,launch返回一个「Job」,用于协程监督与取消,用于无返回值的场景。

GlobalScope.launch { 
    
}
  • async()

     需要在CoroutineScope中启动,async返回一个Job的子类「Deferred」,可通过await()获取完成时返回值。

val res = GlobalScope.async {

}
  • runBlocking()

     runBlocking函数会建立一个 堵塞当前线程的协程。

runBlocking {

}

  2.3 协程作用域(CoroutineScope)

     用于管理协程生命周期。

  • GlobalScope

     定义成了一个 单例对象, 在整个JVM虚拟中只有一份对象实例,生命周期贯穿整个JVM,故使用时需要警惕 内存泄漏!!!

  • CoroutineScope

     自定义协程作用域,可以指定协程作用域的上下文参数。

val scope = CoroutineScope(Dispatchers.IO + CoroutineName("MyCoroutineScope"))
scope.launch {

}
scope.cancel()
  • MainScope() 

     开启一个创建基于主线程的协程作用域

open class BaseActivity: AppCompatActivity(), CoroutineScope by MainScope(){  //可以构建一个实现了CoroutineScope接口的BaseActivity用于方便的调用launch开协程
    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}
  • coroutineScope()

     会继承外部的协程作用域并且创建一个子作用域。在作用域内所有代码和子协程运行完之前一直阻塞当前协程,但不影响其他协程和线程。可以用于在suspend函数中开协程。

  2.4 Job(作业)

  • 协程的 Job 是上下文的一部分,并且可以使用 coroutineContext [Job] 表达式在上下文中检索它。
  • 调用launch函数会返回一个Job对象,代表一个 协程的工作任务,可以用于查看协程状态以及控制协程。
//查看协程状态
isActive: Boolean    //是否存活
isCancelled: Boolean //是否取消
isCompleted: Boolean //是否完成
children: Sequence<Job> // 所有子作业

//控制协程
cancel()             // 取消协程
join()               // 堵塞当前线程直到协程执行完毕
cancelAndJoin()      // 两者结合,取消并等待协程完成
cancelChildren()     // 取消所有子协程,可传入CancellationException作为取消原因
attachChild(child: ChildJob) // 附加一个子协程到当前协程上

  2.5 CoroutineDispatcher(调度器)

     协程调度器是协程上下文的一部分,它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

  • Kotlin协程有四种调度器

 

 

  • withContext

     和launch、async及runBlocking不同,withContext不会创建新的协程,常用于 切换代码执行所运行的线程。
     它也是一个挂起方法,直到结束返回结果。多个withContext是串行执行的,所以很适合那种一个任务依赖上一个任务返回结果的情况。

  2.6 suspend关键字

  • suspend 也就是被挂起,而所谓的被挂起,就是切个线程;当运行到挂起函数的时候,协程会处于挂起状态(不会阻塞当前线程,脱离当前线程,开一个新的线程),等返回结果后,自动切回当前线程(协程会帮我 post 一个 Runnable,让我剩下的代码继续回到之前的线程去执行。)在协程挂起(等待)时,当前线程会回到线程池,当等待结束,协程会从线程池中一个空闲的线程上恢复。
fun main() {
    println("start ${Thread.currentThread().name}")
    GlobalScope.launch(Dispatchers.Main) {
        delay(1000)// 挂起函数,另开一个线程执行,不会阻塞当前线程
        println("launch ${Thread.currentThread().name}")
    }
    println("end ${Thread.currentThread().name}")
}
  • suspend函数运行在协程里面或者另一个suspend函数中,协程里才有上下文信息,让挂起函数切线程之后,能跳转到原来的线程。Kotlin协程中的上下文环境 → CoroutineContext,以 键值对 的方式存储各种不同元素:

CoroutineContext

Job(协程唯一标识) + CoroutineDispatcher(调度器) + ContinuationInterceptor(拦截器) + CoroutineName(协程名称,一般调试时设置)

  • suspend用于提醒和标记,代表是耗时函数,需要在后台运行,调用者不需要自行判断代码是否耗时。
  • 创建一个 suspend 函数,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 函数,这个 suspend 才是有意义的。

3. Jetpack库对协程的支持

  • ViewModelScope

对于 ViewModelScope,请使用 androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 或更高版本。

     为应用中的每个 ViewModel 定义了 ViewModelScope。如果 ViewModel 已清除,则在此范围内启动的协程都会自动取消。如果您具有仅在 ViewModel 处于活动状态时才需要完成的工作,此时协程非常有用。例如,如果要为布局计算某些数据,则应将工作范围限定至 ViewModel,以便在 ViewModel 清除后,系统会自动取消工作以避免消耗资源。
     可以通过 ViewModel 的 viewModelScope 属性访问 ViewModel 的 CoroutineScope,如以下示例所示:

class MyViewModel: ViewModel() {
        init {
            viewModelScope.launch {
                // ViewModel被清除时协程将会自动取消
            }
        }
    }
  • LifecycleScope

对于 LifecycleScope,请使用 androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 或更高版本。

     为每个 Lifecycle 对象定义了 LifecycleScope。在此范围内启动的协程会在 Lifecycle 被销毁时取消。您可以通过 lifecycle.coroutineScope 或 lifecycleOwner.lifecycleScope 属性访问 Lifecycle 的 CoroutineScope。
     以下示例演示了如何使用 lifecycleOwner.lifecycleScope 异步创建预计算文本:

class MyFragment: Fragment() {
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            viewLifecycleOwner.lifecycleScope.launch {
                val params = TextViewCompat.getTextMetricsParams(textView)
                val precomputedText = withContext(Dispatchers.Default) {
                    PrecomputedTextCompat.create(longTextContent, params)
                }
                TextViewCompat.setPrecomputedText(textView, precomputedText)
            }
        }
    }
  • 将协程与 LiveData 一起使用

对于 liveData,请使用 androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 或更高版本。

     使用 LiveData 时,您可能需要异步计算值。例如,您可能需要检索用户的偏好设置并将其传送给界面。在这些情况下,您可以使用 liveData 构建器函数调用 suspend 函数,并将结果作为 LiveData 对象传送,可以用于Repository中生成livedata数据返回给ViewModel。
     在以下示例中,loadUser() 是在其他位置声明的暂停函数。使用 liveData 构建器函数异步调用 loadUser(),然后使用 emit() 发出结果:

val user: LiveData<User> = liveData {
        val data = database.loadUser() // loadUser 需要声明为suspend函数,否则liveData将创建一个在主线程执行
        emit(data)
    }

     当 LiveData 变为活动状态时,代码块开始执行;当 LiveData 变为非活动状态时,代码块会在可配置的超时过后自动取消。如果代码块在完成前取消,则会在 LiveData 再次变为活动状态后重启;如果在上次运行中成功完成,则不会重启。请注意,代码块只有在自动取消的情况下才会重启。如果代码块由于任何其他原因(例如,抛出 CancelationException)而取消,则不会重启。
     还可以从代码块中发出多个值。每次 emit() 调用都会暂停执行代码块,直到在主线程上设置 LiveData 值。

val user: LiveData<Result> = liveData {
        emit(Result.loading())
        try {
            emit(Result.success(fetchUser()))
        } catch(ioException: Exception) {
            emit(Result.error(ioException))
        }
    }

     也可以将 liveData 与 Transformations 结合使用,如以下示例所示:

class MyViewModel: ViewModel() {
        private val userId: LiveData<String> = MutableLiveData()
        val user = userId.switchMap { id ->
            liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
                emit(database.loadUserById(id))
            }
        }
    }

4. Retrofit对协程的支持

     Retrofit  2.6.0 版本,内置了对 Kotlin Coroutines 的支持,进一步简化了使用 Retrofit 和协程来进行网络请求的过程。

     举例如下:
     在ApiService中,利用suspend关键字声明该函数需要在后台执行,同时函数可以直接返回我们需要的对象,而不是返回Deferred<T> 对象。

interface ApiService {
    @GET("article/list/{page}/json")
    suspend fun getHotArticle(@Path("page")page: Int): ArticleResponse
}    
object ServiceCreator {
    val retrofit: Retrofit = Retrofit.Builder()
    .baseUrl("https://www.wanandroid.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    
    inline fun <reified T> create() = retrofit.create(T::class.java)
}    
object ApiNetwork {
    private val apiService = ServiceCreator.create<ApiService>()   // 创建动态代理对象
    
    suspend fun getArticle(page: Int) = apiService.getHotArticle(page) // 调用suspend函数,该函数也需要声明为suspend函数
}   

     在Repository中获取数据

object HotRepository {
    fun getArticle(page: Int) = liveData(Dispatchers.IO){   // livedata() 函数最后一个参数是一个lambda表达式,该lambda表达式中可以调用suspend函数

        val res = try {
            val articleResponse = ApiNetwork.getArticle(page)
            if (articleResponse.errorCode == 0) {
                Result.success(articleResponse) // Result 类可以对结果进行封装,返回更加直观的信息
            } else {
                Result.failure(RuntimeException("errorCode is ${articleResponse.errorCode}"))
            }
        } catch (e: Exception) {
            Result.failure<ArticleResponse>(e)
        }
        emit(res)
    }
}

 


参考文章

枯燥的Kotlin协程三部曲(中)——应用实战篇

Kotlin 的协程用力瞥一眼 - 学不会协程?很可能因为你看过的教程都是错的

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值