玩转RxJava3,领略设计之美!

学最好的别人,做最好的自己

点击关注后端面试那些事Java面经都在这里玩转RxJava3,领略设计之美!插图1

玩转RxJava3,领略设计之美!插图3
后端面试那些事
专注分享后端干货!面向大厂,一起进步!
5篇原创内容
公众号

来源:https://blog.csdn.net/qq_29856589/article/details/121031029

面对逐渐奋力起追的协程,RxJava还能有一战之地吗?难道只能停留在简单用用的情况下不能另RxJava的使用变得更为简便。为了让前后端的同学们能够更加易用RxJava,我将我与沈哲老哥在Kotlin进阶实战书中,我是如何利用抽象思维从搭建RxTask的抽象到实现,一步一步给同学们进行抽丝剥茧。而且RxTask现已开源欢迎使用。

https://github.com/KilleTom/RxTask

1

RxJava真的难用吗?

每次运用函数响应式编程写起异步时,总感觉欠缺点意思?总感觉不够简便?总感觉有些臃肿?没错同学们你需要变得更强了也需要变得变秃了,什么你不想变秃,那就赶紧躺好且听我来讲解。RxJava即好用也难用。好用在于有过多的操作符以及线程操作空间,但也难用,因为太多的操作符以及线程操作空间这就间接导致学习成本会呈曲线上升。

2

万物皆对象融入异步框架

在开始构建我们的RxTask之前我们先试想一下这句话:万物皆对象。看看我们能想到啥:能否将将大部分需要进行异步处理业务逻辑抽象看成一个抽象运算逻辑?而成功失败只是一个运算结果的输出?

接下来请看:

玩转RxJava3,领略设计之美!插图5

3

抽象的构建

在这里我们将学习如何利用抽象思维构建一个抽象的异步框架。

基本状态的构建

在进入RxTask实现前,先将异步的基本状态抽取出来。俗话说的好:要想轮子玩得转,齿轮得通用。

基本状态譬如抽象成如下面这张期望图:

玩转RxJava3,领略设计之美!插图7

那么结合上图我们可以这样去构建我们的抽象代码:

//利用接口抽象ITask//利用泛型定义Task的目标interfaceITask<RESULT>{funstart()funcancel()}

当我们构建了基本的奠基石后,那我们还需点什么呢?我们是否缺少了对一个异步的状态管理呢?

状态管理的构建

譬如我们能否这样想象呢基于ITask,构建出一个抽象的ISuperTask它里面存在ITask的异步状态描述以及管理呢?如下图:

玩转RxJava3,领略设计之美!插图9

那么结合上文以及上图我们可以这样去构建ISuperTask。

abstractclassISuperTask<RESULT> :ITask<RESULT>{//利用它去针对Task当前进行判断protectedvarTASK_CURRENT_STATUS = NORMAL_STATUSoverridefunstart(){if(TASK_CURRENT_STATUS == RUNNING_STATUS) {logD("TASK already start")return}TASK_CURRENT_STATUS = RUNNING_STATUS}overridefuncancel(){TASK_CURRENT_STATUS = CANCEL_STATUS}protectedopenfunfinalResetAction(){}//每一个Task runnning 状态判断可能都是不一样的 抽象成一个方法abstractfunrunning():Boolean//use this can be judge task can be running, it was throw error when task was stopfunjudgeRunningError(){if(!running()) {TASK_CURRENT_STATUS = STOP_STATUSthrowRxTaskRunningException("task was stop")}}@ThrowsfunthrowCancelError(){throwRxTaskCancelException("TASK Cancel")}// 定义Task状态码companionobject{valNORMAL_STATUS =0x00valRUNNING_STATUS =0x100valCANCEL_STATUS =0x200valERROR_STATUS =0x300valDONE_STATUS =0x400valSTOP_STATUS =0x500}protectedfunlogD(message:String){if(BuildConfig.DEBUG)Log.d(this::class.java.simpleName,message)}}

运算抽象的构建

写到这里的时候,不禁有些同学就会提出疑问了,这个ISuperTask为什么这么奇怪呀?,为什么没有产生结果的方法或者概念呢?,有了泛型RESULT但是却没有实现它的抽象方法?面对同学来自灵魂的三连问。

容我一一解释:有些Task针对的场景是不需要结果返回的例如定时器TimerTask只针对做一些定时性的任务;为了更好的应对这一场场景,IEvaluation凭空出世。IEvaluation仅仅负责运算产生RESULT。ISuperTask则负责构建一些底层基类Task的奠基石。

那么我们的IEvaluation可以这样子去实现:

interfaceIEvaluation<RESULT>{funevaluationResult(): RESULT}

既然IEvaluation、ISuperTask有了那么我们可以这样构建这样一个概念的ISuperEvaluationTask一个负责运算产生结果的的Task。

玩转RxJava3,领略设计之美!插图11

结合上图那么我们可以这样子去构建我们的ISuperEvaluationTask

abstractclassISuperEvaluationTask<RESULT> :IEvaluation<RESULT>,ISuperTask<RESULT>() {overridefunstart(){if(TASK_CURRENT_STATUS == RUNNING_STATUS) {logD("TASK already start")return}TASK_CURRENT_STATUS = RUNNING_STATUS}overridefuncancel(){TASK_CURRENT_STATUS = CANCEL_STATUS}//继续抽象最终运算结果的动作用来给后面的子类去实现它产生自己专属的运算过程并返回对应结果//可以是成功的结果也可以抛出运行异常@ThrowsabstractfunevaluationAction(): RESULTprotectedvarresultAction: ((RESULT) ->Unit)? =null//Task外部成功后最终执行的动作openfunsuccessAction(resultAction: (RESULT)->Unit): ISuperEvaluationTask {this.resultAction = resultActionreturnthis}protectedvarfailAction: ((Throwable) ->Unit)? =null//Task失败后最终执行的动作openfunfailAction(failAction: (Throwable)->Unit): ISuperEvaluationTask {this.failAction = failActionreturnthis}// 计算生成ResultoverridefunevaluationResult(): RESULT {returnevaluationAction()}//内部错误回调protectedfunerrorAction(t:Throwable){if(tisRxTaskCancelException) {TASK_CURRENT_STATUS = CANCEL_STATUS}else{//用标记位判断当前Task是否真正处于运行状态if(TASK_CURRENT_STATUS == RUNNING_STATUS){failAction?.invoke(t)}TASK_CURRENT_STATUS = ERROR_STATUS}finalResetAction()}//结果回调protectedfunresultAction(result:RESULT){//用标记实现判断当前Task是否真正处于运行的状态并标记位DONEif(TASK_CURRENT_STATUS == RUNNING_STATUS) {resultAction?.invoke(result)TASK_CURRENT_STATUS = DONE_STATUS}finalResetAction()}}

写到这里的时候我们的RxTask的基础抽象已经实现好了。接下来让我们磨刀霍霍迈向RxJava,利用它实现RxTask。

4

基于抽象结合RxJava实现多种异步Task

SingleTask

还记得我们实现的奠基石吗?现在我们就要开始动手使用RxJava于奠基石实现一个专注运算结果的Task。

在实现这个Task之前,还记得我讲解的过的Scheduler,没错这里我们就要把这个Scheduler用上了,利用Scheduler去实现线程的调度以及异步。

那么你还记得五大兄弟的Maybe吗?对了这里我们也会用到Maybe这个大兄弟,Maybe的好处在于:Maybe是一种从RxJava.2.x中出现的一种新的类型,近似的将它理解为Single与Completable的结合!利用这一个特性我们可以封装出只针对运算结果的产生以及运算过程中异常的产生这一问题。

//利用私有的构造函数避免在其他地方使用不规范创建TaskclassRxSingleEvaluationTaskTask<RESULT>privateconstructor(privatevalrunnable: (RxSingleEvaluationTaskTask) -> RESULT) : ISuperEvaluationTask() {privatevalresultTask: Maybeprivatevardisposable: Disposable? =nullinit {//利用Maybe 与 runnable 构建出一个运算的MayberesultTask = Maybe.create { emitter ->try{if(TASK_CURRENT_STATUS == CANCEL_STATUS) {emitter.onError(RxTaskCancelException("Task cancel"))}valaction = evaluationAction()if(TASK_CURRENT_STATUS == CANCEL_STATUS) {emitter.onError(RxTaskCancelException("Task cancel"))}emitter.onSuccess(action)}catch(e: Exception) {emitter.onError(e)}}}//这里就负责运算的动作overridefunevaluationAction(): RESULT {if(!running())throwRxTaskRunningException("Task unRunning")returnrunnable.invoke(this)}overridefunstart(){super.start()disposable = resultTask.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()).subscribe({ resultAction(it) },{ errorAction(it) })}overridefuncancel(){TASK_CURRENT_STATUS = CANCEL_STATUSfinalResetAction()}overridefunrunning():Boolean{valdis = disposable ?:returnfalseif(TASK_CURRENT_STATUS == RUNNING_STATUS) {return!dis.isDisposed}returnfalse}overridefunfinalResetAction(){disposable?.dispose()disposable =null}companionobject{funcreateTask(taskRunnable:(RxSingleEvaluationTaskTask<RESULT>)-> RESULT): RxSingleEvaluationTaskTask {returnRxSingleEvaluationTaskTask(taskRunnable)}}}

我们已经把RxSingleEvaluationTaskTask实现了那么,怎么使用呢?接下来我们来看看一段请求新闻的Api业务逻辑的实现。

// 创建一个TaskvalsingleTask = RxSingleEvaluationTaskTask.createTask {valresult = okHttpClient.newCall(createRequest(createNewUrl("top"))).execute()valbody = result.body ?:throwRuntimeException("body null")return@createTaskGson().fromJson(body.string(), JsonObject::class.java)}//点击一个叫single的button 触发这个Task真正的请求网络并回调single.setOnClickListener {singleTask.successAction {Log.i("KilleTom","$it")}.failAction {it.printStackTrace()}.start()}

看到没有通过利用RxJava去实现我们的奠基石,也可以这么简单清爽实现出一个简单易用RxTask。

ProgressTask

有没有一种这样的一个场景,需要专注于运算某一种事物并返回结果,且过程中需要将一些进度信息返回?

例如:解压超大型文件、上传文件、下载文件、升级硬件(如电脑的BIOS升级)等等都符合上诉场景。

那么利用RxJava我们怎么实现进度信息的返回呢?在RxJava中有这样一个Subject:PublishSubject;

PublishSubject:与普通的Subject不同,在订阅时并不立即触发订阅事件,而是允许我们在任意时刻手动调用onNext,onError(),onCompleted来触发事件。

利用这样一个特性我们可以实现出进度推送功能。然后在利用Maybe去做结果的运算。那么一个ProgressTask就会被实现出来。

部分核心代码:

classRxProgressEvaluationTaskTask<PROGRESS, RESULT>privateconstructor(createRunnable: (RxProgressEvaluationTaskTask) -> RESULT) :ISuperEvaluationTask() {privatevarcreateRunnable: ((RxProgressEvaluationTaskTask) -> RESULT)? =createRunnableprivatevalresultTask: MaybeprivatevarresultDisposable: Disposable? =nullprivatevalprogressTask: PublishSubject = PublishSubject.create()privatevarprogressDisposable: Disposable? =nullprivatevarprogressAction: ((PROGRESS) ->Unit)? =nullinit {resultTask = Maybe.create { emitter ->try{if(TASK_CURRENT_STATUS == CANCEL_STATUS) {emitter.onError(RxTaskCancelException("Task cancel"))}valaction = evaluationAction()if(TASK_CURRENT_STATUS == CANCEL_STATUS) {emitter.onError(RxTaskCancelException("Task cancel"))}emitter.onSuccess(action)}catch(e: Exception) {emitter.onError(e)}}}overridefunstart(){super.start()resultDisposable = resultTask.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()).subscribe({ resultAction(it) },{ errorAction(it) })progressDisposable = progressTask.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()).subscribe({progressAction?.invoke(it)},{//nothing to do by error})}overridefunevaluationAction(): RESULT {if(!running())throwRxTaskRunningException("Task unRunning")valresult = runnable.invoke(this)//throw RunningException when result by evaluationif(!running())throwRxTaskRunningException("Task unRunning")returnresult}overridefuncancel(){super.cancel()finalResetAction()}funprogressAction(action: (PROGRESS)->Unit): RxProgressEvaluationTaskTask {progressAction = actionreturnthis}funpublishProgressAction(progress:PROGRESS){if(running())progressTask.onNext(progress)}}

同样我们利用请求新闻接口去实现这样一个场景:有多种新闻类型需要循环请求,每请求成功一次就就返回一个JsonObject,循环请求只要发生异常或者失败直接中断整个异步;

valprogressTask = RxProgressEvaluationTaskTask.createTaskBoolean> { task ->valtypes = arrayListOf("top","shehui","guonei")types.forEach { value ->valresult = okHttpClient.newCall(createRequest(createNewUrl(value))).execute()valbody = result.body ?:throwRuntimeException("body null")valjsonObject = Gson().fromJson(body.string(), JsonObject::class.java)Log.d("KilleTom","推送新闻类型$value")task.publishProgressAction(jsonObject)}return@createTasktrue}//点击按钮触发网络请求progress.setOnClickListener {progressTask.progressAction {Log.i("KilleTom","收到进度,message:$it")}.successAction {Log.i("KilleTom","Done")}.failAction {Log.i("KilleTom","error message:${it.message ?: "unknown"}")}.start()}

TimerTask

针对于一些定时的异步任务场景,还记得ISuperTask吗?ISuperTask就是这个TimerTask的基类了,当然我们还需要用到Ticker、interval;Ticker?讲到这里同学们肯定心里大有疑问了?

一个TimerTask,循环了多少定时任务以及Timer起始时间、当前的时间,如何确定?这里就运用了Ticker,利用一个Ticker将这个TimerTask的一些信息记录下来并且每次都可以由TimerTask去获取到并针对它(Ticker)去做一些业务逻辑处理。

openclassTimerTick{varstartTime:Long=-1varcurrentTime:Long=-1varcountTimes:Long=-1overridefuntoString(): String {return"TimerTick(startTime=$startTime, currentTime=$currentTime, currentTimes=$countTimes)"}}

我们的Ticker有了那么我怎么去拓展这样TimerTask里面的Ticker呢?

openclassRxTimerTaskprivateconstructor(privatevaltimerAction: (RxTimerTask) ->Unit): ISuperTask<Long>() {//通过使用 createTick 创建出相应的Tickerprotectedvalticker = createTick()//可重写Ticker的创建根据自己的需求继承RxTimerTask在实现业务逻辑需要的TickeropenfuncreateTicker(): TimerTicker {returnTimerTicker()}//把Ticker暴露在外方便调用使用openfungetTimeTicker(): TimerTicker {returnticker}}

那么Ticker的拓展到这已经完成了,那么我们的的interval,可以这样去实现。

openclassRxTimerTaskprivateconstructor(privatevaltimerAction: (RxTimerTask) ->Unit): ISuperTask<Long>() {privatevartimerDisposable: Disposable? =nullprivatevarworkDelayTime:Long= workDelayDefaultTimeprivatevarworkIntervalTime:Long= workIntervalDefaultTimeprivatevarworkTimeUnit = workDefaultUnitprivatevarworkScheduler = getDefaultWorkScheduler()//最终启动的时候再去利用Flowable.interval创建出相应的定时器overridefunstart(){super.start()ticker.startTime = System.currentTimeMillis()timerDisposable =Flowable.interval(workDelayTime, workIntervalTime, workTimeUnit).observeOn(workScheduler).subscribe({ times ->if(running()) {ticker.countTimes = timesticker.currentTime = System.currentTimeMillis()timerAction.invoke(this)}},{ errorAction?.invoke(it) })}}

有了上述那一步的时候,是不是感觉我们还少了些什么?对你的直觉是对的,一个TimerTask它的间隔时间以及启动是否延迟还有工作线程难道就这样固定写了吗?对于这样的代码我们要说不,坚决拓展到底,为了简单复用,我们还可以这样子写道:

openclassRxTimerTaskprivateconstructor(privatevaltimerAction: (RxTimerTask) ->Unit): ISuperTask<Long>() {funsetDelayTime(time:Long): RxTimerTask {if(!running()) {workDelayTime = time}returnthis}funsetIntervalTime(time:Long): RxTimerTask {if(!running()) {workIntervalTime = time}returnthis}funsetTimeUnit(unit:TimeUnit): RxTimerTask {if(!running()) {workTimeUnit = unit}returnthis}funsetTaskScheduler(scheduler:Scheduler): RxTimerTask {if(!running()) {workScheduler = scheduler}returnthis}}

在未启动TimerTask之前我们利用这些方法去链式调用并且为延迟时间、间隔时间、工作线程的设置,去一一配置尽量贴合常用的业务场景。

到这我们的TimerTask基本就算大功完成了。那么剩下我们调用呢?

//获取android的网络管理valmanager = application.getSystemService(Context.CONNECTIVITY_SERVICE)asConnectivityManagervaltimerTask = RxTimerTask.createTask {task->if(task.getTimeTicker().countTimes >=10){task.cancel()}//利用TimerTask 监测一段时间的网络变换if(Build.VERSION.SDK_INT >=23) {valnetwork = manager.activeNetworkif(network ==null){Log.d("KilleTom","network null connect false")return@createTask}valconnectInfo = manager.getNetworkCapabilities(network)if(connectInfo ==null){Log.d("KilleTom","connectInfo null connect false")return@createTask}valisInterNet =connectInfo.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)Log.d("KilleTom","$isInterNet")}return@createTask}.setDelayTime(0L).setIntervalTime(1000).setTaskScheduler(Schedulers.computation())//最后点击按钮调用timer.setOnClickListener {timerTask.start()}

至此RxTask的基础实现已经实现完了,那么我们该如何改进试用多平台呢?且听我缓缓描叙。

5

RxTask的改进以及优化

RxTask 针对 Java、android 平台进行适应

我们的之前完成的RxTask吗?由于使用以下这段代码导致了RxTask仅仅适用于Android平台。

disposable = resultTask.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()).subscribe({ resultAction(it) },{ errorAction(it) })

那么这就是导致RxTask也必须依赖了rxandroid,当我们的RxTask可以运行在Java后端上并且不需要额外依赖rxandroid的时候,我们可以这样子改进:

首先我们可以构建出一个概念:有一个抽象的对象里面包含RxTask指定工作的两个线程,具体是哪个线程由开发者决定,最后传入RxTask,由RxTask取出设置其内部线程。

//分别定义 观察者模式的 工作线程interfaceRxTaskScheduler{fungetObserveScheduler(): SchedulerfungetSubscribeScheduler(): Scheduler}

抽象的概念有了,那么此时又可能存在这样一个需求,在某些场景下可能需要一个全局的Manager它负责获取一个RxTaskScheduler,用作全局性质的RxTask的线程管理,那么我们可以这样子去实现它:

objectRxTaskSchedulerManager {privatevarrxTaskScheduler: RxTaskScheduler = RxDefaultScheduler()//设置全局的线程funsetLocalScheduler(rxTaskScheduler:RxTaskScheduler){RxTaskSchedulerManager.rxTaskScheduler = rxTaskScheduler}//获取全局线程fungetLocalScheduler():RxTaskScheduler{returnrxTaskScheduler}}//默认针对Java平台实现一个RxTaskSchedulerclassRxDefaultScheduler:RxTaskScheduler {overridefungetObserveScheduler(): Scheduler {returnSchedulers.computation()}overridefungetSubscribeScheduler(): Scheduler {returnSchedulers.newThread()}}

但我们将Scheduler抽离出来后,那么就可以针对性将rxandroid和Scheduler封装成一个针对android平台拓展出适用于RxTask一个拓展库libRxTaskAndroidExpand

classRxAndroidDefaultScheduler:RxTaskScheduler {overridefungetObserveScheduler(): Scheduler {returnAndroidSchedulers.mainThread()}overridefungetSubscribeScheduler(): Scheduler {returnSchedulers.newThread()}}

有了我们定义的RxTaskScheduler那么我们的Single、Progress就可以这样子改进来实现。

classRxSingleEvaluationTask<RESULT>internalconstructor(privatevalrunnable: (RxSingleEvaluationTask) -> RESULT,privatevaltaskScheduler: RxTaskScheduler) : ISuperEvaluationTask() {overridefunstart(){super.start()// 利用rxScheduler去获取工作线程disposable = resultTask.subscribeOn(taskScheduler.getSubscribeScheduler()).observeOn(taskScheduler.getObserveScheduler()).subscribe({ resultAction(it) },{ errorAction(it) })}}classRxProgressEvaluationTask<PROGRESS, RESULT>privateconstructor(privatevalcreateRunnable: (RxProgressEvaluationTask) -> RESULT,privatevalrxTaskScheduler: RxTaskScheduler) :ISuperEvaluationTask() {overridefunstart(){super.start()resultDisposable = resultTask.subscribeOn(rxTaskScheduler.getSubscribeScheduler()).observeOn(rxTaskScheduler.getObserveScheduler()).subscribe({ resultAction(it) },{ errorAction(it) })progressDisposable = progressTask.subscribeOn(rxTaskScheduler.getSubscribeScheduler()).observeOn(rxTaskScheduler.getObserveScheduler()).subscribe({progressAction?.invoke(it)},{//nothing to do by error})}}

日志输出的改进

还记得我们之前实现的ISuperTask吗?里面的日志输出使用了android的方法,如下。

abstractclassISuperTask<RESULT> :ITask<RESULT>{protectedfunlogD(message:String){if(BuildConfig.DEBUG)Log.d(this::class.java.simpleName,message)}}

为了抽离日志输出更好的适配两个平台我们可以采用类似RxTaskScheduler的实现思路。

//定义一个全局的LogManager通过它获取以及设置全局的LogAction达到控制输出的具体实现classRxTaskLogManagerprivateconstructor() {privatevarlogAction: RxLogAction =object: RxLogAction {}funlogD(iSuperTask:ISuperTask<*>, message:String){logAction.d(iSuperTask, message)}funset(rxLogAction:RxLogAction){this.logAction = rxLogAction}companionobject{valinstantsbylazy { RxTaskLogManager() }}}//定义logAction 负责日志输出interfaceRxLogAction{fund(objects:ISuperTask<*>, message:String){System.out.println("${objects::class.java.simpleName}->Message:$message")}}

还记得libRxTaskAndroidExpand库吗?没错在这里我们也需要对RxLogAction进行拓展适配。

实现思路与RxTaskSceduler的拓展大体相同。

classRxAndroidDefaultLogActionprivateconstructor(): RxLogAction {overridefund(objects:ISuperTask<*>, message:String){if(BuildConfig.DEBUG) {Log.d(objects::class.java.simpleName,message)}}companionobject{valinstantbylazy { RxAndroidDefaultLogAction() }}}

那么针对android平台,我们如何能够快速的全局初始化我们的一些RxTask的配置呢?

这里就可以使用这样的一种思维专门针对android平台进行初始化的实现:

classRxTaskAndroidDefaultInitprivateconstructor() {fundefaultInit(){RxTaskSchedulerManager.setLocalScheduler(RxAndroidDefaultScheduler())RxTaskLogManager.instants.set(RxAndroidDefaultLogAction.instant)}companionobject{valinstantbylazy { RxTaskAndroidDefaultInit() }}}

通过使用RxTaskAndroidDefaultInit的单例,快速将针对android进行初始化。

6

RxTask总结

通过抽象思维去分解一个异步框架的所需基本动作,如何基于一个基本动作的抽象去横纵向的拓展构建出一个异步框架。

基于抽象的构建,利用RxJava去对前后端进行适配兼容。以此达到前后端通用的效果。

RxTask可用在实际项目运用目前其地址:https://github.com/KilleTom/RxTask。本作者还会继续维护RxTask便于同学们工作上使用。

有了HTTP,为什么还要RPC?

大文件上传下载、分片、断点续传教程

换Macbook M1芯片之后,IDEA很卡是怎么回事?

面试官:@Validated 和 @Valid 的区别是什么?

玩转Java注解:元注解、内置注解、自定义注解的原理和实现

重要提示:关注我回复【简历】,限时免费领取优质word版Java简历模板!

玩转RxJava3,领略设计之美!插图13

玩转RxJava3,领略设计之美!插图3
后端面试那些事
专注分享后端干货!面向大厂,一起进步!
5篇原创内容
公众号

点击阅读原文,领取 2022 年最新免费学习资料

↓↓↓

原创文章 玩转RxJava3,领略设计之美!,版权所有
如若转载,请注明出处:https://www.itxiaozhan.cn/20228709.html

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注