一般在主线程执行网络请求需要使用回调(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