Android进阶知识:RxJava相关,腾讯T3面试官透露

@Override

public void subscribeActual(final Observer<? super T> observer) {

final SubscribeOnObserver parent = new SubscribeOnObserver(observer);

observer.onSubscribe(parent);

parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));

}

ObservableSubscribeOnsubscribeActual方法中流程和之前的也很类似,这次是先创建了一个SubscribeOnObserver对象,将观察者对象传入,接着同样先调用了observer.onSubscribe方法,然后将传入的SubscribeOnObserver封装入了一个SubscribeTask对象中,接着调用了scheduler.scheduleDirect方法再将返回结果得到的Disposable设置到SubscribeOnObserver中。下面一个方法一个方法看。首先是创建SubscribeTask

final class SubscribeTask implements Runnable {

private final SubscribeOnObserver parent;

SubscribeTask(SubscribeOnObserver parent) {

this.parent = parent;

}

@Override

public void run() {

source.subscribe(parent);

}

}

SubscribeTaskObservableSubscribeOn的内部类,其实现很简单就是实现了一个Runnable接口,构造方法中传入了SubscribeOnObserver对象,在其run方法中调用了ObservableSubscribeOn中的成员变量sourcesubscribe方法。这个source是在创建ObservableSubscribeOn时传入的,根据前面的代码可以找到是在subscribeOn方法中创建的对象并且这个source对应传入的是当前这个Observable对象即通过Observable.create获得的被观察者对象,其实现之前看过是一个ObservableCreate所以这里就和之前一样又会走到了其父类Observablesubscribe方法中,继而调用ObservableCreatesubscribeActual方法,之后最终会调用到观察者的对应onNext等方法,不过此时的观察者不直接是在使用时创建传入的Observer,而是之前看到的SubscribeOnObserver类型,不过其中的onNext等方法还是调用了在使用时创建传入的Observer的对应方法。

static final class SubscribeOnObserver extends AtomicReference implements Observer, Disposable {

private static final long serialVersionUID = 8094547886072529208L;

final Observer<? super T> downstream;

final AtomicReference upstream;

SubscribeOnObserver(Observer<? super T> downstream) {

this.downstream = downstream;

this.upstream = new AtomicReference();

}

@Override

public void onNext(T t) {

downstream.onNext(t);

}

@Override

public void onError(Throwable t) {

downstream.onError(t);

}

@Override

public void onComplete() {

downstream.onComplete();

}

}

下面接着看到scheduleDirect这个方法,在创建好SubscribeTask之后调用了scheduleDirect方法。这里的scheduler就是subscribeOn中传入的,对应开始例子中的Schedulers.newThread

public static Scheduler newThread() {

return RxJavaPlugins.onNewThreadScheduler(NEW_THREAD);

}

// 静态成员变量NEW_THREAD

static final Scheduler NEW_THREAD;

NEW_THREAD = RxJavaPlugins.initNewThreadScheduler(new NewThreadTask());

进入Schedulers.newThread一步步跟踪,看到newThread方法返回静态成员变量中的NEW_THREAD,而NEW_THREAD又是通过NewThreadTask创建。

static final class NewThreadTask implements Callable {

@Override

public Scheduler call() throws Exception {

return NewThreadHolder.DEFAULT;

}

}

static final Scheduler DEFAULT = new NewThreadScheduler();

继续跟踪查看发现NewThreadTask实际是实现了Callable接口,其call方法中返回了静态内部类中的NewThreadHolder.DEFAULT。这个DEFAULT的实现类型为NewThreadScheduler。至此终于找到了我们传入的Scheduler的真正实现类。于是继续看其scheduleDirect方法。

public Disposable scheduleDirect(@NonNull Runnable run) {

return scheduleDirect(run, 0L, TimeUnit.NANOSECONDS);

}

public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {

final Worker w = createWorker();

final Runnable decoratedRun = RxJavaPlugins.onSchedule(run);

DisposeTask task = new DisposeTask(decoratedRun, w);

w.schedule(task, delay, unit);

return task;

}

scheduleDirect方法是在其父类中实现的,看到其中进而调用了同名重载方法,方法中首先是调用createWorker方法创建一个Worker。这个方法的实现就是在NewThreadScheduler中了。

public Worker createWorker() {

return new NewThreadWorker(threadFactory);

}

createWorker方法中只做了一件事就是创建返回了一个NewThreadWorker

public class NewThreadWorker extends Scheduler.Worker implements Disposable {

private final ScheduledExecutorService executor;

volatile boolean disposed;

public NewThreadWorker(ThreadFactory threadFactory) {

executor = SchedulerPoolFactory.create(threadFactory);

}

}

NewThreadWorker中看到创建了一个线程池,再回到scheduleDirect方法,创建完Worker后将传入的RunnableSubscribeTask进行一个装饰得到新的Runnable对象。接着将Worker和新的Runnable封装到一个DisposeTask对象中。

static final class DisposeTask implements Disposable, Runnable, SchedulerRunnableIntrospection {

@NonNull

final Runnable decoratedRun;

@NonNull

final Worker w;

@Nullable

Thread runner;

DisposeTask(@NonNull Runnable decoratedRun, @NonNull Worker w) {

this.decoratedRun = decoratedRun;

this.w = w;

}

@Override

public void run() {

runner = Thread.currentThread();

try {

decoratedRun.run();

} finally {

dispose();

runner = null;

}

}

}

DisposeTask同样实现了Runnable接口,在run方法中调用了从构造传入的decoratedRunrun方法执行任务。回到最后一步,调用Workerschedule方法,这里就对应的NewThreadWorkerschedule方法。

public Disposable schedule(@NonNull final Runnable action, long delayTime, @NonNull TimeUnit unit) {

if (disposed) {

return EmptyDisposable.INSTANCE;

}

return scheduleActual(action, delayTime, unit, null);

}

schedule方法中又进一步调用了其scheduleActual方法

public ScheduledRunnable scheduleActual(final Runnable run, long delayTime, @NonNull TimeUnit unit, @Nullable DisposableContainer parent) {

Runnable decoratedRun = RxJavaPlugins.onSchedule(run);

ScheduledRunnable sr = new ScheduledRunnable(decoratedRun, parent);

if (parent != null) {

if (!parent.add(sr)) {

return sr;

}

}

Future<?> f;

try {

if (delayTime <= 0) {

f = executor.submit((Callable)sr);

} else {

f = executor.schedule((Callable)sr, delayTime, unit);

}

sr.setFuture(f);

} catch (RejectedExecutionException ex) {

if (parent != null) {

parent.remove(sr);

}

RxJavaPlugins.onError(ex);

}

return sr;

}

scheduleActual方法里看到又将decoratedRunDisposableContainer封装成ScheduledRunnable最后将这个ScheduledRunnable 交给构造函数中创建的线程池去运行,最终就会执行到前面看过的SubscribeTask中的run方法完成订阅逻辑,调用观察者的onNext等方法。到这里就看出最终的source.subscribe是会通过线程池切换到子线程中去执行了。

通过查看subscribeOn方法源码可以发现,方法里实际上是在前一个创建的ObservableCreate外面包了一层,把它包成一个ObservableSubscribeOn对象,同样的原先的Observer也被包了一层包成一个SubscribeOnObserver对象,而线程切换的工作是由Scheduler完成的。

4.2 observeOn

接着再来看看切换回主线程的方法observeOn,还是先修改使用代码,查看运行日志。

Observable observable2 = observable0.observeOn(AndroidSchedulers.mainThread());

Log.d(getClass().getName(), Thread.currentThread().getName() + " observable2:"+observable2.getClass().getName());

observable2.subscribe(observer);

运行日志:

接着还是进入来看源码。

public final Observable observeOn(Scheduler scheduler) {

return observeOn(scheduler, false, bufferSize());

}

public final Observable observeOn(Scheduler scheduler, boolean delayError, int bufferSize) {

ObjectHelper.requireNonNull(scheduler, “scheduler is null”);

ObjectHelper.verifyPositive(bufferSize, “bufferSize”);

return RxJavaPlugins.onAssembly(new ObservableObserveOn(this, scheduler, delayError, bufferSize));

}

这里看到observeOn方法里调用了重载方法,方法中还是同一个套路,不过这里创建的又是另一个对象ObservableObserveOn了。根据前面的经验这里就又是将前一个Observable传递到ObservableObserveOn中的成员变量source上,这里看到就是构造函数中的第一个参数。接着还是会调用subscribe与观察者建立订阅关系进而会执行到ObservableObserveOn对象的subscribeActual方法。

@Override

protected void subscribeActual(Observer<? super T> observer) {

if (scheduler instanceof TrampolineScheduler) {

source.subscribe(observer);

} else {

Scheduler.Worker w = scheduler.createWorker();

source.subscribe(new ObserveOnObserver(observer, w, delayError, bufferSize));

}

}

subscribeActual方法中判断了scheduler的类型,这里的scheduler就是由AndroidSchedulers.mainThread()传入的,于是先来看一下这个方法。

public static Scheduler mainThread() {

return RxAndroidPlugins.onMainThreadScheduler(MAIN_THREAD);

}

private static final Scheduler MAIN_THREAD = RxAndroidPlugins.initMainThreadScheduler(

new Callable() {

@Override public Scheduler call() throws Exception {

return MainHolder.DEFAULT;

}

});

private static final class MainHolder {

static final Scheduler DEFAULT = new HandlerScheduler(new Handler(Looper.getMainLooper()), false);

}

mainThread开始看,发现代码调用逻辑和之前的Schedulers.newThread方法类似,最终会返回一个HandlerScheduler而这个Scheduler中的Handler则是主线程的Handler,看到这里就能猜想到了,后面观察者的对应方法一定是由这个Handler来切换到主线程执行的。回到subscribeActual方法。

@Override

protected void subscribeActual(Observer<? super T> observer) {

if (scheduler instanceof TrampolineScheduler) {

source.subscribe(observer);

} else {

Scheduler.Worker w = scheduler.createWorker();

source.subscribe(new ObserveOnObserver(observer, w, delayError, bufferSize));

}

}

这里判断完类型会走else中的方法首先还是会调用HandlerSchedulercreateWorker方法创建一个Worker

@Override

public Worker createWorker() {

return new HandlerWorker(handler, async);

}

这里是个HandlerWorker其中具体方法后面再看。接着上面创建完Worker后同样还是一样调用source.subscribe创建了一个ObserveOnObserver对象传入。这里的source就还是之前的ObservableCreate,所以这里还是会调用ObservableCreate中的subscribeActual方法。

protected void subscribeActual(Observer<? super T> observer) {

CreateEmitter parent = new CreateEmitter(observer);

observer.onSubscribe(parent);

try {

source.subscribe(parent);

} catch (Throwable ex) {

Exceptions.throwIfFatal(ex);

parent.onError(ex);

}

}

ObservableCreate中的subscribeActual方法中的逻辑之前看过,不过此时传入的observer仍然不再是在使用时创建的观察者对象了,而是传过来的ObserveOnObserver对象,此时创建的CreateEmitter中的observer也就是这个ObserveOnObserver对象。和之前逻辑一样,接着就会调用observeronNext等方法,此时调用的即是ObserveOnObserver中的onNext等方法。所以进入ObserveOnObserver查看。

@Override

public void onNext(T t) {

if (done) {

return;

}

if (sourceMode != QueueDisposable.ASYNC) {

queue.offer(t);

}

schedule();

}

@Override

public void onComplete() {

if (done) {

return;

}

done = true;

schedule();

}

void schedule() {

if (getAndIncrement() == 0) {

worker.schedule(this);

}

}

查看ObserveOnObserver中的代码会发现onNext方法中先将传入的参数放入了一个队列,然后无论是onNext还是onComplete方法最后都调用了schedule方法,进而再进入查看,发现schedule方法中又调用了worker.schedule方法。这里的worker就是之前创建的HandlerWorker,这时再来看它的schedule方法。

public Disposable schedule(@NonNull Runnable run) {

return schedule(run, 0L, TimeUnit.NANOSECONDS);

}

单个参数schedule方法是在其父类中的,而这个方法中又调用另一个三个参数的schedule方法,这个方法父类中是抽象方法所以实现就在子类HandlerWorker里了。

@Override

@SuppressLint(“NewApi”) // Async will only be true when the API is available to call.

public Disposable schedule(Runnable run, long delay, TimeUnit unit) {

if (run == null) throw new NullPointerException(“run == null”);

if (unit == null) throw new NullPointerException(“unit == null”);

// 判断是否取消订阅

if (disposed) {

return Disposables.disposed();

}

run = RxJavaPlugins.onSchedule(run);

// 创建ScheduledRunnable

ScheduledRunnable scheduled = new ScheduledRunnable(handler, run);

// 创建消息,并将主线程Handler和ScheduledRunnable

Message message = Message.obtain(handler, scheduled);

message.obj = this;

// 判断设置异步消息

if (async) {

message.setAsynchronous(true);

}

// 发送消息执行callback

handler.sendMessageDelayed(message, unit.toMillis(delay));

// 检查是否取消订阅

if (disposed) {

handler.removeCallbacks(scheduled);

return Disposables.disposed();

}

return scheduled;

}

在子类的这个方法里在做了取消订阅的判断后将方法传入的RunnableHandler又封装到一个ScheduledRunnable对象中。接着创建了一个Message并将ScheduledRunnable放入Message,最后调用handler.sendMessageDelayed方法通过这个主线程的Handler执行这个ScheduledRunnable

最后来追溯下ScheduledRunnable到底执行了什么,不过猜也知道最后一定调用到观察者中的对应方法。

private static final class ScheduledRunnable implements Runnable, Disposable {

private final Handler handler;

private final Runnable delegate;

private volatile boolean disposed; // Tracked solely for isDisposed().

ScheduledRunnable(Handler handler, Runnable delegate) {

this.handler = handler;

this.delegate = delegate;

}

@Override

public void run() {

try {

delegate.run();

} catch (Throwable t) {

RxJavaPlugins.onError(t);

}

}

}

ScheduledRunnable中的run方法很简单就是调用了构造中传入的Runnablerun方法。而根据之前看过得创建ScheduledRunnable时传入的Runnable又是从scheduleDirect方法中传入的,而scheduleDirect方法中的Runnable又是从worker.schedule(this)方法时传入的,根据上下文代码发现这个this指代的是ObserveOnObserver对象,于是进一步进入它的run方法查看。

static final class ObserveOnObserver extends BasicIntQueueDisposable

implements Observer, Runnable {

ObserveOnObserver(Observer<? super T> actual, Scheduler.Worker worker, boolean delayError, int bufferSize) {

this.downstream = actual;

this.worker = worker;

this.delayError = delayError;

this.bufferSize = bufferSize;

}

@Override

public void run() {

if (outputFused) {

drainFused();

} else {

drainNormal();

}

}

}

可以看到run方法中判断了outputFused的真假,然后分别调用了drainFuseddrainNormal方法。这里的outputFused是与RxJava2中的背压处理相关暂时先不管,根据方法名也能知道正常调用会执行drainNormal方法,于是直接来看drainNormal方法。

void drainNormal() {

int missed = 1;

// 存放onNext传入的事件对象队列

final SimpleQueue q = queue;

// 传入的观察者对象

final Observer<? super T> a = downstream;

// 循环check事件是否完成或者发生错误

for (;😉 {

if (checkTerminated(done, q.isEmpty(), a)) {

return;

}

for (;😉 {

boolean d = done;

T v;

try {

// 从队列中取出发送事件传入的对象

v = q.poll();

} catch (Throwable ex) {

Exceptions.throwIfFatal(ex);

disposed = true;

upstream.dispose();

q.clear();

a.onError(ex);

worker.dispose();

return;

}

boolean empty = v == null;

// 再次判断是否完成或者发生错误

if (checkTerminated(d, empty, a)) {

return;

}

// 判断队列中取出的发送事件传入的对象v是否为空

if (empty) {

break;

}

// 执行观察者对象的onNext方法

a.onNext(v);

}

missed = addAndGet(-missed);

if (missed == 0) {

break;

}

}

}

drainNormal方法中先通过checkTerminated方法校验发送事件是否完成或者发生异常,接着从队列中取出事件对象,再次判断是否完成或者发生错误和取出的对象是否为空,没有问题的话就会执行观察者的onNext方法。而发送完成和出现异常的方法则是在checkTerminated方法处理。

boolean checkTerminated(boolean d, boolean empty, Observer<? super T> a) {

if (disposed) {

queue.clear();

return true;

}

if (d) {

Throwable e = error;

if (delayError) {

if (empty) {

disposed = true;

if (e != null) {

a.onError(e);

} else {

a.onComplete();

}

worker.dispose();

return true;

}

} else {

if (e != null) {

disposed = true;

queue.clear();

a.onError(e);

worker.dispose();

return true;

} else

if (empty) {

disposed = true;

a.onComplete();

worker.dispose();

return true;

}

}

}

return false;

}

checkTerminated方法里根据delayError判断是否设置了超时的错误,接着再根据获得的错误e是否为空再决定调用的是观察者的onError()方法还是onComplete方法。至此observeOn切换线程的流程也梳理结束。

5. map操作符


RxJava中有很多功能强大的操作符,通过使用这些操作符,可以很容易的解决代码编写时遇到的一些复杂繁琐的问题。这里就用map操作符来作为一个例子,来看看操作符是怎样工作的。首先还是来了解map操作符的使用方法和作用。

final Observable observable = Observable.create(new ObservableOnSubscribe() {

@Override

public void subscribe(ObservableEmitter emitter) throws Exception {

Log.d(getClass().getName(), Thread.currentThread().getName() + " ObservableOnSubscribe subscribe");

emitter.onNext(“5”);

emitter.onComplete();

}

});

Observer observer = new Observer() {

@Override

public void onSubscribe(Disposable d) {

Log.d(getClass().getName(), Thread.currentThread().getName() + " onSubscribe");

}

@Override

public void onNext(Integer i) {

Log.d(getClass().getName(), Thread.currentThread().getName() + " onNext "+i);

}

@Override

public void onError(Throwable e) {

Log.d(getClass().getName(), Thread.currentThread().getName() + " onError");

}

@Override

public void onComplete() {

Log.d(getClass().getName(), Thread.currentThread().getName() + " onComplete");

}

};

Observable mapObservable = observable.map(new Function<String, Integer>() {

@Override

public Integer apply(String s) throws Exception {

return Integer.parseInt(s);

}

});

Log.d(getClass().getName(), Thread.currentThread().getName() + " mapObservable:"+mapObservable.getClass().getName());

mapObservable.subscribe(observer);

运行日志:

map操作符作用是可以将被观察者发送事件的数据类型转换成其他的数据类型。它的使用方法很简单,例如上面这个例子就将一开始发送的String类型转换成观察者接收到的Integer类型。下面开始看map方法的源码。

public final Observable map(Function<? super T, ? extends R> mapper) {

ObjectHelper.requireNonNull(mapper, “mapper is null”);

return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));

}

看到map方法中依旧还是同样的套路,通过RxJavaPlugins.onAssembly方法返回一个被观察者对象,只不过这次构建传入的类型又是另一个ObservableMap类型的对象。订阅的流程前面已经看过了,这里和之前的一样最终会走到ObservableMapsubscribeActual方法,所以直接来看这个方法。

@Override

public void subscribeActual(Observer<? super U> t) {

source.subscribe(new MapObserver<T, U>(t, function));

}

ObservableMapsubscribeActual方法里看到很熟悉还是会调用source.subscribe方法,只是这里传入的Observer对象是一个MapObserver对象。接下来的逻辑又和之前一样,根据之前的经验source.subscribe方法最终会调用ObserveronNext方法,所以接下来直接来看MapObserveronNext方法。

public void onNext(T t) {

if (done) {

return;

}

if (sourceMode != NONE) {

downstream.onNext(null);

return;

}

U v;

try {

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

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

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

img

img

img

img

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

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

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

总而言之,成功是留给准备好的人的。无论是参加什么面试,都要做好充足的准备,注意好面试的礼仪和穿着,向面试官表现出自己的热忱与真诚就好。即使最后没有过关,也要做好经验的总结,为下一次面试做好充足准备。

这里我为大家准备了一些我在面试后整理的面试专题资料,除了面试题,还总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料分享给大家,希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

毕竟不管遇到什么样的面试官,去面试首先最主要的就是自己的实力,只要实力够硬,技术够强,就不怕面试拿不到offer!

为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!希望读到这的您能点个小赞和关注下我,以后还会更新技术干货,谢谢您的支持!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

果低效又漫长,而且极易碰到天花板技术停滞不前!**

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

[外链图片转存中…(img-8gMDxyOd-1712442970828)]

[外链图片转存中…(img-PH5PWMPx-1712442970828)]

[外链图片转存中…(img-ge5fHZT0-1712442970829)]

[外链图片转存中…(img-kzyP8vin-1712442970829)]

[外链图片转存中…(img-c303xazT-1712442970829)]

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

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

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

总而言之,成功是留给准备好的人的。无论是参加什么面试,都要做好充足的准备,注意好面试的礼仪和穿着,向面试官表现出自己的热忱与真诚就好。即使最后没有过关,也要做好经验的总结,为下一次面试做好充足准备。

这里我为大家准备了一些我在面试后整理的面试专题资料,除了面试题,还总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料分享给大家,希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

毕竟不管遇到什么样的面试官,去面试首先最主要的就是自己的实力,只要实力够硬,技术够强,就不怕面试拿不到offer!

[外链图片转存中…(img-uUR7K0Yx-1712442970830)]

[外链图片转存中…(img-plRpNoqz-1712442970830)]

为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!希望读到这的您能点个小赞和关注下我,以后还会更新技术干货,谢谢您的支持!

[外链图片转存中…(img-BOKzdMof-1712442970830)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值