挂起函数
挂起函数是指使用 suspend 关键字修饰的函数。
suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "BoyCoder"
}
挂起和恢复
挂起函数与普通函数的区别在于:挂起函数可以挂起和恢复。挂起和恢复也是协程与线程相比的优势。
考虑下面一种场景:
- 获取用户信息
- 获取用户的好友
- 获取每位好友的动态
如果使用 Java,可能会这么写:
public class SuspendFunction {
public void getAllFeeds() {
getUserInfo(new Callback() {
@Override
public void onSuccess(String user) {
if (user != null) {
System.out.println(user);
getFriendList(user, new Callback() {
@Override
public void onSuccess(String friendList) {
if (friendList != null) {
System.out.println(friendList);
getFeedList(friendList, new Callback() {
@Override
public void onSuccess(String feedList) {
if (feedList != null) {
System.out.println(feedList);
}
}
});
}
}
});
}
}
});
}
private void getFeedList(String friendList, Callback callback) {
new Thread(() -> {
SystemClock.sleep(1000L);
if (callback != null) {
callback.onSuccess("feedList");
}
}).start();
}
private void getFriendList(String user, Callback callback) {
new Thread(() -> {
SystemClock.sleep(1000L);
if (callback != null) {
callback.onSuccess("friendList");
}
}).start();
}
private void getUserInfo(Callback callback) {
new Thread(() -> {
SystemClock.sleep(1000L);
if (callback != null) {
callback.onSuccess("user");
}
}).start();
}
public interface Callback {
void onSuccess(String response);
}
}
这种多重回调的模式被称为“回调地狱”,代码嵌套层次多,可读性差。
如果使用 Kotlin 的挂起函数改写,会变得很简单:
fun main() = runBlocking {
val userInfo = getUserInfo()
val friendList = getFriendList(userInfo)
val feedList = getFeedList(friendList)
println(feedList)
}
suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "BoyCoder"
}
suspend fun getFriendList(user: String): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "friendList"
}
suspend fun getFeedList(friendList: String): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "feedList"
}
挂起函数的特点是使用同步的方式完成异步任务。
以下面的代码为例
val userInfo = getUserInfo()
getUserInfo 使用 withContext 切换到 IO 线程,延迟 1 秒,然后返回结果。程序在调用 getUserInfo 时挂起,然后返回结果给 userInfo 时恢复。不需要像 Java 使用回调来传递结果。
等号 “=” 右边的代码执行在子线程并挂起,右边执行完毕后,等号 “=” 左边的代码恢复到主线程执行。
深入理解 suspend
挂起函数的本质就是 Callback。Kotlin 的编译器会将 suspend 函数转换为带有 Callback 的普通函数。
反编译之前的 Kotlin 代码,可以看出 getUserInfo 挂起函数转换为了带有 Continuation 参数的普通函数。
@Nullable
public static final Object getUserInfo(@NotNull Continuation var0) {
...
}
Continuation 的定义如下:
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
Continuation 是一个接口,带有 resumeWith 方法用来返回结果。本质和 Callback 的作用是一样的。
这种将 suspend 函数转换为带有 Continuation 普通函数的过程叫做 CPS 转换。
CPS:Continuation-Passing-Style Transformation。
CPS转换就是将程序接下来要执行的代码进行传递的一种模式,它将原本的同步挂起函数转换为 Callback 异步代码。
协程之所以是非阻塞,是因为它支持“挂起和恢复”,而挂起和恢复的能力,主要来自挂起函数。挂起函数是由 CPS 实现的,其中的 Continuation,本质上是 Callback。
协程与挂起函数
协程的主要能力来自挂起函数,但是协程不等同于挂起函数。
挂起函数只能在协程或者其他挂起函数中调用。因为只有协程和其他挂起函数能够提供 Continuation。
从 runBlocking 的参数 block 可以看出,block 也是一个挂起函数。
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
因为挂起函数能够调用挂起函数,协程提供了 block 挂起函数,所以协程也能调用挂起函数。
挂起和恢复是协程的一种底层能力,而这种能力的实现,依靠的是挂起函数。
小结
- 挂起函数使用 suspend 关键字表示。
- 挂起函数能够以同步的方式写异步代码。
- 挂起函数拥有挂起和恢复的能力。
- 挂起函数的本质是 Callback,也就是 Continuation,Kotlin 编译器会完成 CPS 转换。
- 挂起函数只能在协程或者其他挂起函数中调用。
|