Android知识点19--Kotlin的协程

63 阅读7分钟

1. Kotlin 协程是什么?比线程有哪些优势?

对协程的认识? 协程提供了一种避免阻塞线程并且用更简单可控的操作替代线程阻塞的方法:协程挂起。协程可以被挂起而不阻塞线程,线程的阻塞代价比较高,协程的挂起几乎是无代价的,不需要上下文切换或者OS的干预。 协程主要让原来要使用“异步+回调”的写法,简化成可以看似同步的方式,这样可以按串行的思维去编码,不去过多考虑异步处理。

协程是一种用户态的轻量级线程,有协程构建器(launch coroutine builder) 启动。

协程的特点

  • 轻量: 可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞,挂起比阻塞节省内存,且支持多个并行操作。
  • 内存泄漏少:使用结构化并发在一个作用域内执行多项操作。
  • 内置取消支持:取消操作会自动在运行中的真个协程层次结构内传播。
  • Jetpack集成:许多Jetpack提供协程支持的扩展,某些还有协程作用域,可用于结构化并发。
  • 可以用同步的方式写出异步的代码
coroutineScope.launch(Dispatchers.Main) {       // 开始协程:主线程
    val token = api.getToken()                  // 网络请求:IO 线程
    val user = api.getUser(token)               // 网络请求:IO 线程
    nameTv.text = user.name                     // 更新 UI:主线程
}

2. 如何理解挂起的概念?

挂起是什么? 使用suspend修饰,挂起的对象不是线程也不是函数,而是协程。 启动一个协程可以使用launch/async函数,协程其实就是这两个函数中闭包的代码块。 当创建的协程执行到某个suspend函数时,这个协程会被「suspend」,也就是会被挂起。也可以理解为这个协程从正在执行它的线程上脱离了。也可以理解为线程执行到协程的suspend函数时,暂时不继续执行协程代码了。

协程运行在线程中,当协程遇到了suspend后执行挂起操作,协程从线程中脱离,分别看一下线程和协程接下来要做什么? //参考 www.jianshu.com/p/e4e7ae947…

  • 对于线程而言: 遇到suspend后,不再继续执行剩余的协程代码,跳出协程的代码块。 如果是后台线程,可能会去执行其他任务或者被系统回收。 如果是Android主线程,会继续执行其他操作,如界面刷新任务。
  • 对于协程而言: 协程会从这个suspend函数开始继续执行下去,不过是在指定线程中。通过Dispatchers调度器执行的线程。Dispathers可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池。常用的调度器有三种: Dispatchers.Main: Android中的主线程 Dispatchers.IO: 针对磁盘和网络IO进行了优化,适合IO密集型任务,如读写文件、操作数据库、网络请求 Dispatchers.Default: 适合CPU密集型计算,比如计算

在suspend函数执行完成后,协程能自动帮我们把线程切回到执行挂起操作前的线程。这个线程操作的恢复需要在协程中完成,所以suspend函数只能在协程里或者另一个suspend函数里被调用。 因为我们在挂起操作的时候,切换到其他线程中去执行了,所以不会阻塞之前的线程,也就是我们说的非阻塞式挂起。阻塞是针对单线程而言的。

withContext(Dispatchers.IO){}使用withContext可以确保每个函数都是主线程安全的,这样可以从主线程调用每个函数。使用suspend不会让Kotlin在后台线程上运行,应该始终在suspend函数内使用withContext()。 注意: 利用一个使用线程池的调度程序(例如 Dispatchers.IO 或 Dispatchers.Default)不能保证块在同一线程上从上到下执行。在某些情况下,Kotlin 协程在 suspend 和 resume 后可能会将执行工作移交给另一个线程。这意味着,对于整个 withContext() 块,线程局部变量可能并不指向同一个值。

3. 启动协程的方式有哪些?

  • 使用launch方式可启动新协程而不是将结果返回给调用方。
  • async会启动一个新的协程,并允许您使用一个名为await的挂起函数返回结果。 通常,使用launch从常规函数启动新协程,常规函数无法调用await。只有在另一个协程内时,或在挂起函数内且正在执行并行分解时,才能使用async。???

launch和async处理异常的方式不同,如果使用async从常规函数中启动协程,则能以静默的方式丢弃异常,不会被捕获或显示在logcat中。

结构化并发是什么? 在suspend函数启动的所有协程都必须在函数返回结果时停止,通过结构化并发机制可以定义用于启动一个或多个协程的coroutineScope。

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

还可以使用awaitAll()

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
    }

注意,即便我们没有调用awaitAll(),coroutineScope构建器也会等所有协程都完成后才恢复名为fetchTwoDocs的协程。coroutineScope会捕获协程抛出的所有异常,并将其传送回调用方。

CoroutineScope会追踪它使用的launch或async创建的所有协程。可以调用scope.cancel()取消正在进行的协程。 ViewModel 有 viewModelScope Lifecycle 有 lifecycleScope

如果自定义一个CoroutineScope?

    // Job 和 Dispatcher 组合成一个CoroutineContext
    val scope = CoroutineScope(Job() +Dispatchers.Main)
    
    fun method(){
        // 在scope作用域内启动一个新的协程
        scope.launch { 
            // 新的协程可以调用suspend修饰的方法
            fetchDocs()
        }
    }
    fun cancel(){
        // 取消协程作用域,已取消的作用域无法再创建协程
        scope.cancel()
    }

    private suspend fun fetchDocs() {
        
    }

使用viewModelScope时,ViewModel会在ViewModel的onCleared()方法中自动为您取消作用域。

4. Job是什么?

Job是协程的句柄,使用launch或async创建的每个协程都返回一个Job实例,该实例是相应协程的唯一标识并管理其生命周期。还可以将Job传递给CoroutineScope以进一步管理。

    fun method(){
        // 在scope作用域内启动一个新的协程
        val job = scope.launch {
            // 新的协程可以调用suspend修饰的方法
            fetchDocs()
        }
        
        if(){
            job.cancel()
        }
    }

虽然协程有New、Cancelling、Completing状态,外部无法感知这三种状态,Job只提供了isActive、isCancelled、isCompleted属性来供外部判断协程是否处于Active、Cancelled、Completed状态。

Android中使用协程的最佳做法?// developer.android.google.cn/kotlin/coro…

  • 创建新协程或调用withContext时,最好不使用Dispatchers硬编码,先定义出来再注入到使用的地方。
  • 挂起函数应该能够安全的从主线程调用
  • ViewModel应该首选创建创建协程,而不是公开挂起函数来执行业务逻辑。
  • ViewModel最好向其他类公开不可变类型。
  • 避免使用GlobalScope
  • 在业务层和数据层中创建协程
    • 如果要完成的工作不限于特定屏幕,工作的存在时间比调用方的声明周期更长,可以使用外部CoroutineScope。externalScope 应由存在时间比当前屏幕更长的类进行创建和管理,并且可由 Application 类或作用域限定为导航图的 ViewModel 进行管理。
      class ArticlesRepository(
          private val articlesDataSource: ArticlesDataSource,
          private val externalScope: CoroutineScope,
          private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
      ) {
          // As we want to complete bookmarking the article even if the user moves
          // away from the screen, the work is done creating a new coroutine
          // from an external scope
          suspend fun bookmarkArticle(article: Article) {
              externalScope.launch(defaultDispatcher) {
                  articlesDataSource.bookmarkArticle(article)
              }
                  .join() // Wait for the coroutine to complete
          }
      }