Kotlin:Flow 全面详细指南,附带源码解析。

结果

send1

receiver1

send2

receiver2

send3

receiver3

end

For example 3 : 异步计算并返回?引入flow

有什么办法可以异步计算多个值并且返回的吗?当然可以,我们可以使用挂起函数,使计算过程在异步线程执行,最终以list的形式返回。

举个🌰

suspend fun simple(): List {

//模拟耗时操作

delay(1000)

return listOf(1, 2, 3)

}

fun main()= runBlocking {

val job = launch {

simple().forEach { value -> println(value) }

}

launch {

println(“other operate”)

}

job.join()

}

结果:从结果来看,耗时操作并没有影响主线程的运行😎

other operate

1

2

3

但是,这样就够了吗?no no no !🙅‍♀️

使用list意味着我们只能一次性的返回所有的值。所以为了表示流的计算,引入了flow。就像可以使用Sequence类型用于同步计算值一样。

Flow使用


Flow 简单使用

上面介绍了flow要解决什么问题,那么我们就开始使用起来吧。

先看一个简单的🌰

fun simple(): Flow = flow { // flow 构建

for (i in 1…3) {

//模拟异步耗时计算

delay(100)

//发射值

emit(i)

}

}

fun main() = runBlocking {

// launch 一个协程 同时延时100毫秒打印 校验主线程是否阻塞

launch {

for (k in 1…3) {

println(“I’m not blocked $k”)

//主线程在这个时间段可以干别的事情

delay(100)

}

}

// collect flow value

simple().collect { println(it) }

}

结果:通过线程打印 I'm not blocked证明异步计算不会阻塞主线程,计算成功之后会resume到collect里面继续执行。

I’m not blocked 1

1

I’m not blocked 2

2

I’m not blocked 3

3

通过上述代码,需要注意一下几点:🙆‍♀️

  • 使用flow代码块构建出来的类型为Flow

  • flow代码块里面允许写挂起函数。比如上面的,delay emit

  • 使用emit进行值的发射,使用collect进行值的收集

Flow 构建

除了上面使用的flow{}进行构建之外,还可以使用其他的方式进行构建。

  1. 使用flowOf可以定义一组固定的值

fun simple(): Flow = flowOf(1, 2, 3)

  1. 可以使用 asFlow() 扩展函数将各种集合和序列转换为流。

// 将list转换为flow

listOf(1,2,3).asFlow().collect { value -> println(value) }

Flow 冷流

Flow是冷流,构建器代码在调用collect之前是不会进行调用的,对于多个调用者,都会重新走一遍构建器的代码。

废话不多说,上🌰

fun simple(): Flow = flow {

println(“Flow started”)

for (i in 1…3) {

delay(100)

emit(i)

}

}

fun main() = runBlocking {

println(“Calling simple function…”)

val flow = simple()

println(“Calling collect…”)

flow.collect { value -> println(value) }

println(“Calling collect again…”)

flow.collect { value -> println(value) }

}

结果

Calling simple function…

Calling collect…

Flow started

1

2

3

Calling collect again…

Flow started

1

2

3

每次收集流时都会开始,这就是为什么我们再次调用 collect 时会看到“Flow started”的原因。

Flow 取消

如何取消一个Flow呢?

Kotlin官方并没有提供flow取消的函数。啊 这???😕听到这个是不是还满疑惑。且听我细细道来。

Flow需要在协程里面使用,因为collect是挂起函数,另外基于冷流的特性,不调用collect构建器的代码压根不会走。所以只能是协程。那 我取消协程不就行了吗?😮。好像之前有看到过有开发者提出过,是否要给flow单独加一个取消的函数,被Jetbrains无情的拒绝了,哈哈哈哈很搞笑。下面引用Kotlin官方的一段话。

Flow adheres to the general cooperative cancellation of coroutines. As usual, flow collection can be cancelled when the flow is suspended in a cancellable suspending function (like delay).

adheres 坚持

Flow 坚持协程的一般协作取消。 像往常一样,当流在可取消的挂起函数(如延迟)中被挂起时,可以取消流收集。

这个adheres好像就像是在回复广大的开发者,你取消协程就行了😂😂😂。

好了,下面看取消的🌰

fun simple(): Flow = flow {

for (i in 1…3) {

delay(100)

println(“emit $i”)

emit(i)

}

}

fun main() = runBlocking {

val job = launch {

simple().collect { println(it) }

}

delay(250)

job.cancel(CancellationException(“timeout 250”))

println(“done”)

}

结果:看我们只需要取消对应的协程即可,对应的flow也会被取消收集。

emit 1

1

emit 2

2

done

这里引申一点,对于timeout,官方有提供专用的操作函数,withTimeout系列。不需要我们手动delay然后继续调用取消,毕竟不是很优雅。

上述代码也可以写成如下的形式

fun simple(): Flow = flow {

for (i in 1…3) {

delay(100)

println(“emit $i”)

emit(i)

}

}

fun main() = runBlocking {

withTimeoutOrNull(250) {

simple().collect { value -> println(value) }

}

println(“done”)

}

看到了吗?直接将launch替换为withTimeoutOrNull就可以做到延时取消效果了,这里简单做一下源码分析。

源码

public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T? {

try {

return suspendCoroutineUninterceptedOrReturn { uCont ->

val timeoutCoroutine = TimeoutCoroutine(timeMillis, uCont)

coroutine = timeoutCoroutine

setupTimeout<T?, T?>(timeoutCoroutine, block)

}

} catch (e: TimeoutCancellationException) {

}

}

private class TimeoutCoroutine<U, in T: U>(

@JvmField val time: Long,

uCont: Continuation // unintercepted continuation

) : ScopeCoroutine(uCont.context, uCont), Runnable {

override fun run() {

cancelCoroutine(TimeoutCancellationException(time, this))

}

}

private fun <U, T: U> setupTimeout(

coroutine: TimeoutCoroutine<U, T>,

block: suspend CoroutineScope.() -> T

): Any? {

// schedule cancellation of this coroutine on time

coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine, coroutine.context))

}

public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =

DefaultDelay.invokeOnTimeout(timeMillis, block, context)

省略了一大堆非核心的代码,我们直接看延时取消的操作,这里简单分析一下:

  • 创建TimeoutCoroutine对象,它同时实现了Runnable,在run里面调用了取消函数,抛出TimeoutCancellationException。

  • 调用当前context的invokeOnTimeout函数,该函数需要一个Runnable,传入了timeoutCoroutine。此实现使用内置的单线程调度执行器服务。会在延时对应的事件后调用Runnable的run函数,然后就会取消当前的协程。

  • 在取消协程之后,会取消掉当前上下文的所有将在完成时调用的回调,disposeOnCompletion函数被调用。

Flow 相关操作符

这一块的操作符,其实是比较多的。但是如果您熟悉RxJava的话,其实都是差不多的。这一块这里就不做源码分析了,只是看一下怎么使用即可。内部其实是创建了新的流返回出来了,有兴趣的话可以自行查看一下源码。

中间流操作符
map

映射。看🌰(传入请求流可以使用 map 操作符映射到结果,即使执行请求是由挂起函数实现的长时间运行的操作)

suspend fun performRequest(request: Int): String {

delay(1000) // 模拟长时间的异步工作

return “response $request”

}

fun main() = runBlocking {

(1…3).asFlow() // 转换为flow

.map { request -> performRequest(request) }

.collect { response -> println(response) }

}

结果

response 1

response 2

response 3

filter

过滤操作,看🌰

suspend fun performRequest(request: Int): String {

delay(1000) // 模拟长时间的异步工作

return “response $request”

}

fun main() = runBlocking {

(1…3).asFlow() // 转换为flow

.map { request -> performRequest(request) }

.filter { it == “response 1” }

.collect { response -> println(response) }

}

结果:仅返回匹配到的值

response 1

变换操作符

在流变换算子中,最通用的一种叫做变换。 它可以用来模仿简单的转换,比如 map 和 filter,也可以实现更复杂的转换。 使用transform,我们可以发出任意次数的任意值。

例如🌰,使用transform,我们可以在执行长时间运行的异步请求之前发出一个字符串,并在其后响应:

(1…3).asFlow() // a flow of requests

.transform { request ->

emit(“Making request $request”)

emit(performRequest(request))

}

.collect { response -> println(response) }

结果

Making request 1

response 1

Making request 2

response 2

Making request 3

response 3

size限制操作符

顾名思义,限制收集的数量。使用运算符take

它会在判断当发射的值在达到相应限制时取消流程的执行。 因为协程中的取消总是通过抛出异常来执行。所以需要考虑进行相应的异常捕获来保证后续的流畅正常进行不被取消掉

fun numbers(): Flow = flow {

try {

emit(1)

emit(2)

println(“not execute”)

emit(3)

} finally {

println(“finally in numbers”)

}

}

fun main() = runBlocking {

numbers()

.take(2) // take 两个值

.collect { value -> println(value) }

}

结果

1

2

finally in numbers

终端操作符

终端操作符可以启动一个流,最基础的就是上述常提到的collect。但是还有一些其他的终端操作符,它可能会让一些操作变得更简单:

转换为各种集合, toList 和 toSet。

flowOf(1,2).toList().forEach {

println(it)

}

flowOf(1,2).toSet().forEach(::println)

first , 确保获取且进获取第一个值

返回流发出的第一个元素然后取消流的集合的终端运算符。 如果流为空,则抛出 NoSuchElementException。

val value : Int = flowOf(1, 2).first()

println(value)

使用 reduce 和 fold 将流合并到一个值。

val sum = (1…5).asFlow()

.map { it * it } //平方

.reduce { a, b -> a + b } // 进行累加

println(sum)

//结果

55

fold和reduce使用起来差不多,区别就是fold可以定义初始化,其实很简单,reduce传入的lambda前一个参数是每次计算的结果累计,后一个参数是当前需要传入的值,不明白可以去瞅一眼源码,这里不在引申。

onEach

这个操作符也较为常用,这里也介绍一下,返回在上游流的每个值向下游发出之前调用给定操作的流。

🌰

(1…5)

.asFlow()

.onEach {

println(“onEach$it”)

}.collect()

//结果

onEach1

onEach2

onEach3

onEach4

onEach5

操作符的顺序

除非使用对多个流进行操作的特殊运算符,否则流的每个单独集合都按顺序执行。 该集合直接在调用终端运算符的协程中工作。 默认情况下不会启动新的协程。 每个发出的值都由从上游到下游的所有中间操作符处理,然后交付给终端操作符。

🌰

(1…5).asFlow()

.filter {

println(“Filter $it”)

it % 2 == 0

}

.map {

println(“Map $it”)

“string $it”

}.collect {

println(“Collect $it”)

}

//结果 按照顺序没有值依次向下发射

Filter 1

Filter 2

Map 2

Collect string 2

Filter 3

Filter 4

Map 4

Collect string 4

Filter 5

Flow 调度器切换

对于UI驱动型的程序来说,需要将长时间计算的任务放在异步线程处理,UI展示工作需要放在主线程处理。也就是说需要将构建器的代码放到异步线程执行,但是终端操作符,比如collect需要在主线程获取,那么怎么做呢?

使用flowOn操作符

在这之前您可能的了解一下,协程的调度器。可以简单参考之前写的一篇文章,有对调度器做简单介绍:https://blog.csdn.net/weixin_44235109/article/details/119981210

fun main() = runBlocking {

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)//使用flowOn传入Default的调度器

.collect { value ->

log(“Collected $value”)

}

}

结果

16:39:21:954 [DefaultDispatcher-worker-1] Emitting 1

16:39:21:969 [main] Collected 1

16:39:22:071 [DefaultDispatcher-worker-1] Emitting 2

16:39:22:071 [main] Collected 2

16:39:22:178 [DefaultDispatcher-worker-1] Emitting 3

16:39:22:178 [main] Collected 3

可以很明显的看出,构建模块被调度到异步线程处理了。而收集的工作还在主线程进行。

flowOn负责构建的模块调度,那么收集的谁负责呢?

其实和异常处理类似,collect受调用它的协程上下文限制,所以最后的执行线程和当前协程上下文的调度器有关,目前我使用的是idea测试的,默认runBlocking的调度器就是主线程。如果是android上面的话,runBlocking可能就需要传入Dispatchers.Main了。

其实和RxJava还是非常相似的😂。

注意一点,此时其实已经改变流执行的顺序了

官方的解释如下:

Another thing to observe here is that the flowOn operator has changed the default sequential nature of the flow. Now collection happens in one coroutine (“coroutine#1”) and emission happens in another coroutine (“coroutine#2”) that is running in another thread concurrently with the collecting coroutine. The flowOn operator creates another coroutine for an upstream flow when it has to change the CoroutineDispatcher in its context.

这里要注意的另一件事是 flowOn 运算符更改了流的默认顺序性质。 现在收集发生在一个协程(“coroutine#1”)中,发射发生在另一个协程(“coroutine#2”)中,该协程与收集协程同时运行在另一个线程中。 当必须在其上下文中更改 CoroutineDispatcher 时, flowOn 运算符为上游流创建另一个协程。

这一块我简单看了一下源码,这里面不同的调度器会遇到多线程的问题,最里面使用了channel进行了调度处理。具体的核心类是ChannelFlow。后面会对flow进行简单源码分析,但篇幅有限不对这一块过深入分析,感兴趣可以自行查看,或找博主私下探讨。

其实上面的看不出来会改变流执行的顺序,下面改变一下代码,验证一下看看🌰

fun main() = runBlocking {

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)

.collect { value ->

delay(200)

log(“Collected $value”)

}

}

结果

16:52:33:258 [DefaultDispatcher-worker-1] Emitting 1

16:52:33:386 [DefaultDispatcher-worker-1] Emitting 2

16:52:33:482 [main] Collected 1

16:52:33:493 [DefaultDispatcher-worker-1] Emitting 3

16:52:33:684 [main] Collected 2

16:52:33:887 [main] Collected 3

我们只需要将,collect里面增加一个delay即可,发现其实这时候就是发射归发射,收集归收集了。不似上面我们写的程序,发射一个值只有到终端操作符之后才会发射第二个。这里面肯定就会对值进行缓存。那么就会牵扯到一个问题。老生常谈🙆‍♀️,背压处理

Flow 背压处理

对于背压处理,Kotlin 提供三种解决方案:

| 操作符 | 含义 |

| — | — |

| buffer | 指定固定容量缓存 |

| conflate | 保留最新的值 |

| collectLatest | 新值发送时取消之前的 |

buffer

这里有必要看一下buffer函数的源码定义

public fun Flow.buffer(capacity: Int = BUFFERED, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND)

可以看出需要两个参数,都有默认值。

  • 第一个好理解容量,默认等于BUFFERED,这个值其实时64,可以按照使用时需要自己指定具体的数字。

  • 第二个是指定,当buffer溢出时的操作,默认的操作是挂起。还要两个操作分别是删除最旧的值不要挂起或者删除当前最新的值不要挂起,可以自行查看源码,这里不再引申。

使用🌰

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)

.buffer()

.collect { value ->

delay(200)

log(“Collected $value”)

}

//结果

17:17:28:420 [DefaultDispatcher-worker-1] Emitting 1

17:17:28:536 [DefaultDispatcher-worker-1] Emitting 2

17:17:28:646 [main] Collected 1

17:17:28:646 [DefaultDispatcher-worker-1] Emitting 3

17:17:28:846 [main] Collected 2

17:17:29:049 [main] Collected 3

conflate

这个只获取最新值也比较好理解,应用场景,比如说获取下载进度,对于用户来说其实每次只需要获取当前最新的进度就好了,不需要把之前的值再去获取一遍,下面也举一个例子🌰

fun main() = runBlocking {

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)

.conflate()

.collect { value ->

delay(300)//模拟下游处理比较慢

log(“Collected $value”)

}

}

//结果 第一个值肯定可以拿到 当地一个值处理完成之后 最新的值就是3了 所以丢弃了2

17:21:42:916 [DefaultDispatcher-worker-1] Emitting 1

17:21:43:034 [DefaultDispatcher-worker-1] Emitting 2

17:21:43:140 [DefaultDispatcher-worker-1] Emitting 3

17:21:43:236 [main] Collected 1

17:21:43:546 [main] Collected 3

collectLatest

说明一点:这玩意其实也是一个终端操作符

前两个可能都比较好理解,那新值发送时取消之前的是什么意思呢?为了便于理解,直接上例子,按照结果说明:

🌰

fun main() = runBlocking {

flow {

for (i in 1…3) {

//模拟异步处理

delay(100)

log(“Emitting $i”)

emit(i) // emit next value

}

}.flowOn(Dispatchers.Default)

.collectLatest {

delay(300)

log(“Collected $it”)

}

}

//结果

17:25:33:916 [DefaultDispatcher-worker-1] Emitting 1

17:25:34:038 [DefaultDispatcher-worker-1] Emitting 2

17:25:34:150 [DefaultDispatcher-worker-1] Emitting 3

17:25:34:453 [main] Collected 3

对比上面的例子,这里只是将collect替换成了collectLatest而已。为什么1没有了呢?

显而易见了,这玩意会在最新的到来会直接取消下游上一个消费的处理,因为有delay所以1还没有来得及打印,就因为下一个值发射了,然后就被取消了!!!您可真霸道呢?🙆‍♀️

Flow 异常处理

当操作符内的发射器或代码抛出异常时,流收集可以以异常结束。 有几种方法可以处理这些异常。

try…catch

较为简单,上🌰

fun simple(): Flow = flow {

for (i in 1…3) {

println(“Emitting $i”)

emit(i) // emit next value

}

}

fun main() = runBlocking {

try {

simple().collect { value ->

println(value)

check(value <= 1) { “Collected $value” }

}

} catch (e: Throwable) {

println(“Caught $e”)

}

}

//结果

fun simple(): Flow = flow {

for (i in 1…3) {

println(“Emitting $i”)

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
减轻大家的负担。**
[外链图片转存中…(img-9jlhMHC8-1712799168074)]
[外链图片转存中…(img-aeXCcnxW-1712799168074)]
[外链图片转存中…(img-lQmdGslf-1712799168075)]
[外链图片转存中…(img-6T0qUSHO-1712799168075)]
[外链图片转存中…(img-P3uBwgVa-1712799168075)]
[外链图片转存中…(img-tuobtv2m-1712799168076)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-Rq9h9MId-1712799168076)]

最后

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

[外链图片转存中…(img-4HOoV7RN-1712799168076)]

【算法合集】

[外链图片转存中…(img-E5Tx8RnX-1712799168077)]

【延伸Android必备知识点】

[外链图片转存中…(img-Eld7QfYX-1712799168077)]

【Android部分高级架构视频学习资源】

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-23JMkhpA-1712799168077)]

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值