Android技术知识Android开发Android开发经验谈

Kotlin协程原理

2023-11-04  本文已影响0人  奔跑吧李博
线程与协程关系:

协程虽然不能脱离线程而运行,但可以在不同的线程之间切换。

我为什么要用上协程呢?

Kotlin 协程的核心竞争力在于:它能简化异步并发任务。

比如需要做如下的一个功能:
查询用户信息 --> 查找该用户的好友列表 -->拿到好友列表后,查找该好友的动态
用地狱回调写法如下:


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 feed) {
                                if (feed != null) {
                                    System.out.println(feed);
                                }
                            }
                        });
                    }
                }
            });
        }
    }
});

而使用协程的写法代码就是如此清晰:

val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)
suspend fun getUserInfo(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "BoyCoder"
}

suspend fun getFriendList(user: String): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "Tom, Jack"
}

suspend fun getFeedList(list: String): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "{FeedList..}"
}

协程实现原理

定义挂起函数

suspend fun requestLoadUser(id: String)

挂起函数spspend的作用:
声明suspend函数,它是提醒开发者,这个方法里面是个耗时函数,如果你在主线程要调用,你得在协程中调用,并且切换到其它线程去调用,否则会卡顿主线程。

原本需要异步执行并使用回调的写法,用同步的写法,用挂起和恢复进行实现。

协程的挂起(suspend)和恢复(resume):

Kotlin 的编译器检测到 suspend 关键字修饰的函数以后,会自动将挂起函数转换成带有回调的函数。
反编译之后:

//                              Continuation 等价于 CallBack
//                                         ↓         
public static final Object getUserInfo(Continuation $completion) {
  ...
  return "BoyCoder";

可以看到,多了个Continuation(继续)参数,这是个接口,是在本次函数执行完毕后执行的回调。

Continuation的代码:

public interface Continuation<in T> {
    /**
     * 保存上下文(比如变量状态)
     */
    public val context: CoroutineContext
    /**
     * 方法执行结束的回调,参数是个泛型,用来传递方法执行的结果
     */
    public fun resumeWith(result: Result<T>)
}

suspend代码示例:

suspend fun getToken(id: String): String = "token"
suspend fun getInfo(token: String): String = "info"

// 添加了局部变量a,看下suspend怎么保存a这个变量
suspend fun test() {
    val token = getToken("123") // 挂起点1,这里是异步线程
    var a = 10 // 这里是10  //主线程
    val info = getInfo(token) // 挂起点2,需要将前面的数据保存(比如a),在挂起点之后恢复   //异步线程
    println(info)  //主线程
    println(a
}

反编译之后:

public final Object getToken(String id, Continuation completion) {
    return "token";
}

public final Object getInfo(String token, Continuation completion) {
    return "info";
}

// 重点函数(伪代码)
public final Object test(Continuation<String>: continuation) {
    Continuation cont = new ContinuationImpl(continuation) {
        int label; // 保存状态
        Object result; // 保存中间结果,还记得那个Result<T>吗,是个泛型,因为泛型擦除,所以为Object,用到就强转
        int tempA; // 保存上下文a的值,这个是根据具体代码产生的
    };
    switch(cont.label) {
        case 0 : {
            cont.label = 1; //更新label
            
            getToken("123",cont) // 执行对应的操作,注意cont,就是传入的回调
            break;
        }

        case 1 : {
            cont.label = 2; // 更新label
            
            // 这是一个挂起点,我们要保存上下文数据,这里就保存a的值
            int a  = 10;
            cont.tempA = a; // 保存a的值 

            // 获取上一步的结果,因为泛型擦除,需要强转
            String token = (Object)cont.result;
            getInfo(token, cont); // 执行对应的操作
            break;
        }

        case 2 : {
            String info = (Object)cont.result; // 获取上一步的结果
            println(info); // 执行对应的操作

            // 在挂起点之后,恢复a的值
            int a = cont.tempA;
            println(a);

            return;
        }
    }
}

我们可以将每个case理解为一个状态,每个case分支对应的语句,理解为一个Continuation实现。
上述伪代码大致描述了协程的调度流程:

1 调用test函数时,需要传入一个Continuation接口,我们会对它进行二次装饰。
2 装饰就是根据函数具体逻辑,在内部添加额外的上下文数据和状态信息(也就是label)。
3 每个状态对应一个Continuation接口,里面会执行对应的业务逻辑。
4 每个状态都会: 保存上下文信息 -> 获取上一个状态的结果 -> 执行本状态业务逻辑 -> 恢复上下文信息。
5 直到最后一个状态对应的逻辑执行完毕。

总结:

1 Kotlin中,每个suspend方法,都需要一个Continuation接口实现,用来执行下一个状态的操作;并且,每个suspend方法的调用点都会产生一个挂起点。
2 每个挂起点,都会产生一个label,对应于状态机的一个状态,不同的状态之间,通过Continuation来切换。
3 Kotlin协程会在每个挂起点保存当前的上下文数据,并且在挂起点之后进行恢复。这样,每个状态之间就是相互独立的,可以独立调度。
4 协程的切换,只不过是从一种状态切换到另一种状态,因为不同状态是相互独立的,所以在合适的时机,再切换回来也不会对结果造成影响。

参考:
https://www.modb.pro/db/211852
https://mp.weixin.qq.com/s/70wBBKwFFLb0X_zrsvNzDA
https://blog.csdn.net/jinking01/article/details/130520579

https://www.bilibili.com/video/BV1KJ41137E9/?spm_id_from=333.337.search-card.all.click&vd_source=40c24e77b23dc2e50de2b7c87c6fed59

上一篇 下一篇

猜你喜欢

热点阅读