关于Rxjava for android学习(基础篇)

一 简介

RxJava是实现异步操作的库 那么在有很多异步成熟实现的基础上 我们为什么还要使用RxJava呢?

    异步操作很关键的一点是程序的简洁性,因为在调度过程比较复杂的情况下,异步代码经常会既难写也难被读懂。 Android 创造的 AsyncTask eventBus 和Handler ,其实都是为了让异步代码更加简洁。RxJava 的优势也是简洁,但它的简洁的与众不同之处在于,随着程序逻辑变得越来越复杂,它依然能够保持简洁

二 原理

  RxJava使用的是扩展的观察者模式 

  传统的观察者模式:注册观察者 当被观察者发生变化的时候(发生事件) 能够及时的通知观察者 观察者做出自己的反应 也就是 被观察者-->观察者-->订阅 /注册-->事件 一般传统的观察者 只有点击 和触摸事件 也就是过程事件处理onNext  (也就是 onclick()和onEvent())

  扩展的观察者模式:在传统观察者模式的基础上 添加了完成事件 和错误事件处理(onComplete ()和onError()) RxJava不仅将事件进行独立的处理  还可以将所有事件堪称一个队列 进行统一的管理 。

  规定: 当没有onNext事件进入队列的时候 就触发onComplete ()  

              当事件处理过程中发生异常的时候 就出触发onError()  同时 事件队列 不允许事件在进行进出  也就是队列被终止了

具体实现原理在后面 


三 实现

    RxJava的使用和Builder很像 属于链式编程 介绍几个之后常见的名词 
         Observable:被观察者,也就是消息的发送者
         Observer:  观察者,消息的接收者
         Subscriber:订阅者,观察者的另一种表示
         Scheduler:调度器,进行线程切换

   1.首先 搭建环境 添加Rxjava的依赖库
          compile 'io.reactivex:rxjava:1.2.1'
          compile 'io.reactivex:rxandroid:1.2.1'

   2.下面我们一起来做一个小demo

      2.1 创建一个观察者 (一般我们在考虑的时候 先找中介 告诉中介我们需要什么  具体中介怎么做事情我们是不管的   )

           方式一:使用Observer 

     // 创建观察者
Observer<String> observable=new Observer<String>() {
@Override
public void onCompleted() {
// 当被观察者事件对列正常完成 没有其他事件进入的时候
Log.d(tag, "onCompleted");

}

@Override
public void onError(Throwable e) {
// 当被观察者队列中事件执行过程中 出现错误的时候
Log.d(tag, "onError");
}

@Override
public void onNext(String s) {
// 事件的执行 就相当于我们在onClick方法中重写的事件一样
Log.d(tag, "Item: " + s);
}
};  

      方式二:使用Subscriber(Observer的子类)

Subscriber<String> subscriber=new Subscriber<String>() {
@Override
public void onCompleted() {
// 当被观察者事件对列正常完成 没有其他事件进入的时候
Log.d(tag, "onCompleted");

}

@Override
public void onError(Throwable e) {
// 当被观察者队列中事件执行过程中 出现错误的时候
Log.d(tag, "onError");
}

@Override
public void onNext(String s) {
// 事件的执行 就相当于我们在onClick方法中重写的事件一样
Log.d(tag, "Item: " + s);
}

@Override
public void onStart() {
super.onStart();
}

};
subscriber.unsubscribe();

我们可以看到  对于subscriber 属于observer的子类(但是一般由observer声明的订阅者 也会转成Subscriber) 相对于父类来说 他添加了onstart方法  该方法在观察者被调用 但是 onnext()方法还没有执行前调用  一般在事件没有发送前 做一些准备工作 比如数据的清零或者重置  该方法并不是抽象方法  但是需要注意的是 该方法一般执行在订阅所在的线程  并不能制定线程来执行 所以如果是对UI的操作(比如 进度条等操作) 一般不在该方法进行。如果需要指定的线程来做准备工作,可以使用 doOnSubscribe() 方法


两者的区别:

   Subscriber可以调用unSubscribe()方法 取消订阅 

2.2 创建被订阅者(被观察者)

// 创建被订阅者 在传入参数的时候就可以看出来 是将Observer转化成对应的子类
Observable<String> observable= Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
// 调用订阅者的方法
subscriber.onStart();
subscriber.onNext("hello world");

subscriber.onNext("hello world111111");
subscriber.onError(new Throwable("失败了"));
subscriber.onCompleted();
}
});
}

当Observable被订阅的时候 就会调用call方法  事件会被依次执行

// 订阅
observable.subscribe(subscriber);

除了上述的create方法 Observable 对象的时候 Rxjava海提供了快速创建的方式

Create — 通过调用观察者的方法从头创建一个Observable
Defer — 在观察者订阅之前不创建这个Observable,为每一个观察者创建一个新的Observable
Empty/Never/Throw — 创建行为受限的特殊Observable
From — 将其它的对象或数据结构转换为Observable
Interval — 创建一个定时发射整数序列的Observable
Just — 将对象或者对象集合转换为一个会发射这些对象的Observable
Range — 创建发射指定范围的整数序列的Observable
Repeat — 创建重复发射特定的数据或数据序列的Observable
Start — 创建发射一个函数的返回值的Observable
Timer — 创建在一个指定的延迟之后发射单个数据的Observable ..


一般根据需要进行选择


2.3 订阅

   讲了这么多  还没有讲两者链接起来  者怎么整呢? 使用订阅方法 讲订阅者何被订阅者 连接起来 才能实现他们之间的互动 

  为了练时编程的方便  将格式定义成了  

 // 订阅
observable.subscribe(subscriber);  这么看来 好像反了 被订阅者订阅了订阅者  但是为了连式编程 的方便  只能定义成这样  那么我们就跑跑吧
Observable.subscribe(Subscriber) 的内部实现是这样的(仅核心代码):

// 注意:这不是 subscribe() 的源码,而是将源码中与性能、兼容性、扩展性有关的代码剔除后的核心代码。
// 如果需要看源码,可以去 RxJava 的 GitHub 仓库下载。
public Subscription subscribe(Subscriber subscriber) {
subscriber.onStart();
onSubscribe.call(subscriber);
return subscriber;
}
可以看到,subscriber() 做了3件事:

调用 Subscriber.onStart() 。这个方法在前面已经介绍过,是一个可选的准备方法。
调用 Observable 中的 OnSubscribe.call(Subscriber) 。在这里,事件发送的逻辑开始运行。从这也可以看出,在 RxJava 中,Observable 并不是在创建的时候就立即开始发送事件,而是在它被订阅的时候,即当 subscribe() 方法执行的时候。
将传入的 Subscriber 作为 Subscription 返回。这是为了方便 unsubscribe().

10-13 16:34:52.043 6025-6025/? D/MainActivity: Item: hello world
10-13 16:34:52.043 6025-6025/? D/MainActivity: onCompleted

ok 

除了完整定义 还有不完整定义

除了 subscribe(Observer) 和 subscribe(Subscriber) ,subscribe() 还支持不完整定义的回调,RxJava 会自动根据定义创建出Subscriber 。形式如下:

Action1<String> onNextAction = new Action1<String>() {
// onNext()
@Override
public void call(String s) {
Log.d(tag, s);
}
};
Action1<Throwable> onErrorAction = new Action1<Throwable>() {
// onError()
@Override
public void call(Throwable throwable) {
// Error handling
}
};
Action0 onCompletedAction = new Action0() {
// onCompleted()
@Override
public void call() {
Log.d(tag, "completed");
}
};

// 自动创建 Subscriber ,并使用 onNextAction 来定义 onNext()
observable.subscribe(onNextAction);
// 自动创建 Subscriber ,并使用 onNextAction 和 onErrorAction 来定义 onNext() 和 onError()
observable.subscribe(onNextAction, onErrorAction);
// 自动创建 Subscriber ,并使用 onNextAction、 onErrorAction 和 onCompletedAction 来定义 onNext()、 onError() 和 onCompleted()
observable.subscribe(onNextAction, onErrorAction, onCompletedAction);
简单解释一下这段代码中出现的 Action1 和 Action0。 Action0 是 RxJava 的一个接口,它只有一个方法 call(),这个方法是无参无返回值的;由于 onCompleted() 方法也是无参无返回值的,因此 Action0 可以被当成一个包装对象,将 onCompleted() 的内容打包起来将自己作为一个参数传入 subscribe() 以实现不完整定义的回调。这样其实也可以看做将 onCompleted() 方法作为参数传进了subscribe(),相当于其他某些语言中的『闭包』。 Action1 也是一个接口,它同样只有一个方法 call(T param),这个方法也无返回值,但有一个参数;与 Action0 同理,由于 onNext(T obj) 和 onError(Throwable error) 也是单参数无返回值的,因此 Action1可以将 onNext(obj) 和 onError(error) 打包起来传入 subscribe() 以实现不完整定义的回调。事实上,虽然 Action0 和 Action1在 API 中使用最广泛,但 RxJava 是提供了多个 ActionX 形式的接口 (例如 Action2, Action3) 的,它们可以被用以包装不同的无返回值的方法。


就是使用Action 实现 将方法进行包装  生成不完整的订阅者 


说了一大半天  练时编程呢  让我们连起来 吧

String[] words={"aa","cc","bb"};
// 将数组转化成被订阅者对象 然后注册 订阅者 使用不完整定义定义订阅者对象
Observable.from(words).subscribe(new Action1<String>() {
@Override
public void call(String s) {
Log.d(tag,s);
}
});

跑跑呗 

10-13 16:46:51.213 18046-18046/com.example.rxdemo D/MainActivity: aa
10-13 16:46:51.213 18046-18046/com.example.rxdemo D/MainActivity: cc
10-13 16:46:51.213 18046-18046/com.example.rxdemo D/MainActivity: bb
10-13 16:46:51.233 18046-18046/com.example.rxdemo D/Atlas: Validating map...

这个结果真的是运行出来的 不是我自己瞎编的  然而 并没有什么卵用


四   异步的实现(线程控制 Scheduler)
    4.1     在 RxJava 的默认规则中,事件的发出和消费都是在同一个线程的。也就是说,如果只用上面的方法,实现出来的只是一个同步的观察者模式。观察者模式本身的目的就是『后台处理,前台回调』的异步机制,因此异步对于 RxJava 是至关重要的。而要实现异步,则需要用到 RxJava 的另一个概念: Scheduler 。

Scheduler 我在前面大概提了一下 是调度器  实现线程的切换 总而言之 就是完成线程的控制 就成了 

RxJava的线程原则:

        在不制定线程的情况下 RxJava遵循的是线程不变的原则  也就是 订阅在那个线程 那么就在那个线程执行  如果需要切换线程  就需要使用Scheduler 进行线程的切换 

Scheduler的使用:

      RxJava已经内置了几个常用的Scheduler 能够解决大部分场景的使用  介绍一下子

Schedulers.immediate(): 直接在当前线程运行,相当于不指定线程。这是默认的 Scheduler。

Schedulers.newThread(): 总是启用新线程,并在新线程执行操作。
Schedulers.io(): I/O 操作(读写文件、读写数据库、网络信息交互等)所使用的 Scheduler。行为模式和 newThread() 差不多,区别在于 io() 的内部实现是是用一个无数量上限的线程池,可以重用空闲的线程,因此多数情况下 io() 比 newThread() 更有效率。不要把计算工作放在 io() 中,可以避免创建不必要的线程。
Schedulers.computation(): 计算所使用的 Scheduler。这个计算指的是 CPU 密集型计算,即不会被 I/O 等操作限制性能的操作,例如图形的计算。这个 Scheduler 使用的固定的线程池,大小为 CPU 核数。不要把 I/O 操作放在                                                              computation() 中,否则 I/O 操作的等待时间会浪费 CPU。
另外, Android 还有一个专用的 AndroidSchedulers.mainThread(),它指定的操作将在 Android 主线程运行。
有了这几个 Scheduler ,就可以使用 subscribeOn() 和 observeOn() 两个方法来对线程进行控制了。

subscribeOn(): 指定 subscribe() 所发生的线程,即 Observable.OnSubscribe 被激活时所处的线程。或者叫做事件产生的线程。
observeOn(): 指定 Subscriber 所运行.observeOn(AndroidSchedulers.mainThread()) // 指定事件的回调发生在主线程的线程。或者叫做事件消费的线程。

 /**
* 指定线程运行
*/
private void demo2() {
Observable.just(1,2,3,4) //将整形数据转化成被订阅者对象
.subscribeOn(Schedulers.io()) // 注册线程控制发生在io线程 (使用线程控制 调用io进行输入输出)

.observeOn(AndroidSchedulers.mainThread()) // 指定 Subscriber 的回调发生在主线程

.subscribe(new Action1<Integer>() { //创建订阅者 使用不完整定义(有一个参数 没有返回值 )
@Override
public void call(Integer integer) {
Log.d("demo2","num="+integer);
}
});
}


结果我就不说了  注释写得很清楚

上面这段代码中,由于 subscribeOn(Schedulers.io()) 的指定,被创建的事件的内容 1、2、3、4 将会在 IO 线程发出;而由于observeOn(AndroidScheculers.mainThread()) 的指定,因此 subscriber 数字的打印将发生在主线程 。事实上,这种在subscribe() 之前写上两句 subscribeOn(Scheduler.io()) 和 observeOn(AndroidSchedulers.mainThread()) 的使用方式非常常见,它适用于多数的 『后台线程取数据,主线程显示』的程序策略。

不说扯淡的例子了 说个比较实用的例子  我们都知道图片的加载比较好费时间 有人说了 不是有图片加载框架吗 什么picasso啦 imageLoader了 等等  可以完全ok  自己用着玩吧 但是 底层他们都是需要请求网络加载图片的  一般使用的Asynctask进行实现的  那么我之前也说个 Rxjava最大的用处就是异步  那么 我们底层完全可以根据Rxjava来实现  来个小例子 

Observable.create(new Observable.OnSubscribe<Drawable>() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void call(Subscriber<? super Drawable> subscriber) {
int drawableRes = R.mipmap.ic_launcher;
Drawable drawable = getTheme().getDrawable(drawableRes);
// 设置加载完成前的默认现实图片
subscriber.onNext(drawable);
// 模拟加载图片
SystemClock.sleep(2000);
drawable=getTheme().getDrawable(R.mipmap.icon_baoxiudan_wxdd);
subscriber.onNext(drawable);
// 图片加载完成后进行图片的替换
subscriber.onCompleted();
}
})
.subscribeOn(Schedulers.io()) // 指定 subscribe() 发生在 IO 线程
.observeOn(AndroidSchedulers.mainThread()) // 指定 Subscriber 的回调发生在主线程
.subscribe(new Observer<Drawable>() {
@Override
public void onNext(Drawable drawable) {
imageView.setImageDrawable(drawable);
}

@Override
public void onCompleted() {
}

@Override
public void onError(Throwable e) {
Toast.makeText(MainActivity.this, "Error!", Toast.LENGTH_SHORT).show();
}
});


结果 我就不说了  要不会显得太没档次

4.2 变换 

--1. map() 将String类型转化成Bitmap类型的对象然后将对象进行返回  (事务对象直接变换 最常见 也最容易 )

demo:

  Observable.just(new File(Environment.getExternalStorageDirectory(),"Pictures/JPEG_20160803_003209.jpg").getAbsolutePath())
.map(new Func1<String, Bitmap>() {
@Override
public Bitmap call(String path) {
return getBitmapFromPath(path);//根据路径获取bitMap图片 
}
})
.subscribe(new Action1<Bitmap>() {
@Override
public void call(Bitmap bitmap) {
imageView.setImageBitmap(bitmap); //将图片显示在指定的控件中 
}
});

关于返回值的问题:

   ActionX 是没有返回值的java的接口实现类 (Action0 没有参数 Action1有一个参数 我前面已经说过了  不过我自己又忘记了)

   FunX 是有返回值的java接口实现类  

--2.flatMap()

需求:输出一个数据中所有学生对应的课程信息 

使用map()实现 

ArrayList<String> course=new ArrayList<String>();
course.add("1");
course.add("2");
course.add("3");
Student student1=new Student(123,"zhang",18,course);
Student student2=new Student(124,"zhang1",19,course);
student=new Student[2];
student[0]=student1;
student[1]=student2;
Observable.from(student)
.map(new Func1<Student, Student>() {
@Override
public Student call(Student student) {
return student;
}
})
.subscribe(new Subscriber<Student>() {
@Override
public void onCompleted() {

}

@Override
public void onError(Throwable e) {

}

@Override
public void onNext(Student s) {
// TODO 遍历循环输出对应课程
Log.d(tag,"===="+s);
}
});


遍历 很麻烦 我们就像 能不能不这么烦 直接传递给我课程信息 然后我们直接就输出了 遍历干嘛 

乡亲们 不要着急  我们使用flatmap()实现一下 

Observable.from(student)
.flatMap(new Func1<Student, Observable<String>>() {
//第一个参数便是传入的参数类型 第二个参数是想要得到的参数类型 在当前例子中我们需要得到的是各门学科的名称
@Override
public Observable<String> call(Student student) {
return Observable.from(student.getCourses());
//获取学科数据 转化成被订阅者类型 具体为什么能够直接取出学科中的数据 请看原理
}
})
.subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {

}

@Override
public void onError(Throwable e) {

}

@Override
public void onNext(String name) {
Log.d(tag,name);

}
});


上面例子使用起来是不是就简单了 大兄弟 那下面我们看看原理

flatMap() 的原理是这样的:

 1. 使用传入的事件对象创建一个 Observable 对象;

 2. 并不发送这个 Observable, 而是将它激活,于是它开始发送事件;

 3. 每一个创建出来的 Observable 发送的事件,都被汇入同一个 Observable ,而这个 Observable 负责将这些事件统一交给 Subscriber 的回调方法。这三个步骤,把事件拆成了两级,通过一组新创建的 Observable 将初始的对象『铺平』之后通过统一路径分发了下去。而这个『铺平』就是 flatMap() 所谓的 flat。

来点高级的:

   一般可以在flatmap中进行异步操作 所以可以进行网络请求吧 这就涉及到Retrofit了 我们可以使用retrofit+RxJava来代替ok和EventBus

具体代码我站在下面了 从别的地方站过来的 因为我还没学习Retrofit 尴尬了

  1. networkClient.token() // 返回 Observable<String>,在订阅时请求 token,并在响应后发送 token
  2.     .flatMap(new Func1<String, Observable<Messages>>() {
  3.         @Override
  4.         public Observable<Messages> call(String token) {
  5.             // 返回 Observable<Messages>,在订阅时请求消息列表,并在响应后发送请求到的消息列表
  6.             return networkClient.messages();
  7.         }
  8.     })
  9.     .subscribe(new Action1<Messages>() {
  10.         @Override
  11.         public void call(Messages messages) {
  12.             // 处理显示消息列表
  13.             showMessages(messages);
  14.         }
  15.     });
一般我们在使用ok等网络请求框架的时候使用callback进行结果的回调 那么 广大的乡亲们注意了 我们现在不需要了  我们只需要一条链 通过flatMap就可以搞定了 这样程序会少了几个接口 世界安静了 

下面在看一个省事的throttleFirst():

RxView.clicks(imageView)
.throttleFirst(500, TimeUnit.MILLISECONDS)
.subscribe(new Action1<Void>() {
@Override
public void call(Void aVoid) {
Log.d(tag,"间隔0.5s点击有作用 用来过滤点击的");
}
});

throttleFirst():是用来过滤事件的 在制定间隔时间内的事件时不给予处理的  常用来做都送处理 妈妈再也不用担心恶作剧的频繁点击了 

对于事件序列的处理还有很多 在后续的文中我在介绍(但愿还有后续)


4.3 变换的原理

   在上面的例子中我们都是对事件的序列进行的处理 看似葛优特点 但其实都是对事件序列的管理 也就是重新处理事件 然后在发送 

   在RxJava中他们都是 通过一个共同的方法实现的  lift(操作) 核心代码(不用说 又是从别的地方拿来的):

  

  1. // 注意:这不是 lift() 的源码,而是将源码中与性能、兼容性、扩展性有关的代码剔除后的核心代码。
  2. // 如果需要看源码,可以去 RxJava 的 GitHub 仓库下载。
  3. public <R> Observable<R> lift(Operator<? extends R, ? super T> operator) {
  4.     return Observable.create(new OnSubscribe<R>() {
  5.         @Override
  6.         public void call(Subscriber subscriber) {
  7.             Subscriber newSubscriber = operator.call(subscriber);
  8.             newSubscriber.onStart();
  9.             onSubscribe.call(newSubscriber);//这个onSubscribe是新的的Observable的 通知给原来的Observable的Observable
  10.         }
  11.     });
  12. }

   改方法的返回值是Observable 原来我们有一个Observable 但是通过这个方法之后我们就有了两个Observable  我们成为 原来的 和新的(能理解吧 不能理解就走吧)

简单来说:

  在新的Observable中 通过opertor.call方法 将创建新的Subscriber(在opertor中包含着用户定义的变换 那么这样就相当于将新的变化插入到新的Subscriber中了),并且和旧的Subscriber建立联系 那么此时 newSubscriber就可以向原来的observable进行订阅

注意啦注意啦 父老乡亲们:

    讲述 lift() 的原理只是为了让你更好地了解 RxJava ,从而可以更好地使用它。然而不管你是否理解了 lift() 的原理,RxJava 都不建议开发者自定义 Operator 来直接使用 lift(),而是建议尽量使用已有的 lift() 包装方法(如map() flatMap() 等)进行组合来实现需求,因为直接使用 lift() 非常容易发生一些难以发现的错误。

4.4 compose (对 observable整体的转换) 

 都是转换 那么 lift 和compose的区别是什么呢

 lift:对事件序列的改变 

 compose:针对 observable本身的转换

有的乡亲大哥就说了 貌似没有什么卵用

 那么我们看一个需求 : 如果我们需要对一个observable进行多个转换 ? 老张头说:直接使用多个lift() 不久成了  反正每个的返回值都是Observable 多次转换没毛病

                                       如果我们需要对多个observable进行多个相同的转换? 老李头说:直接封装方法 往里面传observable不久成了   有瑕疵 这样好像限制了observable的灵                                        活性  那怎么办 找小舅子好像不靠谱  那么这个就需要compose上阵了

来个例子:

  1. public class LiftAllTransformer implements Observable.Transformer<Integer, String> {
  2.     @Override
  3.     public Observable<String> call(Observable<Integer> observable) {
  4.         return observable
  5.             .lift1()
  6.             .lift2()
  7.             .lift3()
  8.             .lift4();
  9.     }
  10. }
  11. ...
  12. Transformer liftAll = new LiftAllTransformer();
  13. observable1.compose(liftAll).subscribe(subscriber1);
  14. observable2.compose(liftAll).subscribe(subscriber2);
  15. observable3.compose(liftAll).subscribe(subscriber3);
  16. observable4.compose(liftAll).subscribe(subscriber4);
又是粘来的  关键是代码太多  明白意思就成了 

5.线程控制scheduler(调度器)二

  在4里面我们白活了事件序列的变换 什么map  flatmap 以及他们的基本原理  我们还白活了一下 如何制定线程运行 比如图片加载等 忘了的乡亲们 回头瞅瞅去 别干瞪眼 

 (可以利用 subscribeOn() 结合 observeOn() 来实现线程控制,让事件的产生和消费发生在不同的线程) 结合之前的map()和 flatmap()我们了解了原理 那我们就像能不能产生多次线程的变换呢

  observableOn()指定的是subscriber(订阅者)的线程 那么当然这个订阅者 不一定是subscribe()方法调用的注册者 (通过之前我们讲述 lift原理可知 ) 所以observeOn()执行的subscriber并不是Observable所对应的subscriber 而是他的下一级 或者说是我们上面说的新的subscriber  那么我们就可以这样说 : 如果需要切换线程 那么我们只需要调用observeOn()就可以啦


subscribeOn() 和 observeOn() 都做了线程切换的工作。不同的是, subscribeOn()的线程切换发生在 OnSubscribe 中,即在它通知上一级 OnSubscribe 时,这时事件还没有开始发送,因此 subscribeOn() 的线程控制可以从事件发出的开端就造成影响;而 observeOn() 的线程切换则发生在它内建的 Subscriber 中,即发生在它即将给下一级Subscriber 发送事件时,因此 observeOn() 控制的是它后面的线程。

5.1扩展 

最近项目太忙了 未完待续...









 







      






 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值