Android-重学kotlin(协程源码第一阶段)新学习总结

发布于:2025-07-10 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、挂起函数源码分析

1.编译器转换(CPS 转换)

        挂起函数,只是比普通的函数多了suspend关键字。有了这个suspend关键字以后,Kotlin编译器就会特殊对待这个函数,将其转换成一个带有Callback的函数,这里的Callback就是Continuation接口。而这个过程,我们称之为CPS转换
suspend fun fetchData(url: String): String {
    delay(1000) // 挂起点1
    val result = api.request(url) // 挂起点2
    return result
}


//编译器会将其转换为带有 Continuation 参数的普通函数,函数签名变为:

fun fetchData(url: String, continuation: Continuation<String>): Any?
  • 新增的Continuation<String>参数:本质是一个 “回调对象”,保存了挂起函数暂停时的执行状态(如局部变量、程序计数器、调用栈信息等),用于后续恢复执行。
  • 参数返回值变为Any?:有两种可能结果:
    1. 若函数正常执行完成(未挂起),返回实际结果(如String);
    2. 若函数需要挂起,返回一个特殊标记COROUTINE_SUSPENDED,告知调用者 “当前函数已挂起,需等待后续恢复”。

Continuation是挂起函数状态保存与恢复的核心接口,源码定义简化如下:

interface Continuation<in T> {
    val context: CoroutineContext // 协程上下文(含调度器等信息)
    fun resumeWith(result: Result<T>) // 恢复执行的触发方法
}
  • context:保存协程的调度器(Dispatcher)、Job 等信息,决定恢复执行时使用哪个线程。
  • resumeWith:当挂起条件满足(如delay时间到、网络请求返回)时,通过此方法触发挂起函数的恢复,result为挂起期间的执行结果(成功或异常)。

2.状态机实现

        当挂起函数经过反编译以后,它会变成由switch和label组成的状态机结构编译器会生成一个状态枚举(如State),每个状态对应一个挂起点前后的逻辑:
private enum class FetchDataState {
    INITIAL, // 初始状态(未执行)
    AFTER_DELAY, // 执行完delay后的状态
    AFTER_REQUEST // 执行完api.request后的状态
}

同时,函数会被改造为一个状态机处理函数,大致逻辑如下:

fun fetchData(url: String, continuation: Continuation<String>): Any? {
    // 从continuation中获取或初始化状态机
    val stateMachine = continuation as? FetchDataStateMachine ?: FetchDataStateMachine(continuation)
    val context = continuation.context

    return when (stateMachine.state) {
        FetchDataState.INITIAL -> {
            // 第一次执行:处理初始逻辑,调用第一个挂起函数(delay)
            stateMachine.url = url // 保存局部变量
            stateMachine.state = FetchDataState.AFTER_DELAY // 更新状态
            // 调用delay,将当前状态机作为其continuation传递
            val delayResult = delay(1000, stateMachine)
            if (delayResult == COROUTINE_SUSPENDED) {
                return COROUTINE_SUSPENDED // 触发挂起
            } else {
                // 若delay未挂起(如时间为0),直接进入下一个状态
                stateMachine.state = FetchDataState.AFTER_DELAY
                // 继续执行下一段逻辑(相当于“伪恢复”)
                fetchData(url, stateMachine)
            }
        }

        FetchDataState.AFTER_DELAY -> {
            // 从delay恢复后:执行第二个挂起函数(api.request)
            stateMachine.state = FetchDataState.AFTER_REQUEST
            val requestResult = api.request(stateMachine.url, stateMachine) // 传递状态机
            if (requestResult == COROUTINE_SUSPENDED) {
                return COROUTINE_SUSPENDED // 再次挂起
            } else {
                // 若request未挂起,直接进入下一个状态
                stateMachine.state = FetchDataState.AFTER_REQUEST
                fetchData(url, stateMachine)
            }
        }

        FetchDataState.AFTER_REQUEST -> {
            // 从api.request恢复后:返回结果
            val result = stateMachine.result // 从状态机中取出request的结果
            stateMachine.continuation.resumeWith(Result.success(result)) // 通知上层恢复
            null // 执行结束
        }
    }
}

// 状态机类:保存局部变量和当前状态
private class FetchDataStateMachine(
    val continuation: Continuation<String>
) : Continuation<String> by continuation {
    var state: FetchDataState = FetchDataState.INITIAL
    var url: String = "" // 保存局部变量url
    var result: String = "" // 保存api.request的结果
}
        协程状态机代码:其中,when
表达式实现了协程状态机,而continuation.label 则代表了当前状态机的具体状态,continuation.label 改变一次,就代表了挂起函数被调用了一次;

3.挂起函数触发后的挂起和恢复流程

  1. 编译转换:挂起函数被转换为带Continuation参数的函数,逻辑拆分为状态机。
  2. 初始执行:从INITIAL状态开始,执行到第一个挂起点(如delay)。
  3. 挂起触发:若挂起函数返回COROUTINE_SUSPENDED,当前函数返回该标记,线程释放。
  4. 等待恢复:挂起期间,Continuation(状态机)被保存,等待条件满足(如时间到)。
  5. 恢复执行:条件满足后,Continuation.resumeWith被调用,状态机从上次保存的状态(如AFTER_DELAY)继续执行。
  6. 执行完成:当所有状态执行完毕,通过最上层的Continuation返回结果,整个挂起函数执行结束。

阶段总结

        挂起函数因带有suspend关键字会被 Kotlin 编译器进行 CPS 转换,即转换为带有Continuation参数的普通函数,Continuation作为回调对象保存挂起时的执行状态(如局部变量、调用栈等),函数返回值变为Any?,要么是实际结果,要么是特殊标记COROUTINE_SUSPENDED表示挂起;

        转换后的函数会形成由switchlabel组成的状态机结构,编译器生成对应挂起点的状态枚举,状态机通过when表达式实现状态流转,状态机类保存局部变量和当前状态,每次状态改变代表挂起函数被调用一次;

        其挂起和恢复流程为:编译转换后从INITIAL状态开始执行,到挂起点时若返回COROUTINE_SUSPENDED则触发挂起并释放线程,挂起期间Continuation保存状态等待条件满足,条件满足后通过Continuation.resumeWith恢复执行,状态机从上次保存的状态继续,直至所有状态执行完毕,通过最上层Continuation返回结果。

二、lauch的启动源码分析

        底层都依赖着协程构建器(Coroutine Builder)协程上下文(CoroutineContext)

1.协程启动的入口函数:CoroutionScope.launch

launchCoroutineScope的扩展函数,用于启动一个不返回结果的协程。其源码定义如下:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    // 获取当前协程作用域的上下文
    val newContext = newCoroutineContext(context)
    // 创建一个新的协程实例(根据不同的启动模式)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block)
    else
        StandaloneCoroutine(newContext, active = true)
    // 根据启动模式决定是立即执行还是延迟执行
    coroutine.start(start, coroutine, block)
    return coroutine
}
  1. context: CoroutineContext
    协程上下文,包含调度器(Dispatcher)、Job、异常处理器等元素。默认值为EmptyCoroutineContext,表示使用父协程的上下文。

  2. start: CoroutineStart
    协程启动模式,枚举类型,主要包括:

    • DEFAULT:立即调度协程执行
    • LAZY:懒启动,需显式调用start()join()触发
    • ATOMIC:立即执行,但在调度前无法取消
    • UNDISPATCHED:立即在当前线程执行,直到第一个挂起点
  3. block: suspend CoroutineScope.() -> Unit
    协程体,是一个挂起函数,可在其中调用其他挂起函数。

2.协程上下文的构建与继承

newCoroutineContext(context)是关键方法,用于合并当前作用域的上下文和用户传入的上下文:

public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    // 获取当前作用域的上下文(如GlobalScope、lifecycleScope的上下文)
    val combined = this.coroutineContext + context
    // 如果没有显式指定Job,则添加一个父Job(确保结构化并发)
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    return if (combined[Job] != null) debug else debug + Job()
}

关键点:

  • 上下文合并:通过+操作符合并上下文元素,相同类型的元素后添加的会覆盖前面的(如重复指定 Dispatcher)。
  • 默认 Job:若用户未显式指定 Job,会自动添加一个,确保协程有父子关系,实现结构化并发。

3.协程示例的创建与启动模式

根据start参数的不同,会创建不同类型的协程实例:

val coroutine = if (start.isLazy)
    LazyStandaloneCoroutine(newContext, block)
else
    StandaloneCoroutine(newContext, active = true)

两种协程实现:

  1. LazyStandaloneCoroutine
    用于LAZY模式,协程体不会立即执行,直到调用start()join()

  2. StandaloneCoroutine
    用于其他模式,协程体会立即调度执行。

4.协程的真正启动:coroutine.start()

start()方法是协程执行的核心入口,源码如下:

public fun <R, T> CoroutineStart.invoke(coroutine: R, block: suspend R.() -> T) {
    when (this) {
        DEFAULT -> coroutine.startCoroutineCancellable(block, coroutine)
        ATOMIC -> coroutine.startCoroutine(block, coroutine)
        UNDISPATCHED -> coroutine.startCoroutineUndispatched(block, coroutine)
        LAZY -> Unit // 懒启动模式下不执行,等待用户显式调用start()
    }
}

不同启动模式的差异:

  1. DEFAULT模式
    调用startCoroutineCancellable(),允许在调度前取消协程:

    public fun <T> (suspend () -> T).startCoroutineCancellable(
        completion: Continuation<T>
    ) {
        createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
    }
    
  2. ATOMIC模式
    调用startCoroutine(),不允许在调度前取消:

    public actual fun <T> (suspend () -> T).startCoroutine(
        completion: Continuation<T>
    ) {
        createCoroutineUnintercepted(completion).intercepted().resumeWith(Result.success(Unit))
    }
    
  3. UNDISPATCHED模式
    调用startCoroutineUndispatched(),直接在当前线程执行直到第一个挂起点:

    internal fun <R, T> (suspend R.() -> T).startCoroutineUndispatched(
        receiver: R,
        completion: Continuation<T>
    ) {
        val continuation = createCoroutineUnintercepted(receiver, completion)
        val context = continuation.context
        val uCont = continuation.intercepted()
        if (context[ContinuationInterceptor] == Dispatchers.Unconfined) {
            // 无限制调度器:直接在当前线程执行
            uCont.resumeWith(Result.success(Unit))
        } else {
            // 其他调度器:先切换到指定线程再执行
            with(context) {
                uCont.resumeWith(Result.success(Unit))
            }
        }
    }
    

5.async与launch的源码核心差异

asynclaunch的源码结构相似,但有两个关键区别:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    // 创建一个携带结果的协程(继承自Job,但多了结果缓存和await()方法)
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block)
    else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

差异点:

  1. 返回值类型

    • launch返回Job(只能控制生命周期,无法获取结果)
    • async返回Deferred<T>(继承自Job,但可通过await()获取结果)
  2. 结果缓存
    DeferredCoroutine内部维护了一个Result<T>缓存,确保await()可重复获取结果,且支持多协程并发等待。

阶段总结

        Kotlin 协程的启动依赖于协程构建器和协程上下文,其中CoroutineScope.launch作为入口函数,通过合并当前作用域上下文与用户传入的上下文创建新的协程上下文,若未显式指定 Job 则自动添加以确保结构化并发;

        根据启动模式(如 DEFAULT 立即调度、LAZY 懒启动、ATOMIC 立即执行但调度前不可取消、UNDISPATCHED 立即在当前线程执行至首个挂起点)创建不同的协程实例(如 LazyStandaloneCoroutine 或 StandaloneCoroutine),并通过coroutine.start()触发协程执行,不同启动模式对应不同的执行策略;

        asynclaunch源码结构相似,但async返回继承自 Job 的 Deferred<T>类型,可通过 await () 获取结果,而launch仅返回 Job 用于控制生命周期。


网站公告

今日签到

点亮在社区的每一天
去签到