Kotlin 协程学习

走进协程的世界

记录学习,持续更新。

相关知识,在代码注释中 进行了具体说明和个人见解。学习阶段,可能不太准确,仅供参考~

  • 协程基础了解
  • 协程作用域构建器
  • 协程结构化并发
  • 协程的生命周期
  • 协程的取消
  • 协程上下文
  • 协程的异常捕捉
package com.corroutine

import android.os.Bundle
import android.os.Environment
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.FileReader
import kotlin.coroutines.*
import kotlin.system.measureTimeMillis

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        corroutine()
    }

    /**
     *协程让异步逻辑同步化,杜绝回调地狱
     *
     * 协程实现分为两个层次:
     * 基础设施层,标准库的协程API kolin包下面的
     * 业务框架层,协程的上层框架支持 kotlinx包下面的 eg. GlobalScope  delay(100)
     */
    fun corroutine() {
        //GlobalScope:顶级协程
        //launch:协程构建器
        //Dispatchers:协程调度器   Dispatchers.Main、 Dispatchers.Default(非主线程) 、Dispatchers.IO(非主线程) 不指定调度器的时候 默认是Dispatchers.Default
        GlobalScope.launch(Dispatchers.Main) {


            /**
             *协程内部调用的必须是挂起函数 suspend
             *
             * 理解挂起和阻塞的区别
             */
            delay(100)
            val a = getUser()
            Log.d("fct", a)
        }
    }

    private suspend fun getUser() = withContext(Dispatchers.IO) {
        "1111"
    }

    /**
     *使用基础设施层创建协程 原生API
     * CoroutineContext:协程上下文
     */
    fun 原生api创建协程() {

        //创建协程
        val continuation = suspend {
            //协程体
            5
        }.createCoroutine(object : Continuation<Int> {
            override val context: CoroutineContext
                get() = EmptyCoroutineContext

            override fun resumeWith(result: Result<Int>) {
                println(result)
            }
        })
        //启动协程
        continuation.resume(Unit)
    }

    /**
     * 协程结构化并发:就是有个管理员可以统一管理这些协程,管理员可以取消协程、追踪协程、以及协程失败时的异常监听处理。
     * 这个管理员就是协程作用域CoroutineScope
     * 带有默认协程作用域的有:
     * GlobalScope:生命周期application级别的,进程级别的
     * MainScope:生命周期activity级别的 。可以通过实现 CoroutineScope by MainScope() 使用
     * viewModelScope:生命周期绑定viewModel
     * lifecycleScope:生命周期绑定viewModellifecycle
     *
     */
    val mainScope = MainScope()
    fun 结构化并发() {
        mainScope.launch {
            try {
                //如果在delay 10秒的过程中取消 mainScope.cancel() 会报异常。
                //delay 函数是个挂起函数,相当于子协程,取消协程 对子协程的影响,以及一个子协程取消了,其它子协程会怎么样?
                delay(10000)
            } catch (e: Exception) {
                e.printStackTrace()
            }

        }
    }

    override fun onDestroy() {
        super.onDestroy()
        mainScope.cancel()
    }

    /**
     *协程构建器:
     * 1、launch
     * 2、async
     */
    fun 协程构建器() = runBlocking {//插曲:runBlocking可以把当前主线程 变成协程 是阻塞得,等待子协程执行完毕 ,才会退出

        val job1 = launch {
            println("job1 finished")
        }

        val job2 = async {
            println("job2 finished")
            "job2 result"
        }

        //await是可以拿到协程返回值得"job2 result"
        println(job2.await())

        //打印结果 :job1 finished  job2 finished  job2 result
    }

    fun 协程得join和await方法的等待协程作业() = runBlocking {
        val job1 = launch {
            println("job1 finished")
            delay(2000)
        }
        //join方法,只有job1执行完毕,才会执行job2和job3子协程
        job1.join()
        val job2 = launch {
            println("job1 finished")
            delay(1000)
        }
        val job3 = launch {
            println("job1 finished")
            delay(1000)
        }


        //==============awiat
        val jobd1 = async {
            println("job1 finished")
            delay(2000)
        }
        //await方法(await可以拿到协程执行结果),jobd1,才会执行jobd2和jobd3子协程
        jobd1.await()
        val jobd2 = async {
            println("job1 finished")
            delay(1000)
        }
        val jobd3 = async {
            println("job1 finished")
            delay(1000)
        }
    }

    fun async组合并发() = runBlocking {
        val times = measureTimeMillis {
            val one = doOne()
            val two = doTwo()
            println("计算结果:${one + two}")
        }
        println("执行耗时:$times")
        //执行耗时:2秒


        //async组合并发 写法
        val times2 = measureTimeMillis {
            val job1 = async { doOne() }
            val job2 = async { doTwo() }
            println("计算结果:${job1.await() + job2.await()}")
        }
        println("执行耗时:$times")
        //执行耗时:1秒
    }

    private suspend fun doOne(): Int {
        delay(1000)
        return 10
    }

    private suspend fun doTwo(): Int {
        delay(1000)
        return 12
    }

    /**
     *协程的启动模式
     *>DEFAULT:协程创建后,立即开始调度,在调度前如果协程被取消,其将直接进入取消
     *响应的状态。
     *>ATOMIC:协程创建后,立即开始调度,协程执行到第一个挂起点之前不响应取消。
     *>LAZY:只有协程被需要时,包括主动调用协程的start、join或者await等函数时才会开始
     *调度,如果调度前就被取消,
     *那么该协程将直接进入异常结束状态。
     *>UNDISPATCHED:协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起
     *的点。
     *
     *
     * 这里调度 和执行的逻辑,有点想消息队列
     * 调度是把 这个协程加入到队列里 等待执行
     * 所以立即调度和立即执行 不是一个意识
     *
     */
    suspend fun 协程启动模式() = runBlocking {

        val job1 = launch(start = CoroutineStart.DEFAULT) {
            delay(10000)
            println("job1")
        }
        delay(1000)
        job1.cancel()


        val job2 = launch(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
            //如果使用UNDISPATCHED启动模式,下面会打印的是主线程 因为当前函数栈是主线程
            //因为UNDISPATCHED模式:UNDISPATCHED:协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点
            println(Thread.currentThread().name)
        }
        job2.join()
    }


    /**
     *作用域构建器:coroutineScope 只要当前作用域内 子协程 有一个发生异常了,所有协程都会被取消
     *作用域构建器:supervisorScope 作用域内不会因为某个子协程的失败,而取消其它兄弟 协程
     * coroutineScope是创建一个非堵塞的协程作用域,而 runBlocking是阻塞。
     *
     */
    suspend fun 作用域构建器() = runBlocking {
        coroutineScope {// =协程作用域构建器
            val job1 = async {
                delay(400)
                println("job1..")
            }
            val job2 = async {
                delay(200)
                println("job2..")
                throw IllegalAccessException()
            }
        }

        supervisorScope {
            val job1 = async {
                delay(200)
                println("job1..")
            }
            val job2 = async {
                delay(200)
                println("job2..")
                throw IllegalAccessException()
            }
        }
    }

    /**
     * Job对象
     *>对于每一个创建的协程(通过launch或者async),会返回一个Job实例,该实例是协程
     *的唯一标示,并且负责管理协程的生命周期。
     *>一个任务可以包含一系列状态:新创建(New)、活跃(Active)、完成中(Completi
     *ng)、已完成(Completed)、取消中(Cancelling)和已取消(Cancelled)。虽然
     *我们无法直接访问这些状态,但是我们可以访问)ob的属性:isActive、isCancelled和is
     *Completed
     */
    suspend fun job生命周期() = runBlocking {

        val job = launch {
            delay(10000)
        }

        job.isActive
        job.isCancelled
        job.isCompleted
        job.cancel()
    }

    /**
     *
     * 协程的取消
     *>取消作用域会取消它的子协程。
     *CANCEL
     *>被取消的子协程并不会影响其余兄弟协程。
     *>协程通过抛出一个特殊的异常CancellationException来处理取消操作。
     *>所有kotlinx.coroutines中的挂起函数(withContext、delay等)都是可取消的。
     *
     *===================================================
     * 拓展知识:CoroutineScope和小写coroutineScope得区别,都是构建了协程作用域,用于结构化并发管理协程
     * 但是coroutineScope更想是上层封装好得api,这个协程作用域构建器继承了父协程构建器得协程作用域
     * 而CoroutineScope构建得协程作用域是自己创建得
     *
     *所以当runblocking 里不加入delay得时候,上面得两个launch无法得到执行
     * 因为他们得协程作用域不同,runBlocking本身又是阻塞得,子协程执行完了,也就结束了。
     * 但是CoroutineScope创建得两个子协程并不是runblocking得,
     * 所以runblocking就认为没有需要执行得协程,没有继续阻塞,直接结束当前方法。
     * 所以只有delay一下,给CoroutineScope得协程执行得时间。
     */
    suspend fun 取消协程_取消作用域() = runBlocking<Unit> {
        val scope = CoroutineScope(Dispatchers.Default)
        scope.launch {
            delay(1000)
            println("job 1")
        }
        scope.launch {
            delay(1000)
            println("job 2")
        }
        delay(100)
        scope.cancel()//取消携程作用域 会取消子协程
        delay(2000)
    }


    suspend fun 取消协程_被取消得协程并不会影响兄弟协程() = runBlocking<Unit> {
        val scope = CoroutineScope(Dispatchers.Default)
        val job1 = scope.launch {
            delay(1000)
            println("job 1")
        }
        val job2 = scope.launch {
            delay(1000)
            println("job 2")
        }
        delay(100)
        job1.cancel()//取消携子协程job1,不会影响子协程job2
        delay(2000)
    }

    /**
     * 拓展知识:
     * 协程取消cancel的时候,会抛出一个CancellationException异常。如果不去捕捉,程序也不会奔溃,会被
     * 静默处理了。
     */
    suspend fun 取消协程_取消协程抛出的异常() = runBlocking<Unit> {
        val job = GlobalScope.launch {
            try {
                delay(10000)
                println("job...")
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
//        job.cancel(CancellationException("asd")) //cancel也可以指定异常
        job.cancelAndJoin()
    }

    /**
     *CPU密集性任务是无法使用cancel方法取消的
     *
     *
     * 第一种:isActive 标志位 判断
     * 但是可以利用cancel之后,生命周期的状态改变,就行判断
     *可以通过协程得生命周期属性 isActive 来进行判断 是否继续执行协程里得代码。
     *
     * 第二种:ensureActive() 使用这种方式会抛个异常,也会被静默处理掉
     * 第三种:yield() 出让协程执行权
     */
    suspend fun CPU密集性任务取消() = runBlocking {

        val startTime = System.currentTimeMillis()
        val job = launch(Dispatchers.IO) {
            var nextPrintTime = startTime
            var i = 0

            //第一种:
            while (i < 5 && isActive) {
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500
                }
            }

            //第二种:
            while (i < 5) {
                ensureActive()
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500
                }
            }

            //第三种:
            while (i < 5) {
                yield()
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500
                }
            }
        }

        delay(1300)
        println("main I'm tried of wating!")
        job.cancelAndJoin()
        println("main: Now I can quit.")
    }

    /**
     * 在取消协程的时候,可能会导致一些资源没有被释放。
     * 可以通过try catch finally进行释放
     *
     * 如果是io流操作 可以用use函数操作 use函数会自动关闭IO流
     */
    suspend fun 协程取消的副作用() = runBlocking {

        //第一种:
        val br = BufferedReader(
            FileReader(
                Environment.getExternalStorageDirectory().toString() + "/a.txt"
            )
        )
        with(br) {
            var line: String?
            try {
                while (true) {
                    line = readLine() ?: break
                    println(line)
                }
            } finally {
                close()
            }
        }

        //第二种:
        br.use {
            var line: String?
            while (true) {
                line = it.readLine() ?: break
                println(line)
            }
        }
    }

    /**
     * 如果协程被取消了,里面还有子协程,我们不想取消子协程。那么可以使用
     * withContext(NonCancellable){}
     */
    suspend fun 不能被取消的协程任务() = runBlocking {

        val job = launch {

            //下面就是常驻任务,不能因为调用cancel而被取消
            withContext(NonCancellable) {

            }

        }

        job.cancelAndJoin()

    }


    /**
     * 协程内部如果有超时任务,会报异常
     * 可以使用withTimeoutOrNull
     *
     * 使用场景,执行一个耗时任务,如果超时了,我们就取消任务,并且返回一个值
     */
    suspend fun 超时任务() = runBlocking {

        val result = withTimeoutOrNull(1300) {
            repeat(1000) {
                delay(500)
            }
        } ?: "超时了"

        println("$result")
    }

    /**
     * CoroutineContext 是一组用于定义协程行为的元素。由以下结构组成:
     * Job:控制协程的生命周期
     * CoroutineDispatcher:向合适的线程分发任务
     * CoroutineName:协程的名称,调试的时候有用
     * CoroutineExceptionHandler:处理未被捕获的异常
     *
     * 可以用 加号( + ) 进行组合 得到CoroutineContext
     */
    suspend fun 协程上下文() = runBlocking {
        val job = launch(Job() + Dispatchers.Default + CoroutineName("asd")) {
            println("")
        }
    }

    /**
     *对于新创建的协程,它的CorountineContext会包含一个全新的Job实列,它会帮助
     * 我们控制协程的生命周期。而剩余的元素会从CorountineContext的父类继承,该父类可能
     * 是另外一个协程或者创建该协程的CorountineScope(协程作用域)。
     *
     * 所以job1打印的coroutineContext[Job]  job对象 和job1_child的job对象是不一样的
     * 但是CoroutineName是一样的
     */
    suspend fun 协程上下文的继承() = runBlocking {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, e ->
            println(e.message)
        }
        val scope =
            CoroutineScope(Job() + Dispatchers.IO + CoroutineName("我的协程")) + coroutineExceptionHandler
        val job1 = scope.launch {
            println("i am corountine ${coroutineContext[Job]} ${Thread.currentThread()}")

            val job1_child = async {
                println("i am corountine ${coroutineContext[Job]} ${Thread.currentThread()}")
            }
        }
    }

    /**
     *如果继承了协程上下文,又重新覆盖了某个部分,会被覆盖掉,不会使用父协程的上下文
     * 所以下面打印的是 IO线程
     */
    suspend fun 协程上下文的继承_2() = runBlocking {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, e ->
            println(e.message)
        }
        val scope = CoroutineScope(Job() + Dispatchers.Main + coroutineExceptionHandler)
        val job = scope.launch(Dispatchers.IO) {
            println("${Thread.currentThread()}")
        }
    }

    /**
     * 协程构建器有两种形式:自动传播异常(launch和actor),向用户
     * 暴露异常(async和produce)当这些构建器用于创建一个根协程时(该协程
     * 不是另一个协程的子协程),前者这类构建器,异常会在它发生的第一时间被抛出,
     * 后者则依赖用户最终消费异常,类如通过await和receive。
     *
     * *** 一定是根协程才符合上面异常的发生规律  ***
     *
     * 所以我们捕捉异常的地方也不一样,如下代码:
     */
    suspend fun 协程异常的自动传播和主动暴露_根协程() = runBlocking<Unit> {

        val job = GlobalScope.launch {
            try {
                throw IndexOutOfBoundsException()
            } catch (e: Exception) {
                println("IndexOutOfBoundsException")
            }
        }
        job.join()

        val deferred = GlobalScope.async {
            throw  NullPointerException()
        }
        try {
            deferred.await()
        } catch (e: Exception) {
            println("NullPointerException")
        }

    }

    /**
     *非根协程所创建的协程中,产生的异常总是会被传播
     *
     * 即使是async构建器,不调用await的情况下,异常也会被传播
     */
    suspend fun 协程异常的自动传播和主动暴露_非根协程() = runBlocking<Unit> {

        val scope = CoroutineScope(Job())

        val job = scope.launch {
            async {
                throw ArrayIndexOutOfBoundsException()
                //如果async 抛出异常,launch 就会立即执行抛出异常,
                //而不用调用await()
            }
        }
        job.join()
    }


    /**
     * 当一个协程由于一个异常而运行失败时,
     * 它会传播这个异常并传递给它的
     * 父级。接下来,父级会进行下面几个操作:
     *  1、取消它自己的子级
     *  2、取消它自己
     *  3、将异常传播并传递给它的父级
     *
     *  按照上面的逻辑 就会导致一个子协程的异常,导致整个协程都被取消
     *  我们可以通过SupervisorJob 打 破整个规律。不会因为一个子协程的异常,
     *  导致其它子协程都被取消(应该和supervisorScope一个道理,一个是作用域构建器,
     *  一个是利用CoroutineScope作用域构建器的上下文对象中的job对象。
     *  后续:查阅资料后 supervisorScope构建的协程可以达到使用SupervisorJob一个效果,
     *  但是如果supervisorScope内部发生错误,会导致所有内部子协程都被取消 举列见://supervisorScope举列)
     *
     *  如果要停止所有协程,可以用supervisor.cancel()方法
     */
    suspend fun 协程异常的传播特性() = runBlocking {

        val supervisor = CoroutineScope(SupervisorJob())
        val job1 = supervisor.launch {
            delay(100)
            println("child 1")
            throw IllegalAccessException()
        }

        val job2 = supervisor.launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                println("child 2 finished...")
            }
        }
        joinAll(job1, job2)
        //此处不会因为job1的异常,导致job2也被取消


        //supervisorScope举列
        supervisorScope {
            val job1 = launch {
                delay(100)
                println("child 1")
                throw IllegalAccessException()
            }
            //如果作用域本身发生异常了,那么job1和job2也会被取消
            throw ArrayIndexOutOfBoundsException()

            val job2 = launch {
                try {
                    delay(Long.MAX_VALUE)
                } finally {
                    println("child 2 finished...")
                }
            }
        }
    }


    /**
     * 使用CoroutineExceptionHandler对协程进行异常捕捉
     * 以下的条件满足时,异常就会被捕获:
     * 1、时机:异常是被自动抛出的(比如launch可以,async就不行)
     * 2、位置:在CorountineScope的CorountineContext中或在一个根协程(CorountineScope或者supervisorScope的
     * 直接子协程)中。
     *
     *
     * 所以下面的代码 只能捕获到job的异常,而无法捕捉到deferred的
     */
    suspend fun 异常捕获的时机() = runBlocking<Unit> {

        val handler = CoroutineExceptionHandler { _, ex ->
            println(ex.message)
        }

        val job = launch(handler) {
            throw ArrayIndexOutOfBoundsException()
        }

        val deferred = async(handler) {
            throw IllegalAccessException()
        }

        job.join()
        deferred.await()

    }

    /**
     * 使用CoroutineExceptionHandler对协程进行异常捕捉
     * 以下的条件满足时,异常就会被捕获:
     * 1、时机:异常是被自动抛出的(比如launch可以,async就不行)
     * 2、位置:在CorountineScope的CorountineContext中或在一个根协程(CorountineScope或者supervisorScope的
     * 直接子协程)中。
     *
     *
     * 所以下面的代码 方式一可以捕获到异常,方式二捕获不到。
     * 方式一 handler设置的地方 符合 在CorountineScope的CorountineContext中。也符合时机 launch
     * 方式二 不符合上述条件
     *
     * 结论:handler最好放在外部协程里,不要放入内部协程(其实之前学习异常传播特性 我们应该也可以联想到,
     * 内部协程的异常 是向外部传输的。所以异常自然 也是在最外部去捕捉)
     *
     *
     */
    suspend fun 异常捕获的时机_2() = runBlocking<Unit> {

        val handler = CoroutineExceptionHandler { _, ex ->
            println(ex.message)
        }

        val scope = CoroutineScope(Job())

        //捕捉方式一:
        val job1 = scope.launch(handler) {
            launch {
                throw ArrayIndexOutOfBoundsException()
            }

        }
        job1.join()

        //捕获方式二:
        val job2 = scope.launch {
            launch(handler) {
                throw ArrayIndexOutOfBoundsException()
            }
        }
        job2.join()
    }


    /**
     * 只能捕获异常,但是不能解决 程序奔溃的问题。这里只是得到异常信息
     *
     * main(目录)->resources(目录)->META-INF(目录)->services(目录)
     * ->kotlinx.corountines.CorountineExceptionHandler(文件)
     *
     * 创建自定义CoroutineExceptionHandler->MyCoroutineExceptionHandler
     *
     * kotlinx.corountines.CorountineExceptionHandler文件里写上
     *com.corrountine.MyCoroutineExceptionHandler(就是MyCoroutineExceptionHandler的全路径)
     *
     *
     */
    suspend fun 协程全局异常捕捉() = runBlocking {


    }

    /**
     *  取消与异常
     *  取消与异常紧密相关,协程内部使用CancellationException:来进行取消,这个异常会被
     *  忽略。
     *  当子协程被取消时,不会取消它的父协程。
     *  如果一个协程遇到了CancellationException以外的异常,它将使用该异常取消它的父协
     *  程。当父协程的所有子协程都结束后,异常才会被父协程处理。
     *
     *  举例一:不会因为child的取消抛出的异常,导致父协程job被取消
     *
     *  举例二:非CancellationException的异常,子协程都被取消了,父协程才会去处理异常
     *
     */
    suspend fun 取消与异常() = runBlocking {

        //举例一:
        val job = launch {
            val child = launch {
                try {
                    delay(Long.MAX_VALUE)
                } finally {
                    println("Child is cancelled.")
                }
            }
            yield()
            println("Cancelling child")
            child.cancelAndJoin()
            yield()
            println("Parent is no cancelled")
        }
        job.join()


        //举例二:
        val handler = CoroutineExceptionHandler { _, ex ->
            println("Caugth:${ex.message}")
        }
        val job2 = GlobalScope.launch(handler) {
            launch {
                try {
                    delay(Long.MAX_VALUE)
                } finally {
                    //常驻协程,不会被取消
                    withContext(NonCancellable) {

                        println("子协程都被取消了,但是异常还没有被处理,要等所有的子协程都被取消了,才会被父协程处理")
                        delay(100)
                        println("第一个子协程处理完毕了")
                    }
                }
            }


            launch {
                delay(10)
                println("第二个子协程抛出异常")
                throw ArrayIndexOutOfBoundsException()
            }

        }
        job2.join()
        //job2 打印结果
        // 第二个子协程抛出异常
        // ->子协程都被取消了,但是异常还没有被处理,要等所有的子协程都被取消了,才会被父协程处理
        // ->第一个子协程处理完毕了
        // ->Caugth:ArrayIndexOutOfBoundsException
    }

    /**
     * 异常聚合
     * 当协程的多个子协程因为异常而失败时,一般情况下取第一个异常进行处理。在
     * 第一个异常之后发生的所有其他异常,都将被绑定到第一个异常之上。
     *
     * ex.suppressed.contentToString()是个数组,异常都会在里面
     */
    suspend fun 异常的聚合() = runBlocking {
        val handler = CoroutineExceptionHandler { _, ex ->
            println("Caugth:${ex.suppressed.contentToString()}")
        }

        val job = GlobalScope.launch(handler) {

            launch {
                try {
                    delay(Long.MAX_VALUE)
                } finally {
                    throw  ArithmeticException()
                }
            }

            launch {
                try {
                    delay(Long.MAX_VALUE)
                } finally {
                    throw  ArrayIndexOutOfBoundsException()
                }
            }

            launch {
                try {
                    delay(Long.MAX_VALUE)
                } finally {
                    throw  IllegalAccessException()
                }
            }
        }
    }
}

记录学习,仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值