【Android】Android协程的使用与封装

如何正确的在 Android 上使用 Kotlin 协程? 前言 你还记得是哪一年的 Google IO 正式宣布 Kotlin 成为 Android 一级开发语言吗?是 Google IO 2017 。如今两年时间过去了,站在一名 Android 开发者的角度来看,Kotlin 的生态环境越来越好了,相关的开源项目和学习资料也日渐丰富,身边愿意去使用或者试用 Kotlin 的朋友也变多了。常年混迹掘金的我也能明显感觉到 Kotlin 标签下的文章慢慢变... 阅读详情

Kotlin协程的使用与封装

前言:

相信作为一个现代Android开发者,应该都用过或者听过协程了,还不少大佬都已经把协程给扒皮了,其本质就是线程池的封装

协程的性能还不如原生线程池,为什么我要用协程,是因为协程可以把碎片化的方法很方便的加入异步处理,发挥Android设备多核的优势。合理的使用协程,应用反而更流畅。

下面我就不涉及太多原理理论了,直接上代码,Android中如何使用和封装协程。

一. 协程的使用

常用的几个关键的函数方法

launch ,runBlocking, withContext ,async/await

前两者启动协程,后两者调度线程。

lauch 是非阻塞的 而runBlocking是阻塞的。直接上例子:

 private fun testCoroutine1() {
       //这里只是协程作用域
      // GlobalScope.launch { 
      // lifecycleScope.launch { 
      // viewModelScope.launch { 都可以
        CoroutineScope(Dispatchers.Main).launch {
            delay(500)
            YYLogUtils.w("协程1作用域内部执行")
        }
        YYLogUtils.w("协程1作用域wai部执行")
    } 

结果是先执行外部,再执行内部

而runBlocking恰恰相反

 private fun testCoroutine2() {
        runBlocking {
            delay(500)
            YYLogUtils.w("协程2作用域内部执行")
        }
        YYLogUtils.w("协程2作用域wai部执行")
    } 

结果是先执行内部,再执行外部,因为阻塞了。

所以一般我们开发绝大多数都是使用launch了。 而我们切换线程一般用withContext和 async/await.区别就是你想顺序执行还是并发执行。

顺序执行:
这里会先等待1秒输入1234,然后调用接口获取Industry,请求完成之后再调用接口获取School,当前全部完成之后隐藏Loading。
其中网络请求异常的处理已经在内部封装处理了,后面会讲到。

 viewModelScope.launch {
                //开始Loading
                loadStartProgress()

                val startTimeStamp = System.currentTimeMillis()
                val res = withContext(Dispatchers.Default) {
                    //异步执行
                    delay(1000)
                    return@withContext "1234"
                }
                val endTimeStamp = System.currentTimeMillis()
                YYLogUtils.w("res: $res  time: ${endTimeStamp-startTimeStamp}")

                //网络请求获取行业数据
                val industrys = mRepository.getIndustry()

                //返回的数据是封装过的,检查是否成功
                industrys.checkResult({
                    //成功
                    _industryLD.postValue(it)
                }, {
                    //失败
                    toastError(it)
                })

                //上面的请求执行完毕才会执行这个请求
                val schools = mRepository.getSchool()
                //返回的数据是封装过的,检查是否成功
                schools.checkSuccess {
                    _schoollLD.postValue(it)
                }

                //完成Loading
                loadHideProgress()
            } 

并发执行:
这里会同时调用Industry和School接口,等待两者都完成之后再展示UI。

 viewModelScope.launch {

                //开始Loading
                loadStartProgress()

                val industryResult = async {
                    mRepository.getIndustry()
                }

                val schoolResult = async {
                    mRepository.getSchool()
                }


                val localDBResult = async {
                    //loadDB()
                    YYLogUtils.w("thread:" + CommUtils.isRunOnUIThread())

                    delay(10000)
                }

                //一起处理数据
                val industry = industryResult.await()
                val school = schoolResult.await()

                //如果都成功了才一起返回
                if (industry is OkResult.Success && school is OkResult.Success) {
                    loadHideProgress()

                    _industryLD.postValue(industry.data!!)
                    _schoollLD.postValue(school.data!!)
                }


                YYLogUtils.e(localDBResult.await().toString() + "完成")

            } 

大家开发App常用的两种方式都已经掌握了,还有一个不常用但是很重要的点,就是网络请求去重。
场景:点击CheckBox调用接口是否开启通知,那么我们就要把用户推送id传给服务器。如果用户狂点CheckBox,那么我怎么请求网络?

常用的两种去重手段。一种是取消上一次的,另一种是队列排队一个一来。

老规矩直接上代码了:

 /**
     * 网络请求去重
     */
    private var controlledRunner = ControlledRunner<OkResult<List<Industry>>>()  //取消之前的
    private val singleRunner = SingleRunner()       //任务队列,排队,单独的
    fun netDuplicate() {
        
        viewModelScope.launch {
            //比较常用
            //取消上一次的,执行这一次的
            controlledRunner.cancelPreviousThenRun {
                return@cancelPreviousThenRun mRepository.getIndustry()
            }.checkSuccess {
                YYLogUtils.e("请求成功:")
                _industryLD.postValue(it)
            }

            //前一个执行完毕了,再执行下一个
//                singleRunner.afterPrevious {
//                    mMainRepository.getIndustry()
//                }.checkSuccess {
//                    YYLogUtils.e("测试重复的数据:" + it.toString())
//                }

        }
        
    } 

控制器源码如下:

class SingleRunner {

    private val mutex = Mutex()

    /**
     * 加入到任务队列,前一个任务执行完毕再执行下一个任务
     */
    suspend fun <T> afterPrevious(block: suspend () -> T): T {
        mutex.withLock {
            return block()
        }
    }
}

class ControlledRunner<T> {

    private val activeTask = AtomicReference<Deferred<T>?>(null)

    suspend fun cancelPreviousThenRun(block: suspend () -> T): T {

        activeTask.get()?.cancelAndJoin()

        return coroutineScope {
            val newTask = async(start = LAZY) {
                block()
            }

            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }

            val result: T

            while (true) {
                if (!activeTask.compareAndSet(null, newTask)) {
                    activeTask.get()?.cancelAndJoin()
                    yield()
                } else {
                    result = newTask.await()
                    break
                }
            }

            result
        }
    }

    /**
     * 不执行新任务,返回上一个任务的结果
     */
    suspend fun joinPreviousOrRun(block: suspend () -> T): T {

        activeTask.get()?.let {
            return it.await()
        }
        return coroutineScope {

            val newTask = async(start = LAZY) {
                block()
            }

            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }

            val result: T

            while (true) {
                if (!activeTask.compareAndSet(null, newTask)) {

                    val currentTask = activeTask.get()
                    if (currentTask != null) {
                        newTask.cancel()
                        result = currentTask.await()
                        break
                    } else {
                        yield()
                    }
                } else {

                    result = newTask.await()
                    break
                }
            }
            result
        }
    }
} 

二. 网络请求协程的封装

Retrofit+协程的使用:
原理就是调用Retrofit方法,对它try-catch.得到的是网络请求错误信息,可以根据不同的Type类型。然后对Retrofit的返回结果再判断如果code不是200,那么就是Api错误(例如Token失效)。对错误和成果的结果做统一的封装返回给ViewModel处理。

方式一:
处理BaseRepository:

open class BaseRepository {

    //无异常处理 -> 一般不用这个,一旦报错会App崩溃
    suspend inline fun <T : Any> handleApiCall(call: suspend () -> BaseBean<T>): BaseBean<T> {
        return call.invoke()
    }

    /**
     * 推荐使用拓展函数extRequestHttp
     * 如果要使用Base里面的方法请求网络这么使用
     *   return handleErrorApiCall(call = {
                    handleApiErrorResponse()
                })
     * 都可以实现网络请求
     */
    //处理Http错误-内部再处理Api错误
    suspend fun <T : Any> handleErrorApiCall(call: suspend () -> OkResult<T>, errorMessage: String = ""): OkResult<T> {
        return try {
            call()
        } catch (e: Exception) {
            if (!TextUtils.isEmpty(errorMessage)) {
                OkResult.Error(IOException(errorMessage))
            } else {
                OkResult.Error(handleExceptionMessage(e))
            }
        }
    }

    //处理Api错误,例如403Token过期 把BaseBean的数据转换为自定义的Result数据
    suspend fun <T : Any> handleApiErrorResponse(
        response: BaseBean<T>,
        successBlock: (suspend CoroutineScope.() -> Unit)? = null,
        errorBlock: (suspend CoroutineScope.() -> Unit)? = null
    ): OkResult<T> {

        return coroutineScope {
            //执行挂起函数
            if (response.code == 200) {  //这里根据业务逻辑来 200 -1 等
                successBlock?.let { it() }
                OkResult.Success(response.data)
            } else {
                errorBlock?.let { it() }
                OkResult.Error(IOException(response.message))
            }
        }
    }


    //处理自定义错误消息
    fun handleExceptionMessage(e: Exception): IOException {
        return when (e) {
            is UnknownHostException -> IOException("Unable to access domain name, unknown domain name.")
            is JsonParseException -> IOException("Data parsing exception.")
            is HttpException -> IOException("The server is on business. Please try again later.")
            is ConnectException -> IOException("Network connection exception, please check the network.")
            is SocketException -> IOException("Network connection exception, please check the network.")
            is SocketTimeoutException -> IOException("Network connection timeout.")
            is RuntimeException -> IOException("Error running, please try again.")
            else -> IOException("unknown error.")
        }

    }

} 

使用如下:

 suspend fun getServerTime(): OkResult<ServerTimeBean> {
        return handleErrorApiCall({
            handleApiErrorResponse(
                MainRetrofit.apiService.getServerTime(
                    Constants.NETWORK_CONTENT_TYPE,
                    Constants.NETWORK_ACCEPT_V1
                )
            )
        })
    } 

方式二:
使用扩展方法的直接一步到位处理:

suspend fun <T : Any> BaseRepository.extRequestHttp(call: suspend () -> BaseBean<T>): OkResult<T> {

    //两种方式都可以,自用下面一种方式
//    runCatching {
//        call.invoke()
//    }.onSuccess { response: BaseBean<T> ->
//        if (response.code == 200) {
//            OkResult.Success(response.data)
//        } else {
//            OkResult.Error(ApiException(response.code, response.message))
//        }
//    }.onFailure { e ->
//        e.printStackTrace()
//        OkResult.Error(handleExceptionMessage(Exception(e.message, e)))
//    }

    return try {

        val response = call()

        if (response.code == 200) {
            OkResult.Success(response.data)
        } else {
            OkResult.Error(ApiException(response.code, response.message))
        }

    } catch (e: Exception) {

        e.printStackTrace()
        OkResult.Error(handleExceptionMessage(e))
    }

} 

使用:

 suspend inline fun getIndustry(): OkResult<List<Industry>> {

        return extRequestHttp {
            DemoRetrofit.apiService.getIndustry(
                Constants.NETWORK_CONTENT_TYPE,
                Constants.NETWORK_ACCEPT_V1
            )
        }

    } 

调用接口都是固定的模板代码,和之前MVP的方式一样,只需要定义Retrofit-Api的接口定义就行。

最后

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长。而不成体系的学习效果低效漫长且无助。时间久了,付出巨大的时间成本和努力,没有看到应有的效果,会气馁是再正常不过的。

所以学习一定要找到最适合自己的方式,有一个思路方法,不然不止浪费时间,更可能把未来发展都一起耽误了。

如果你是卡在缺少学习资源的瓶颈上,那么刚刚好我能帮到你。以上知识笔记全部免费分享,如有需要获取知识笔记的朋友,可以点击下方二维码费领取。

Android开发学习笔记——Kotlin协程 Android开发学习笔记——Kotlin协程Android中的异步编程协程基本使用 Android中的异步编程 我们知道,Android App的进程也是一个DVM,内部有许多线程在执行,比如,主UI线程(Main Thread),垃圾回收线程等。其中主UI线程负责执行我们写的应用代码。对于只做很少的I/O操作或耗时操作的App,单一线程开发模式问题不大,但是如果有大量IO或者CPU计算的任务,我们就必须在其他线程内完成了。 在实际的开发过程中,我们通常会要求应用的帧率达到60帧,也就是说每16毫秒就必须 阅读详情

相关推荐

Android使用Kotlin协程封装网络库

概述 最近又要替换网络库成了,导致所有的业务代码都要替换且要重新测试回归。为了防止后续再遇到这样事情,降低替换核心库的成本,现利用协程对网络库进行封装。 理想API调用方式 launch { val response = TestReq(q = "电脑").request<TestResponse>() Toast.makeText(this, response?.result?.toString()?:"...

long8313002的博客 4914

Android协程

在Looper的死循环中,它会不断地从消息队列中取出消息,并将消息分发给对应的Handler处理。当消息队列为空时,Looper会一直循环等待新的消息到达。在等待消息期间,Looper的死循环会阻塞主线程,因为它会一直占用主线程的执行时间片。这意味着主线程无法继续执行其他任务或响应用户的输入事件或系统事件。只有当主线程长时间占用了 CPU 或其他系统资源,并且长时间无法响应用户输入事件或完成关键操作时,才会触发 ANR 错误。耗时操作本身并不会导致主线程卡死,导致主线程卡死的真正原因是。

m0_61130325的博客 3915

Android中使用Kotlin协程代替RxJava封装网络请求

现在的Android项目普遍使用Retrofit+RxJava的组合实现网络接口请求与数据的展现。这一功能通过Kotlin语言的协程功能也可以很方便的实现。 相比较而言,RxJava功能过于强大,如果仅用于封装网络请求,有些杀鸡用牛刀的感觉。使用Kotlin的协程实现这个需求代码更精简,逻辑也更清晰一些。 以下是一个完整的例子。使用Retrofit结合Kotlin协程,实现网络请求。 点击Acti...

RickyXE 9805

Android协程简介 -- 基础概念

之前通过一个登录请求,简单了解Android协程的实现方式。相信很多人和我刚开始的时候一样,看的云里雾里的不清楚。这是因为我们对协程缺乏基础的了解。这篇文章,我们就详细介绍一下协程相关的基础知识。协程的概念协程优点协程相关的类、函数结构化并发如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

嘴巴吃糖了 1437

Android中使用Kotlin协程(Coroutines)和Retrofit进行网络请求(三)之异常处理与封装

写在前面 前面文章介绍了一些kotlin协程和retorfit结合进行网络请求的基础,但是如果在前面的demo执行的过程中,我们断开手机网络,会发生什么?没错,APP会因为IO异常而崩溃!为什么呢,这是因为retrofit在执行excute()方法时会throws IOException,而enqueue()方法则不会,因为它会把IOException处理放在callback的onFailur...

weixin_44407870的博客 7705

浅谈安卓协程Android coroutine,及其与Retrofit的简单结合与使用

0. 前言 早期的安卓编程。资源的调度与释放从来都是一个不可避免的问题,开发者在onCreate()中调度资源,例如进行网络请求,进行磁盘IO操作,然后在onPause()或者onDestory()中手动释放或打断资源的使用。 LifeCycle的引入。为了实现更简便的生命周期监听,谷歌推出了LifeCycle Support Library支持库(后被迁移到AndroidX中成为Jetpack的一部分),旨在让开发者能够更好地在资源调度时实现自动的生命周期感知 协程的引入。安卓中的协程依赖于Kotlin

weixin_43841630的博客 583

Android使用协程简单封装Room工具类

使用kotlin协程封装的roomUtils,使用简单,操作方便,简直不要太爽

u012556114的博客 1439

安卓开发中优化处理Retrofit协程请求中的异常

通过封装和统一处理Retrofit协程请求中的异常,我们可以提高代码的可维护性和错误处理的准确性。在移动端开发中,使用Retrofit库和协程来进行网络请求已经成为常见的做法。然而,在处理网络请求过程中,异常情况的处理是一个重要的方面。本文将介绍如何封装和处理Retrofit协程请求中的异常,以提高代码的可维护性和错误处理的准确性。例如,我们创建一个名为ApiService的接口,其中包含一个获取用户信息的GET请求方法。现在,我们可以在应用的其他地方使用封装好的请求方法,并处理请求结果。

QwwyEmacs_Lisp的博客 624

kotlin 用协程做网络请求_Android使用Kotlin协程封装网络库

概述最近又要替换网络库成了,导致所有的业务代码都要替换且要重新测试回归。为了防止后续再遇到这样事情,降低替换核心库的成本,现利用协程对网络库进行封装。理想API调用方式launch {val response = TestReq(q = "电脑").request()Toast.makeText(this, response?.result?.toString()?:"网络异常", Toast.L...

weixin_39864101的博客 276

TI MSPM0G3507 循迹小车强制寻黑线代码一

一个基于MSPM0微控制器的循迹小车控制系统。系统通过8路循迹传感器检测黑线位置偏差,使用PID算法计算电机控制量。主程序初始化中断、PWM等外设,在按键触发后进入循迹模式。传感器数据处理函数根据检测到的黑线位置返回偏差值(-7到7),PID控制器根据偏差调整左右电机速度。系统还包含限幅处理、中断服务等模块,实现稳定的自动循迹功能。代码结构清晰,展示了嵌入式系统中传感器数据处理、电机控制和PID算法的典型实现方式。

雾削木的优快云博客 1855

DIY天文摄影:如何用NMP溶剂安全去除佳能CMOS传感器的拜耳滤镜(附详细步骤)

本文为天文摄影爱好者提供了一份详尽的DIY指南,介绍如何使用NMP溶剂安全去除佳能CMOS传感器的拜耳滤镜,将其改造为高灵敏度单色相机。文章深入剖析了NMP溶剂的作用原理与关键风险,并系统性地拆解了从材料准备、分步操作到后期验证的完整实践流程,旨在帮助具备动手能力的爱好者实现深空天体摄影的进阶目标。

w3x4y的博客 308

Android okhttp网络请求封装 结合协程简单使用Kotlin版(2)

第二次封装okhttp

weixin_46897652的博客 3208

【SoloPi】SoloPi使用2-功能使用,录制回放

Soloπ是什么 Soloπ是一个无线化、非侵入式的Android自动化工具,公测版拥有录制回放、性能测试、一机多控三项主要功能,能为测试开发人员节省宝贵时间。 录制回放功能 Soloπ拥有录制操作的能力,用户只需要通过Soloπ执行用例步骤,Soloπ就能够将用户的操作记录下来,并且支持在各个设备上进行回放,这一切都能够在手机上独立完成。 ...

q_Catherine的博客 5829

如何在安卓开发中调用 Shizuku API?

在安卓开发中调用 Shizuku API 实现高权限操作的教程

qq_49848858的博客 2660

手把手教你用ECharts打造炫酷3D饼图(附完整源码)

本文详细介绍了如何使用ECharts创建炫酷的3D饼图,从环境配置到完整源码实现,涵盖参数解析、视觉优化和交互增强技巧。通过实战案例,帮助开发者快速掌握3D数据可视化技术,提升数据展示效果。

weixin_29031057的博客 281

手把手教你用STM32F103的IO口模拟XY2-100协议驱动数字振镜(附Keil O3优化代码)

本文详细介绍了如何使用STM32F103的IO口模拟XY2-100协议驱动数字振镜,包括协议解析、偶校验算法、寄存器级优化及Keil O3编译下的性能调优。通过硬件配置、核心代码实现和优化技巧,帮助开发者实现高速二维偏转控制,适用于工业激光雕刻和精密定位系统。

weixin_42527401的博客 261

【Kotlin安卓开发高效进阶指南】:揭秘Jetpack Compose实战技巧与性能优化秘诀

掌握Kotlin安卓开发高效进阶之道,揭秘Jetpack Compose实战技巧与性能优化关键策略。涵盖响应式UI构建、重组优化与内存管理,提升开发效率与应用流畅度,适用于现代安卓项目开发,值得收藏。

AlgoPerch的博客 1198

YOLOv13改进策略【多YAML融合】| HWD + DSC3k2_PConv + Detect_MBConv

本文记录的是利用HWD + DSC3k2_PConv + Detect_MBConv改进YOLOv13,分别涉及骨干、颈部和检测头部分的改进。一方面按照此配置,其工作量及创新程度能够满足论文发表的需求,另一方面是一些难以融合多个YAML的举例,以帮助大家理解如何将多个YAML整合到一个YAML中。小波下采样:一个简单但有效的语义分割下采样模块。块由两个主要块组成:无损特征编码块和特征表示学习块。HWD模块与其他下采样模块对比:保留信息能力:传统的下采样方法(如最大池化、平均池化和步幅卷积等)会导致信息丢失,

Limiiiing的博客 496

Android开发之MVVM模式实践(六):协程与网络请求的结合

前言 大家好,我是小益!在经过前两章对协程的介绍后,我们终于又回到了MVVM的封装。协程在Android开发中最常用的场景应该是网络请求了,其次是一些使用Thread的场景,本章内容我们将着重介绍如何将协程与网络请求结合。 推荐 文章将率先在公众号「Code满满」与个人博客「李益的小站」上发布,快来关注吧! 一、viewModelScope的使用 自行创建协程 var uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) 在上述代码中我们

李益 3204

C# 零基础入门知识点汇总

本文属于C#零基础入门之百识百例系列目录文章。此系列文章已经完结,欢迎大家点击文中文章链接查看详情进行学习。

陈言必行 -- Unity游戏开发领域优质博主 5351

爬取B站评论+AI情感分析:Python批量抓数据+BERT正负向分类(全流程)

B站作为年轻群体聚集的内容平台,评论区蕴含海量用户真实态度(如好评、吐槽、中立观点),是情感分析的优质数据源!本文分享完整流程:批量爬取B站视频评论→数据清洗去噪→BERT模型情感分类→可视化分析,基于预训练BERT模型实现高精度正负向判断,新手也能快速复刻情感分析系统! 前置配置(关键!获取B站API访问权限) B站评论通过官方API接口返回,无需复杂解析,只需配置用户Cookie即可正常请求: 2. 完整代码(直接复制运行) 三、使用步骤(5步搞定,新手友好) 步骤1:配置关键参数 打开,在类中修改

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 1069

Kotlin协程实用模版合集

模板场景✅ Repository 封装挂起函数网络/数据库请求✅ ViewModel 中的协程控制生命周期绑定、状态管理✅ 异常处理封装避免崩溃,优雅处理错误✅ 超时机制防止请求卡死✅ 并发请求合并提高加载效率✅ 协程作用域封装通用的launchSafe模板✅ Retrofit + suspend 接口实战开发标准写法✅ 单元测试使用协程保证逻辑正确性。

王景程的博客 904

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

php携程 线程,244,android线程与协程以及携程的使用方法

1.线程就是线程,2.协程本质就是一种线程框架,仅仅是针对Java中的Thread做了一次更友好的封装。让我们更方便的使用Java中的线程才是Kotlin-JVM中协程的真正目的。本质上和Handler,AsyncTask,RxJava 基本是一致的。只不过Kotlin中的协程对于切换线程比他们更方便一些。这其中最核心的是suspend这个Kotlin协程中的关键字。suspend就是挂起的意思,...

weixin_39694174的博客 214

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

上一篇: 【android studio】生成apk包
下一篇: 【Android】Handler消息机制详解
不秃头de程序猿
博客等级 码龄5年 4089粉丝 1478原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值