Kotlin 协程 基础概念&原理分析

本文是适合协程初学者的入门介绍,但乍看题目可能会觉得有点奇怪,怎么除了基本概念,还在入门文章中提及了深奥的原理?作为小白,应该要先接触基础概念,等熟悉后再进行原理分析才对。
本文的目标也是如此,让初学者能快速理解概念。以我自身的学习经验,单纯理解协程的挂起,恢复等概念还不如直接深入分析代码实现理解的快。当然,为了减少复杂度,本文只讲几个最最基本的概念,以及通过拆解一个最简单的demo来理解协程的自主挂起,恢复概念。
通过阅读本文,你能收获:

  1. 只能知道suspend关键字,协程最简单的创建流程以及启动和挂起协程
  2. 能完全理解协程基础原理(不夸张,因为原理真的很简单)

1. 协程是什么

协程(Coroutine)是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复,与多线程一样是一种异步编程的方式。 但与多线程依赖系统调度不同,协程由程序自主决定挂起与执行(个人觉得理解自主决定挂起与恢复是理解koltin协程的关键,后面会通过demo进行源码分析)

2. 协程怎么编程

如果你是初学者,看了协程概念后大概率也是一头雾水,没关系,我们先从koltin 协程的基础语法学起,对协程编程有个概念

A. suspend 关键字

suspend用于暂停执行当前协程,并保存所有局部变量。如需调用suspend函数,只能从其他suspend函数进行调用,或通过使用协程构建器(例如 launch)来启动新的协程。

Emmm。。看了等于没看。。
Read The Fucking Source Code --Linus Torvalds

下面是suspend使用的最简单的例子:

suspend fun test1(){
    println("test1")
}

反编译后可以看到其实就是一个普通函数,它接受一个Continuation类型的参数,所以suspend关键字可以理解是一种简化写法的语法糖

@Nullable
public static final Object test1(@NotNull Continuation $completion) {
   String var1 = "test1";
   System.out.println(var1);
   return Unit.INSTANCE;
}
Redundant suspend modeifier

有时候在使用suspend关键字时会提示以下错误,从反编译后的代码上不难看出,其实就是Continuation参数并没被使用,所以可以去掉suspend关键字
在这里插入图片描述

B. 创建协程

下面是kotlin创建一个协程并运行的最简demo。首先,赋值运算符左边定义了一个continuation变量,它是Continuation类型,右边语法比较复杂,我们分成三块逐步分析:

  1. 第一部分是一个由suspend 修饰的lambda表达式
  2. 第二部分是这个lambda表达式调用了createCoroutine函数。这里理解的关键是要理解在kotlin中,所有类型都是对象,所以lambda也是一个对象,lambda.createCoroutine 其实就是调用了一个对象的成员方法。(后面会分析此函数)
  3. 第三部分是调用createCoroutine 时传入的参数,它是一个继承于Continuation 的object对象。

把三部分连接起来,阐述的语义是:当协程启动时,会先调用第一部分的lambda执行体,执行完成后调用第三部分的obejct对象的resumeWith方法(还记得协程是异步的实现方式吗,对比多线程编程,应该可以很容易理解第一部分就是需要异步执行的内容,object对象是执行完成后的callback)
demo的最后是 continuation.resume(Unit),表示启动协程

fun main(){
    // 1. 创建cotinuation对象
    val continuation: Continuation<Unit> = suspend {
        println("hello continuation")
    }.createCoroutine(object : Continuation<Unit>{
        override val context: CoroutineContext
            get() = EmptyCoroutineContext

        override fun resumeWith(result: Result<Unit>) {
            println("end conginuation")
        }
    })
    
    // 2. 运行
    continuation.resume(Unit);
}

C. 通过调用 suspendCoroutine主动挂起协程

suspendCoroutine会自动捕获当前的执行环境(如临时变量, 参数等), 然后存放到一个Continuation中, 并且作为参数传给它的lambda.

先不管上面的介绍,反正我们知道通过调用suspendCoroutine,能中断协程的执行。来一段demo深度理解下:

lateinit var continuation:Continuation<Unit>

suspend fun yield() {
    suspendCoroutine<Unit> {
        continuation = it;
    }
}

fun main(){
    // 1. 创建cotinuation对象
    val continuation: Continuation<Unit> = suspend {
        println("hello continuation,part 1")
        yield();
        println("hello continuation,part 2")
    }.createCoroutine(object : Continuation<Unit>{
        override val context: CoroutineContext
            get() = EmptyCoroutineContext

        override fun resumeWith(result: Result<Unit>) {
            println("end conginuation")
        }
    })
    
    println("开始运行协程")
    // 2. 运行 
    continuation.resume(Unit);
    println("surprised")
}

上面程序运行结果如下。可能现在你会对输出结果感到奇怪,但正如suspendCoroutine 函数的说明,它能中断协程的执行。所以当调用continuation.resume函数执行协程时,会执行lambda里面的内容,首先打印 “hello continuation,part 1” ,然后调用yield()函数,因为它里面调用了suspendCoroutine,中止了协程的执行,即lambda的执行,所以回到continuation.resume的下一行开始继续执行,即打印"surprised"。

开始运行协程
hello continuation,part 1
surprised

是不是跟我一样惊讶于这种运行结果?好奇它底层是如何实现的? 是依靠系统调度?别急,答案马上揭晓

3. 原理分析

A. createCoroutine 函数分析

下面是createCoroutine函数的实现,读懂需要有一定的koltin语法基础。
代码很清晰地告诉我们createCoroutine是一个函数对象的扩展函数,这个函数对象是 suspend ()-> T 类型,调用createCoroutine后会返回一个SafeContinuation类型。

public fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
): Continuation<Unit> =
    SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)

B. createCoroutineUnintercepted 函数分析

构造SafeContinuation对象前先调用了createCoroutineUnintercepted函数,在看代码之前,我们先对completion参数有一个明确概念,它是一个continuation对象,对于 创建协程 部分的demo来说,它就是第三部分的object对象,如果忘记了,可以再重新返回看一遍。
有了上述前提后,我们继续看代码具体阐述了什么内容。 为了简单分析,我们分析else分支的实现,这里调用了createCoroutineFromSuspendFunction函数,并传入两个参数,第一个就是completion,比较清晰。第二个是一个lambda表达式:{(this as Function1<Continuation, Any?>).invoke(it)},理解这一句的关键是理解this指针,因为createCoroutineUnintercepted也是一个对象的扩展函数,所以this肯定是指代了此对象的实例,对于 创建协程 部分的demo来说,它就是第一部分创建的lambda表达式,invoke函数就是执行这个lambda表达式。

public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
): Continuation<Unit> {
    // probeCoroutineCreated 直接返回 completion
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
        create(probeCompletion)
    else
        createCoroutineFromSuspendFunction(probeCompletion) {
            // 因为createCoroutineFromSuspendFunction是扩展函数,this指
            // suspend { println("hello continuation") } 
            (this as Function1<Continuation<T>, Any?>).invoke(it)
        }
}

接下来看createCoroutineFromSuspendFunction函数,还是看else分支,它返回了一个ContinuationImpl object,并且继承了invokeSuspend方法,最重要的是,这个方法里面调用了block(this) (block是(this as Function1<Continuation, Any?>).invoke(it)
看到这里,是不是抓住了什么? 没错,只要我们通过调用ContinuationImpl.invokeSuspend()函数,就能执行demo第一部分定义的lambda表达式,即执行 println(“hello continuation”) 语句
那么进一步,运行协程是通过调用
continuation.resume(Unit)执行,那它肯定是调用到了ContinuationImpl.invokeSuspend()
。事实上也确实是这样,后面也会分析

@SinceKotlin("1.3")
private inline fun <T> createCoroutineFromSuspendFunction(
    completion: Continuation<T>,
    crossinline block: (Continuation<T>) -> Any?
): Continuation<Unit> {
    val context = completion.context
    // label == 0 when coroutine is not started yet (initially) or label == 1 when it was
    return if (context === EmptyCoroutineContext)
        // ......
    else
        object : ContinuationImpl(completion as Continuation<Any?>, context) {
            private var label = 0

            override fun invokeSuspend(result: Result<Any?>): Any? =
                when (label) {
                    0 -> {
                        label = 1
                        result.getOrThrow() // Rethrow exception if trying to start with exception (will be caught by BaseContinuationImpl.resumeWith
                        block(this) // run the block, may return or suspend
                    }
                    1 -> {
                        label = 2
                        result.getOrThrow() // this is the result if the block had suspended
                    }
                    else -> error("This coroutine had already completed")
                }
        }
}

C. ContinuationImpl 类分析

下面是ContinuationImpl的具体实现,没啥重要的内容,最重要的invokeSuspend我们已经分析过了。

// 继承链: ContinuationImpl -> BaseContinuationImpl -> Continuation<Any?>

internal abstract class ContinuationImpl(
    completion: Continuation<Any?>?,
    private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
    constructor(completion: Continuation<Any?>?) : this(completion, completion?.context)

    public override val context: CoroutineContext
        get() = _context!!

    @Transient
    private var intercepted: Continuation<Any?>? = null

    public fun intercepted(): Continuation<Any?> =
        intercepted
            ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }

    protected override fun releaseIntercepted() {
        val intercepted = intercepted
        if (intercepted != null && intercepted !== this) {
            context[ContinuationInterceptor]!!.releaseInterceptedContinuation(intercepted)
        }
        this.intercepted = CompletedContinuation // just in case
    }
}

D. BaseContinuationImpl 类分析

下面是BaseContinuationImpl的源码,里面最关键的是resumeWith 函数的实现逻辑,其他细节我们可以忽略,只需要关注主要流程:

  1. 在try块里调用了invokeSuspend(xx)函数
  2. 函数的最后调用了completion.resumeWith(outcome)
    现在应该更能理解 创建协程 的三个部分了吧?通过invokeSuspend(xx)函数调用第一部分执行,第一部分执行完成后会调用第三部分,即completion的resumeWith() 函数,所以这就是为什么说completion是callback的原因。
@SinceKotlin("1.3")
internal abstract class BaseContinuationImpl(
    public final override fun resumeWith(result: Result<Any?>) {
        // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
        var current = this
        var param = result
        while (true) {
            // Invoke "resume" debug probe on every resumed continuation, so that a debugging library infrastructure
            // can precisely track what part of suspended callstack was already resumed
            probeCoroutineResumed(current)
            with(current) {
                val completion = completion!! // fail fast when trying to resume continuation without completion
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param)
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted() // this state machine instance is terminating
                if (completion is BaseContinuationImpl) {
                    // unrolling recursion via loop
                    current = completion
                    param = outcome
                } else {
                    // top-level completion reached -- invoke and return
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }
}

E. SafeContinuation 类分析

调用createCoroutine 函数最终返回SafeContinuation对象,此对象的构造语句如下: SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
上面已经分析了createCoroutineUnintercepted(completion)返回了一个ContinuationImpl对象,后面的.intercepted()是拦截器的实现,我们先不关心,可以直接理解成SafeContinuation(ContinuationImpl,COROUTINE_SUSPENDED)safeContinuation对象最重要的一环是重新实现了resumeWith函数,在这个函数里调用了delegate.resumeWith(result),即ContinuationImpl.resumeWith(result)ContinuationImpl又继承于 BaseContinuationImpl,固调用的是BaseContinuationImpl.resumeWith() 函数,再往后的的流程在BaseContinuationImpl章节已详细描述。

internal actual class SafeContinuation<in T>
internal actual constructor(
    private val delegate: Continuation<T>,
    initialResult: Any?
) : Continuation<T>, CoroutineStackFrame {

    public actual override fun resumeWith(result: Result<T>) {
        while (true) { // lock-free loop
            val cur = this.result // atomic read
            when {
                cur === UNDECIDED -> if (RESULT.compareAndSet(this, UNDECIDED, result.value)) return
                cur === COROUTINE_SUSPENDED -> if (RESULT.compareAndSet(this, COROUTINE_SUSPENDED, RESUMED)) {
                    delegate.resumeWith(result)
                    return
                }
                else -> throw IllegalStateException("Already resumed")
            }
        }
    }
}

F. continuation.resume(Unit) 方法分析

通过continuation的resume函数可以执行一个协程,看一下它的定义

public inline fun <T> Continuation<T>.resume(value: T): Unit 
    = resumeWith(Result.success(value))

其实很清晰了,调用resume() 后会调用 resumeWith函数,最终会调用invokeSuspend() 函数,从而执行 创建协程 demo中的第一部分,执行println(“hello continuation”)

调用链如下:

  1. continuation.resume(Unit);
  2. SafeContinuation.resumeWith()
  3. ContinuationImpl::BaseContinuationImpl.resumeWith()
  4. ContinuationImpl.invokeSuspend --> 执行协程体
  5. completion.resumeWith(outcome) --> 调用返回函数

4. 以demo分析协程原理

上面章节讲述了调用continuation.resume(Unit)是如何调用到lambda执行体的过程,但并未描述挂起函数suspendCoroutine的实现,本节通过分析一个最简单的demo,能让你很好地理解协程自主挂起,恢复的原理,以及suspendCoroutine函数具体做了什么。(如果上面章节没看懂的话,没有关系,但下面的章节需要详细阅读)

A. demo初探

demo代码:

lateinit var continuation:Continuation<Unit>

suspend fun yield() {
    suspendCoroutine<Unit> {
        continuation = it;
    }
}

fun main(){
    var temp:Int = -1;

    continuation = suspend {
        for (i in 0..5) {
            println("挂起") // ②
            temp = i;
            yield()
            println("恢复执行") // ④
        }
    }.createCoroutine(object: Continuation<Unit>{
        override val context: CoroutineContext
            get() = EmptyCoroutineContext

        override fun resumeWith(result: Result<Unit>) {
            println("End generator")
        }
    });


    for(i in 0..5) {
        continuation.resume(Unit); // ①
        println("从协程体退出," + temp) // ③
        println("\n")
    }

    continuation.resume(Unit);

}

上面的代码仿造了python genertor生成器的实现,点击运行后,输出结果如下:

挂起
从协程体退出,0

恢复执行
挂起
从协程体退出,1

恢复执行
挂起
从协程体退出,2

恢复执行
挂起
从协程体退出,3

恢复执行
挂起
从协程体退出,4

恢复执行
挂起
从协程体退出,5

恢复执行
End generator

简单分析下执行过程,先对过程有个了解,方便往后继续看原理。

  1. ①处执行了continuation.resume() 函数后,执行lambda表达式②(为了方便,下面用协程体指这一部分)。 首先进入for循环,输出"挂起",接下来调用yield() 函数
  2. 调用yield() 函数后从协程体退出,返回③处继续执行,打印输出
  3. 之后又调用continuation.resume()从①处继续执行协程体,此时会从上次中断的位置往后执行,即④处输出"恢复执行"
  4. 重复上述流程

B. 分析demo

将kotlin 代码反编译如下(省略了一些细节代码,我们只关心主要流程)

// invokeSuspend是重点,看不懂的话多看几遍
public final Object invokeSuspend(@NotNull Object $result) {
   Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
   int i;
   int var3;
   String var4;
   switch (this.label) {
      case 0:
         ResultKt.throwOnFailure($result);
         i = 0;
         var3 = 5;
         break;
      case 1:
         var3 = this.I$1;
         i = this.I$0;
         ResultKt.throwOnFailure($result);
         var4 = "恢复执行";
         System.out.println(var4);
         ++i;
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
   }

   while(i <= var3) {
      var4 = "挂起";
      System.out.println(var4);
      temp.element = i;
      this.I$0 = i;
      this.I$1 = var3;
      this.label = 1;
      if (GeneratorKt.yield(this) == var5) {
         return var5;
      }

      var4 = "恢复执行";
      System.out.println(var4);
      ++i;
   }

   return Unit.INSTANCE;
}

@Nullable
public static final Object yield(@NotNull Continuation $completion) {
   SafeContinuation var2 = new SafeContinuation(IntrinsicsKt.intercepted($completion));
   Continuation it = (Continuation)var2;
   int var4 = false;
   continuation = it;
   Object var10000 = var2.getOrThrow();
   if (var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
      DebugProbesKt.probeCoroutineSuspended($completion);
   }

   return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}

其实看到代码时已经豁然开朗,醍醐灌顶… 这不就一个状态机吗

来慢慢拆解下,我们知道调用continuation.resume()最终会调用到一个invokeSuspend函数,没错,就是上述代码里的invokeSuspend函数,它能被多次调用到。下面根据demo来理解下代码:

  1. 在demo的第一次for循环中调用continuation.resume()①执行了协程体,即调用了invokeSuspend函数,此时第一次调用,label为0,固走了case 0 的分支,初始化了for循环的变量。 之后进入while语句块,先输出"恢复执行",将label设为1(这里是第二次运行的关键),然后调用了GeneratorKt.yield(this)函数,即调用了suspendCoroutine函数,这个函数没有具体的实现,好像是依赖具体平台实现,我没找到实现代码,有兴趣的可以关注下。 反正我们知道suspendCoroutine函数会返回COROUTINE_SUSPENDED,所以反编译后的yield函数会返回COROUTINE_SUSPENDED。回到while语句块,它会判断yield返回值是否是COROUTINE_SUSPENDED,如果是则直接return,如果不是继续往下执行
  2. 因为yield函数返回COROUTINE_SUSPENDED,所以走了return的逻辑,从continuation.resume()的调用中返回,并从它的下一句代码③开始执行,所以接下来执行println(“从协程体退出,” + temp)
  3. 第二次for循环又调用continuation.resume()①,进入invokeSuspend函数,此时lable为1,所以走了case 1 的分支,输出了"恢复执行",之后根据this中的成员变量初始化临时变量,继续往下执行,进入while语句块,这里跟1流程一样
  4. 重复上述过程

总结

通过上面的分析,kotlin协程在我们面前已经毫无秘密。它并不复杂,只是一个上层框架,里面封装了一个状态机,后续会通过state多次调用同个函数,即invokeSuspend函数,当然,在每次调用后都会保存一些所需变量,在下次调用时还原现场。

invokeSuspend函数由编译器根据suspend ()->T 实例的实际情况自动生成多个case分支,suspendCoroutine函数为我们提供了在invokeSuspend函数中直接return,中断协程体继续执行的手段,当然,在中断前会通过成员变量保存上次运行的现场,在下次调用continuation.resume() 时能在中断的位置继续执行.

概念回顾

理解了原理,我们再回头看下各种概念的定义:

  1. 协程:协程的概念最核心的点就是函数或者一段程序能够被挂起,稍后再在挂起的位置恢复。挂起和恢复是开发者的程序逻辑自己控制的
  2. 协程挂起:在协程体,即协程代码的执行中插入一条return语句,当然,在return前通过对象的成员变量,如this.I$0,this.I$2… 对变量进行保存,方便下次执行时恢复现场
  3. 协程恢复:重新调用invokeSuspend函数,先恢复现场,并根据上次运行的结果确定case 分支,继续往下执行
  4. 自主挂起与恢复:可以看到程序一直在单线程运行,挂起与恢复的概念并不像多线程一样是系统调度的概念,完全是通过状态机,即switch(label)实现,第一次调用时label状态为0,之后lable状态为1,所以是通过编程实现,固说程序具有完全的自主控制权
  5. 协程是否像多线程一样有自己的栈空间和local 变量?
    没有,协程就是普通函数和对象。只不过这个函数具备被挂起和恢复执行的能力。如何提供这种能力?在kotlin中依靠状态机和switch语句块实现。
  6. 协程挂起后恢复执行时做了什么? 有无调用栈恢复,即如何保存上次运行现场?
    从分析demo的章节中,我们可以看到创建协程实际上是创建了一个对象,这个对象有一个suspendInvoke方法,并且有swicth(label)语句块,通过状态label区分多次调用suspendInvoke的逻辑,在挂起协程时,会通过this.I$0,this.I$2… 对变量进行保存,下次调用时恢复。所以保存现场是通过类成员变量的方式保存
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Kotlin 协程中的 delay 函数是基于线程抢占实现的,它会暂停当前协程的执行,并将当前协程的执行权让给其他协程。当 delay 的时间到了之后,调度器会再次调度这个协程,并将其加入执行队列。 举个例子,假设有一个协程 A,它调用了 delay(1000L),表示暂停 1 秒钟。当 A 协程调用 delay 时,调度器会将 A 协程的执行权让给其他协程,并将 A 协程加入等待队列中。1 秒钟之后,调度器会从等待队列中取出 A 协程,并将其加入执行队列中。 总的来说,delay 函数的原理就是通过线程抢占来实现协程的暂停和唤醒的。 ### 回答2: Kotlin协程中的delay函数是一个挂起函数,它可以让协程暂停一段指定的时间,然后再继续执行。delay函数的原理是利用了协程的挂起和恢复机制。 当我们在协程中调用delay函数时,协程会通过将自己的状态保存起来,然后释放执行线程。在指定的时间间隔过后,协程会被恢复,并继续执行。这样,我们可以实现在协程中暂停一段时间而不会阻塞线程的效果。 具体实现上,delay函数内部使用了定时器来实现暂停的功能。它会创建一个定时任务,在指定的时间间隔过后触发恢复协程的操作。当协程被恢复后,它会继续从上一次暂停的地方开始执行。 需要注意的是,delay函数只能在协程中使用,而不能在普通的线程中使用。这是因为协程的挂起和恢复功能是基于协程的调度器来实现的,而普通的线程并没有这样的机制。 总之,delay函数是Kotlin协程中用来控制协程的暂停和恢复的重要函数之一。它通过利用协程的挂起和恢复机制,以及定时器实现了在协程中暂停一段时间的效果,从而提高了并发编程的效率和易用性。 ### 回答3: Kotlin协程是一种轻量级的并发编程框架,而`delay`是其中一种常用的协程构造器。`delay`的原理是通过暂停当前协程一段时间来模拟延迟操作。 在Kotlin中,协程是基于挂起函数(Suspending Function)实现的。挂起函数是指可以中断执行,并在某个时间点继续执行的函数。`delay`函数就是一个挂起函数,它的作用是中断当前协程的执行,并在指定的延迟时间后继续执行。 具体实现原理如下: 1. 当调用`delay`时,会创建一个`Delay`对象,用于管理当前协程的延迟操作; 2. `Delay`对象会将延迟时间记录下来,并创建一个新的挂起点(Suspend Point); 3. 当前协程在遇到挂起点时,会中断执行并将控制权交给`Delay`对象; 4. `Delay`对象会通过底层的调度机制,将当前协程从线程池中的工作线程中切换到等待队列中; 5. 等待队列中的协程会按照延迟时间的顺序排列,当延迟时间到达时,协程会从等待队列中出队,被重新放回工作线程中执行; 6. 当协程继续执行时,`delay`函数返回,并可以继续执行下面的代码。 总而言之,`delay`函数利用挂起函数的特性,将当前协程暂停一段时间,然后再继续执行。通过这种方式,可以方便地实现协程的延迟操作,而无需阻塞线程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值