Kotlin协程

940 阅读4分钟

学习自码上开学

1.配置

  • 项目根目录下的 build.gradle
buildscript {

   ...

    ext.kotlin_coroutines = '1.3.1'

   ...

}
  • **Module 下的 build.gradle **
dependencies {
   ...
   //依赖协程核心库
   implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
   //依赖当前平台所对应的平台库
   implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
}

2.协程就是切线程,就是一套基于线程的上层框架

启动一个协程可以使用 launch 或者 async 函数,协程其实就是这两个函数中闭包的代码块

  • 基本使用
    • 方法一,使用 runBlocking 顶层函数。适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的
      runBlocking {
      
          getImage(imageId)
      
      }
      
    • 方法二,使用 GlobalScope 单例对象。不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和 app 一致,且不能取消
      GlobalScope.launch {
      
         getImage(imageId)
      
      }
      
    • 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象。推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西)
      val coroutineScope = CoroutineScope(context)
      coroutineScope.launch(Dispatchers.Main) {   // 在主线程开启协程
      
         val user = api.getUser() // IO 线程执行网络请求
      
         nameTv.text = user.name  // 主线程更新 UI
      
      }
      
      //在activity里写法
      class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() {
         ......
         override fun initData() {
             launch(Dispatchers.Default) {
         }
      }
      
      kotlin中合并请求
      coroutineScope.launch(Dispatchers.Main) {
      
         val avatar = async { api.getAvatar(user) }    // 获取用户头像
      
         val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
      
         val merged = suspendingMerge(avatar, logo)    // 合并结果
      
         show(merged) // 更新 UI
      
      }
      
  • 使用withContext切换线程。需要频繁地进行线程切换时
    // 第一种写法
    coroutineScope.launch(Dispachers.IO) {
      ...
      launch(Dispachers.Main){
          ...
          launch(Dispachers.IO) {
              ...
              launch(Dispacher.Main) {
                  ...
              }
          }
      }
    }
    
     // 通过第二种写法来实现相同的逻辑
     coroutineScope.launch(Dispachers.Main) {
      ...
       withContext(Dispachers.IO) {
          ...
      }
      ...
       withContext(Dispachers.IO) {
          ...
       }
      ...
     }
    
    withContext放进一个单独的函数里面
    launch(Dispachers.Main) {              //在 UI 线程开始
       val image = getImage(imageId)
       avatarIv.setImageBitmap(image)     //执行结束后,自动切换回 UI 线程
    }
    
    suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
       ...
    }
    
  • launchasync 函数
    • 相同点:它们都可以用来启动一个协程,返回的都是 Coroutine,我们这里不需要纠结具体是返回哪个类。
    • 不同点:async 返回的 Coroutine 多实现了 Deferred 接口。调用 Deferred.await() 就可以得到结果
    coroutineScope.launch(Dispatchers.Main) {
       //async 函数启动新的协程
       val avatar: Deferred = async { api.getAvatar(user) }    // 获取用户头像
       val logo: Deferred = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
       //获取返回值
       show(avatar.await(), logo.await())                     // 更新 UI
    }
    

3.suspend挂起

launchasync 或者其他函数创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被suspend,也就是从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。
不过这个suspend,其实并不是起到把任何把协程挂起,或者说切换线程的作用。 真正挂起协程这件事,是 Kotlin 的协程框架帮我们做的,比如使用withContext函数。 所以我们想要自己写一个挂起函数,仅仅只加上 suspend 关键字是不行的

suspend fun suspendingPrint() {
   //输出的结果还是在主线程
   //System.out: Thread: main
   println("Thread: ${Thread.currentThread().name}")
}

suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
    //切到的另一个线程
    println("Thread: ${Thread.currentThread().name}")
}

所以当自己自定义suspend 函数,只需两步

  • 给函数加上 suspend 关键字,
  • 使用挂起函数比如withContext或者delay
suspend fun suspendUntilDone() {
  while (!done) {
    //挂起函数 delay,它的作用是等待一段时间后再继续往下执行代码
    delay(5)
  }
}

4.非阻塞式挂起

Java中单线程是阻塞式的。多线程才是非阻塞式。
Kotlin中非阻塞式指的是协程可以用看起来阻塞的代码写出非阻塞式的操作

5.应用

ActivityViewModel的生命周期绑定

abstract class BaseVMActivity<VM : BaseViewModel>() : AppCompatActivity() {
    lateinit var mViewModel: VM
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModelClass: Class<VM> = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<VM>
        //将viewModel的生命周期和activity的生命周期绑定
        mViewModel = ViewModelProviders.of(this).get(viewModelClass)
        ......
    }

这样Activity销毁时候会回调和ViewModelonCleared()方法。在onCleared()方法中我们可以将协程取消

open class BaseViewModel : ViewModel() {
    /**
     * 这是此 ViewModel 运行的所有协程所用的任务。 终止这个任务将会终止此 ViewModel 开始的所有协程。
     */
    protected val viewModelScope = SupervisorJob()

    /**
     * 这是 MainViewModel 启动的所有协程的主作用域。
     * 因为我们传入了 viewModelJob,你可以通过调用viewModelJob.cancel(),来取消所有 uiScope 启动的协程。
     */
    protected val uiScope = CoroutineScope(Dispatchers.Main + viewModelScope)

    override fun onCleared() {
        super.onCleared()
        viewModelScope.cancel() // 取消所有协程
    }

这样当Activity销毁时候会取消由viewModelScope启动的所有协程