协程中的suspend的原理

126 阅读3分钟

一般在主线程执行网络请求需要使用回调(Callbacks),尽管使用回调后,代码的可读性会变得更差。使用回调从 developer.android.com 获取数据的代码类似这样:

class ViewModel: ViewModel() {  

    fun fetchDocs() {  
        get("developer.android.com") { result ->  
            show(result)  
        }  
    }  
    
}

这里 get() 方法是从主线程发起的,我们一般需要在里面启动一个子线程来执行网络请求,这样主线程可以同时去做其他的事情。一旦请求结果返回后,主线程的回调将会被调起。

下面我们使用协程来对这个示例进行改造,代码如下:

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}// look at this in the next section

suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

这里没有使用回调,直接把 get() 方法的返回结果给 result,明明是同步的写法为什么不会阻塞主线程呢?

原因是协程添加了 2 个新的操作:

  • suspend — 暂停当前协程的执行(挂起当前协程),并保存所有的局部变量
  • resume — 让挂起的协程从暂停的地方继续执行

通过给函数添加 suspend 关键字来实现此功能,被 suspend 修饰的函数称为挂起函数,挂起函数只能在挂起函数或协程作用域中调用。

上面的示例中,get() 方法是挂起函数,会在开始网络请求之前挂起当前协程,这里使用了 withContext(Dispatchers.IO) 让网络请求在另一个线程中执行,拿到请求结果之后,直接 resume 挂起的协程,把请求结果给 result。

每当协程挂起时,当前堆栈帧( stack frame:Kotlin 用来跟踪哪个函数正在运行及其变量的位置)都会被复制并保存起来以备后用。 resume 的时候,堆栈帧从保存的地方复制回来并继续执行。当主线程所有的协程都挂起了,主线程还是可以执行其他的更新 UI 和处理用户输入事件的逻辑。这里使用 suspend 和 resume 来代替回调,代码变得更加简洁了,可读性也更高了。

在调用 get() 方法获取网络请求结果的时候使用了 withContext() 并传入了Dispatchers 参数来指定协程运行的线程,Dispatchers 参数常用的有三种:

  • Dispatchers.Main:Android 中的主线程,可以直接操作 UI 和执行简单任务。
  • Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件、操作数据库、网络请求。
  • Dispatchers.Default:适合 CPU 密集型的任务,比如解析 JSON 文件、排序一个较大的列表、DiffUtils。

这样前面示例中 withContext(Dispatchers.IO){...} 代码块中的代码就是运行在IO Dispatcher 中,withContext() 也是一个挂起函数,你在主线程可以很安全地调用它。

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}// Dispatchers.Main

suspend fun get(url: String) =
    // Dispatchers.Main
    withContext(Dispatchers.IO) {
        // Dispatchers.IO
        /* perform blocking network IO here */
    }
    // Dispatchers.Main

这样,使用协程你可以很精细地控制线程的调度。


参考文档:

 Coroutines on Android (part I): Getting the background 

 Understand Kotlin Coroutines on Android (Google I/O'19) 

 Using Kotlin Coroutines in your Android App