RxJava_L

ReactiveX 是一个专注于异步编程与控制可观察数据(或者事件)流的API。
它组合了观察者模式,迭代器模式和函数式编程的优秀思想。
高效、干净和可扩展的方式来处理实时数据处理的这一场景。


RxJava 是 ReactiveX 在 Java 上的开源的实现。


RxJava 最核心的两个参与者 Observables(被观察者、事件源)和Subscribers(观察者)
Observables 发出一系列事件。Subscribers处理这些事件


RxJava 和 观察者模式有一点明显不同,就是如果一个Observable没有任何Subscriber,那么这个Observable不会发出任何事件


RJ 基础写法

Observable<String> myObservable = Observable.create(  
    new Observable.OnSubscribe<String>() {  
        @Override  
        public void call(Subscriber<? super String> sub) {  
            sub.onNext("Hello, world!");  
            sub.onCompleted();  
        }  
    }  
); 
Subscriber<String> mySubscriber = new Subscriber<String>() {  
    @Override  
    public void onNext(String s) { System.out.println(s); }  
  
    @Override  
    public void onCompleted() { }  
  
    @Override  
    public void onError(Throwable e) { }  
};  

订阅建立:myObservable.subscribe(mySubscriber);

Observalbe 创建简写
Observable<String> myObservable = Observable.just("Hello, world!");  

只关心Subscriber 的 onNext;方法专属化写法
Action1<String> onNextAction = new Action1<String>() {  
    @Override  
    public void call(String s) {  
        System.out.println(s);  
    }  
};  
Subscribe 有一个重载方法
myObservable.subscribe(onNextAction, onErrorAction, onCompleteAction);  
我们不关心onError和onComplete
所以订阅可以写成 mObservable.subscrible(onNextAction);

上面的代码可以简写成

Observable.just("Hello, world!")  
    .subscribe(new Action1<String>() {  
        @Override  
        public void call(String s) {  
              System.out.println(s);  
        }  
    });
这里有个需求:
1.修改打印出的内容
2.对内容进行过滤
3.不在onNext中做变动(真正的响应式)
这里就需要引进操作符了Operators进行变换操作
(摘,写的很好)
因为我希望我的Subscribers越轻量越好,
因为我有可能会在mainThread中运行subscriber。
另外,根据响应式函数编程的概念,Subscribers更应该做的事情是“响应”,
响应Observable发出的事件,而不是去修改。
如果我能在某些中间步骤中对“Hello World!”进行变换是不是很酷?
(摘,写的很好)


操作符 Operator 对 Observable进行变换
之一
map:
Observable.just("Hello, world!")  
  .map(new Func1<String, String>() {  
      @Override  
      public String call(String s) {  
          return s + " -Dan";  
      }  
  })  
  .subscribe(s -> System.out.println(s));  
例如:subscriber并不关心返回的字符串,而是想要字符串的hash值
Observable.just("Hello, world!")  
    .map(new Func1<String, Integer>() {  
        @Override  
        public Integer call(String s) {  
            return s.hashCode();  
        }  
    })  
    .subscribe(i -> System.out.println(Integer.toString(i)));  
前面说过,Subscriber做的事情越少越好,我们再增加一个map操作符
Observable.just("Hello, world!")  
    .map(s -> s.hashCode())  
    .map(i -> Integer.toString(i))  
    .subscribe(s -> System.out.println(s));  
map可以进行多次变换,进行类型的切换,直至切换到subscriber最终需要的类型。

(摘,写的很好)
不服?
是不是觉得我们的例子太简单,不足以说服你?你需要明白下面的两点:

1.Observable和Subscriber可以做任何事情
Observable可以是一个数据库查询,Subscriber用来显示查询结果;
Observable可以是屏幕上的点击事件,Subscriber用来响应点击事件;
Observable可以是一个网络请求,Subscriber用来显示请求结果。

2.Observable和Subscriber是独立于中间的变换过程的。
在Observable和Subscriber中间可以增减任何数量的map。
整个系统是高度可组合的,操作数据是一个很简单的过程。

(摘,写的很好)

from 
Observable.from()方法,它接收一个集合作为输入,然后每次输出一个元素给subscriber

map()是一对一的转化,而我现在的要求是一对多的转化。那怎么才能把一个 Student 转化成多个 Course 呢?

这个时候,就需要用 flatMap() 了

flatMap
Observable.flatMap()接收一个Observable的输出作为输入,同时输出另外一个Observable。

query("Hello, world!")  
    .flatMap(new Func1<List<String>, Observable<String>>() {  
        @Override  
        public Observable<String> call(List<String> urls) {  
            return Observable.from(urls);  
        }  
    })  
    .subscribe(url -> System.out.println(url));  

flatMap输出的新的Observable正是我们在Subscriber想要接收的。
现在Subscriber不再收到List<String>,而是收到一些列单个的字符串,就像Observable.from()的输出一样。

query("Hello, world!")  
    .flatMap(urls -> Observable.from(urls))  
    .flatMap(new Func1<String, Observable<String>>() {  
        @Override  
        public Observable<String> call(String url) {  
            return getTitle(url);  
        }  
    })  
    .subscribe(title -> System.out.println(title)); 
可以多次flatMap
·filter()输出和输入相同的元素,并且会过滤掉那些不满足检查条件的。
·take()输出最多指定数量的结果。
.takeLast(N)只发射最后N个元素
.first()只发射第一个
.last()只发射最后一个
.skip(N)不发射前N个元素而是发射它后面的那些数据的序列
.skipLast()
.distinct 仅处理一次,可以处理去除重复的数据
.elementAt(N)仅从一个序列中发射第n个元素然后就完成了,这里是从0开始计的。
.sample(1000, TimeUnit.MILLISECONDS)定期发射Observable最近发射的数据项
·doOnNext()允许我们在每次输出一个元素之前做一些额外的事情,比如存库
query("Hello, world!")  
    .flatMap(urls -> Observable.from(urls))  
    .flatMap(url -> getTitle(url))  
    .filter(title -> title != null)  
    .take(5)  
    .doOnNext(title -> saveTitle(title))  
    .subscribe(title -> System.out.println(title));  
·throttleFirst()
在每次事件触发后的一定时间间隔内丢弃新的事件。
常用作去抖动过滤,例如按钮的点击监听器:
RxView.clickEvents(button) // RxBinding 代码,后面的文章有解释 
.throttleFirst(500, TimeUnit.MILLISECONDS) // 设置防抖间隔为 500ms 
.subscribe(subscriber);

另外还有merge操作符
Observable<Integer> observable1 = Observable.just(1, 3, 5);
Observable<Integer> observable2 = Observable.just(2, 4, 6);
Observable.merge(observable1,observable2)
        .subscribe(new Observer<Integer>() {
            @Override
            public void onCompleted() {
                Log.i("wxl", "onCompleted");
            }
            @Override
            public void onError(Throwable e) {
            }
            @Override
            public void onNext(Integer integer) {
                Log.i("wxl", "onNext=" + integer);
            }
        });
打印结果 135246.顺序打印完observable1、observable2
以及更多操作符,还可以自定义操作符

操作符 zip
合并两个或者多个Observables发射出的数据项,
根据指定的函数Func2变换它们,并发射一个新值。
Observable<Integer> observable1 = Observable.just(1, 3, 5);
Observable<Integer> observable2 = Observable.just(2, 4, 6, 9);
Observable.zip(observable1, observable2, new Func2<Integer, Integer, Integer>
    @Override
    public Integer call(Integer integer, Integer integer2) {
        return integer + integer2;
    }
})
        .subscribe(new Observer<Integer>() {
            @Override
            public void onCompleted() {
                Log.i("wxl", "onCompleted");
            }
            @Override
            public void onError(Throwable e) {
            }
            @Override
            public void onNext(Integer integer) {
                Log.i("wxl", "onNext=" + integer);
            }
        });
打印结果3 7 11 onCompleted
还有join startWith 对数据序列进行插入操作
错误处理

这种模式有以下几个优点:

1.只要有异常发生onError()一定会被调用
这极大的简化了错误处理。只需要在一个地方处理错误即可以。

2.操作符不需要处理异常
将异常处理交给订阅者来做,Observerable的操作符调用链中一旦有一个抛出了异常,就会直接执行onError()方法。

3.你能够知道什么时候订阅者已经接收了全部的数据。
使用RxJava,Observable对象根本不需要知道如何处理错误!
操作符也不需要处理错误状态-一旦发生错误,就会跳过当前和后续的操作符。
所有的错误处理都交给订阅者来做。

调度器Scheduler 

使用RxJava,你可以使用subscribeOn()指定观察者代码运行的线程,
使用observerOn()指定订阅者运行的线程

myObservableServices.retrieveImage(url)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(bitmap -> myImageView.setImageBitmap(bitmap));
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(): 事件产生的线程。
* observeOn(): 事件消费的线程。

Observable.just(1, 2, 3, 4) // IO 线程,由 subscribeOn() 指定
    .subscribeOn(Schedulers.io())
    .observeOn(Schedulers.newThread())
    .map(mapOperator) // 新线程,由 observeOn() 指定
    .observeOn(Schedulers.io())
    .map(mapOperator2) // IO 线程,由 observeOn() 指定
    .observeOn(AndroidSchedulers.mainThread) 
    .subscribe(subscriber);  // Android 主线程,由 observeOn() 指定
如果有多次切换线程的需求,只要在每个想要切换线程的位置调用一次 observeOn()即可。

Subscriber.onStart() 相对应的,有一个方法 Observable.doOnSubscribe() 。
它和 Subscriber.onStart() 同样是在 subscribe() 调用后而且在事件发送前执行,但区别在于它可以指定线程。
默认情况下,doOnSubscribe() 执行在 subscribe() 发生的线程;而如果在 doOnSubscribe() 之后有 subscribeOn() 的话,它将执行在离它最近的 subscribeOn() 所指定的线程。
Observable.create(onSubscribe)
    .subscribeOn(Schedulers.io())
    .doOnSubscribe(new Action0() {
        @Override
        public void call() {
            progressBar.setVisibility(View.VISIBLE); // 需要在主线程执行
        }
    })
    .subscribeOn(AndroidSchedulers.mainThread()) // 指定主线程
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(subscriber);
当调用Observable.subscribe(),会返回一个Subscription对象。
这个对象代表了被观察者和订阅者之间的联系。
subscription.unsubscribe();
RxJava的另外一个好处就是它处理unsubscribing的时候,会停止整个调用链。
如果你使用了一串很复杂的操作符,调用unsubscribe将会在他当前执行的地方终止。不需要做任何额外的工作!

RxAndroid 
retrofitService.getImage(url)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(bitmap -> myImageView.setImageBitmap(bitmap));
如果你已经创建了自己的Handler,你可以使用HandlerThreadScheduler1将一个调度器链接到你的handler上。

AndroidObservable,它提供了跟多的功能来配合Android的生命周期。
bindActivity()和bindFragment()方法默认使用AndroidSchedulers.mainThread()来执行观察者代码,
这两个方法会在Activity或者Fragment结束的时候通知被观察者停止发出新的消息。
AndroidObservable.bindActivity(this, retrofitService.getImage(url))
    .subscribeOn(Schedulers.io())
    .subscribe(bitmap -> myImageView.setImageBitmap(bitmap);

AndroidObservable.fromBroadcast()方法,它允许你创建一个类似BroadcastReceiver的Observable对象。
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
AndroidObservable.fromBroadcast(context, filter)
    .subscribe(intent -> handleConnectivityChange(intent));

ViewObservable,使用它可以给View添加了一些绑定。
如果你想在每次点击view的时候都收到一个事件,可以使用ViewObservable.clicks(),
或者你想监听TextView的内容变化,可以使用ViewObservable.text()。

Retrofit
通常
@GET("/user/{id}/photo")
void getUserPhoto(@Path("id") int id, Callback<Photo> cb);
结合RxJava
@GET("/user/{id}/photo")
Observable<Photo> getUserPhoto(@Path("id") int id);
Retrofit对Observable的支持使得它可以很简单的将多个REST请求结合起来。
比如我们有一个请求是获取照片的,还有一个请求是获取元数据的,
我们就可以将这两个请求并发的发出,并且等待两个结果都返回之后再做处理:
Observable.zip(
    service.getUserPhoto(id),
    service.getPhotoMetadata(id),
    (photo, metadata) -> createPhotoWithData(photo, metadata))
    .subscribe(photoWithData -> showPhoto(photoWithData));
遗留代码,运行极慢的代码
转换成Observable
绝大多数时候Observable.just() 和 Observable.from() 能够帮助你从遗留代码中创建 Observable 对象:
private Object oldMethod() { ... }

public Observable<Object> newMethod() {
    return Observable.just(oldMethod());
}
如果oldMethod()足够快是没有什么问题的,但是如果很慢呢?调用oldMethod()将会阻塞住他所在的线程。 
为了解决这个问题,可以参考我一直使用的方法–使用defer()来包装缓慢的代码:
private Object slowBlockingMethod() { ... }

public Observable<Object> newMethod() {
    return Observable.defer(() -> Observable.just(slowBlockingMethod()));
}

生命周期
我把最难的部分留在了最后。如何处理Activity的生命周期?主要就是两个问题: 
1.在configuration改变(比如转屏)之后继续之前的Subscription。

比如你使用Retrofit发出了一个REST请求,接着想在listview中展示结果。如果在网络请求的时候用户旋转了屏幕怎么办?你当然想继续刚才的请求,但是怎么搞?


2.Observable持有Context导致的内存泄露
这个问题是因为创建subscription的时候,以某种方式持有了context的引用,尤其是当你和view交互的时候,这太容易发生!如果Observable没有及时结束,内存占用就会越来越大。 


第二个问题的解决方案就是在生命周期的某个时刻取消订阅。一个很常见的模式就是使用CompositeSubscription来持有所有的Subscriptions,然后在onDestroy()或者onDestroyView()里取消所有的订阅。
Subscription subscription = Observable.just("Hello", "RxJava", "WuXiaolong")
                .subscribe(new Observer<String>() {
                    @Override
                    public void onCompleted() {
                        Log.i("wxl", "onCompleted");
                    }
                    @Override
                    public void onError(Throwable e) {

                    }
                    @Override
                    public void onNext(String s) {
                        Log.i("wxl", "onNext=" + s);
                    }
                });
addSubscription(subscription);
addSubscription方法可以放到父类:
private CompositeSubscription mCompositeSubscription;

    public void addSubscription(Subscription subscription) {
        if (this.mCompositeSubscription == null) {
            this.mCompositeSubscription = new CompositeSubscription();
        }

        this.mCompositeSubscription.add(subscription);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (this.mCompositeSubscription != null) {
            this.mCompositeSubscription.unsubscribe();
        }
    }
结合Retrofit 用例:
@GET("/token")
public void getToken(Callback<String> callback);

@GET("/user")
public void getUser(@Query("token") String token, @Query("userId") String userId, Callback<User> callback);
..


getToken(new Callback<String>() {
    @Override
    public void success(String token) {
        getUser(token, userId, new Callback<User>() {
            @Override
            public void success(User user) {
                userView.setUser(user);
            }


            @Override
            public void failure(RetrofitError error) {
                // Error handling
                ...
            }
        };
    }


    @Override
    public void failure(RetrofitError error) {
        // Error handling
        ...
    }
});
倒是没有什么性能问题,可是迷之缩进毁一生,你懂我也懂,做过大项目的人应该更懂。

而使用 RxJava 的话,代码是这样的:
@GET("/token")
public Observable<String> getToken();

@GET("/user")
public Observable<User> getUser(@Query("token") String token, @Query("userId") String userId);
...
getToken()
    .flatMap(new Func1<String, Observable<User>>() {
        @Override
        public Observable<User> onNext(String token) {
            return getUser(token, userId);
        })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer<User>() {
        @Override
        public void onNext(User user) {
            userView.setUser(user);
        }
       @Override
        public void onCompleted() {
        }
        @Override
        public void onError(Throwable error) {
            // Error handling
            ...
        }
    });
多个请求,下一个请求依赖上一个请求的结果

结合RxBinding
RxBinding就是类似设置 OnClickListener 、设置 TextWatcher 这样的注册绑定对象的 API。
Button button = ...;
RxView.clickEvents(button) // 以 Observable 形式来反馈点击事件
    .subscribe(new Action1<ViewClickEvent>() {
        @Override
        public void call(ViewClickEvent event) {
            // Click handling
        }
    });
RxBinding 的目的:扩展性。通过 RxBinding 把点击监听转换成 Observable 之后,就有了对它进行扩展的可能。扩展的方式有很多,根据需求而定。一个例子是前面提到过的 throttleFirst() ,用于去抖动,也就是消除手抖导致的快速连环点击:
RxView.clickEvents(button)
    .throttleFirst(500, TimeUnit.MILLISECONDS)
    .subscribe(clickAction);


冷观察和热观察 https://www.aliyun.com/jiaocheng/14805.html?spm=5176.100033.2.11.X83JrU
Think of a hot Observable as a radio station. All of the listeners that are listening to it at this moment listen to the same song.
A cold Observable is a music CD. Many people can buy it and listen to it independently.
by Nickolay Tsvetinov


Cold Observable 
Observable 的 just、creat、range、fromXXX 等操作符都能生成Cold Observable。


尽管 Cold Observable 很好,但是对于某些事件不确定何时发生以及不确定 Observable 发射的元素数量,那还得使用 Hot Observable。
比如:UI交互的事件、网络环境的变化、地理位置的变化、服务器推送消息的到达等等。


Cold Observable 如何转换成 Hot Observable? 
1. 使用publish,生成 ConnectableObservable 
使用 publish 操作符,可以让 Cold Observable 转换成 Hot Observable。
它将原先的 Observable 转换成 ConnectableObservable。
注意:生成的 ConnectableObservable 需要调用connect()才能真正执行


Subject和Processor
Subject 和 Processor 的作用是相同的。
Processor 是 RxJava2.x 新增的类,继承自 Flowable 支持背压控制。而 Subject 则不支持背压控制。
Subject 既是 Observable 又是 Observer(Subscriber)。这一点可以从 Subject 的源码上看到。


当 Subject 作为 Subscriber 时,它可以订阅目标 Cold Observable 使对方开始发送事件。同时它又作为Observable 转发或者发送新的事件,让 Cold Observable 借助 Subject 转换为 Hot Observable。


注意,Subject 并不是线程安全的,如果想要其线程安全需要调用toSerialized()方法。(在RxJava1.x的时代还可以用 SerializedSubject 代替 Subject,但是在RxJava2.x以后SerializedSubject不再是一个public class)
然而,很多基于 EventBus 改造的 RxBus 并没有这么做,包括我以前也写过这样的 RxBus :( 。这样的做法是非常危险的,因为会遇到并发的情况。


Hot Observable 如何转换成 Cold Observable?
RefCount操作符
如果所有的订阅者都取消订阅了,则数据流停止。如果重新订阅则重新开始数据流。
如果不是所有的订阅者都取消了订阅,只取消了部分。部分的订阅者重新开始订阅,则不会从头开始数据流
share操作符封装了publish().refCount()调用


======RxJava1.0和2.x的不同======
https://blog.csdn.net/lengqi0101/article/details/59552953
Rxjava2.0,出现了两种观察者模式:


Observable(被观察者)/Observer(观察者) 
Flowable(被观察者)/Subscriber(观察者)


RxJava2.X中, Observeable用于订阅Observer ,是不支持背压的,
而 Flowable用于订阅Subscriber ,是支持背压(Backpressure)的。
Action
Action这类接口,在1.0中,这类接口是从Action0,Action1…往后排(数字代表可接受的参数),现在做出了改动


Rx1.0———–Rx2.0


Action1——–Action


Action1——–Consumer


Action2——–BiConsumer


后面的Action都去掉了,只保留了ActionN


去掉了Schedulers.immediate()这个线程环境 


======RxJava1.0和2.x的不同======


 RxBus
 RxBus 名字看起来像一个库,但它并不是一个库,而是一种模式,
 它的思想是使用 RxJava 来实现了 EventBus ,
 而让你不再需要使用 Otto 或者 GreenRobot 的 EventBus


 ====EventBus3.0======
 1.compile 'org.greenrobot:eventbus:3.0.0'
 2.定义消息事件类
 public class MessageEvent{
    private String message;
    public  MessageEvent(String message){
        this.message=message;
    }
 
    public String getMessage() {
        return message;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
}
3.注册与解除
onCreate
EventBus.getDefault().register(this);
onDestroy
if(EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().unregister(this);
}
4.发送事件
EventBus.getDefault().post(messageEvent);
5.处理事件
@Subscribe(threadMode = ThreadMode.MAIN)
public void XXX(MessageEvent messageEvent) {
    ...
}
名字可以随意取,但需要加一个注解 @Subscribe,并且指定线程模型
线程模型有4种
POSTING (默认) 表示事件处理函数的线程跟发布事件的线程在同一个线程。
MAIN 表示事件处理函数的线程在主线程(UI)线程,因此在这里不能进行耗时操作。
BACKGROUND 表示事件处理函数的线程在后台线程,因此不能进行UI操作。如果发布事件的线程是主线程(UI线程),那么事件处理函数将会开启一个后台线程,如果果发布事件的线程是在后台线程,那么事件处理函数就使用该线程。
ASYNC 表示无论事件发布的线程是哪一个,事件处理函数始终会新建一个子线程运行,同样不能进行UI操作。


just 了解一下下
 ====EventBus3.0======
RxBus 继续
RxBus的写法 发生异常不取消订阅关系的写法/是否采取背压的写法
https://blog.csdn.net/u011271348/article/details/69946650
上述这种类似EventBus 事件总线(公交车)的写法


而这种写法有如下缺点
Event无论顺序还是时间上都某种程度上不太可控。
EventBus看似将你的程序解耦,但是又有些过了。
过多EventBus的代码会造成代码结构混乱,难以测试和追踪,违背了解耦的初衷。
这时如果有意或无意的造成了Nested Event。那情况会更糟。


怎样才是正确(正常?)的RxJava使用方式?
其实Jake大神也在GitHub的讨论上给出了一个答案:
所以应该是,每当你想发布一个Event在EventBus时,直接暴露一个Observable出来。每当你想接受一个Event时,找到这个Observable并且Subscribe他。


下面这个放弃RxBus系列 ,可以灵活进行1 to 1/n 
https://www.jianshu.com/p/61631134498e
1 to 1/n 可以用share/replay 等操作符完美解决。 n to n 也可以用局部Subject。


- 由于我们将editText封装在Observable里,无论是create()方法还是使用RxBinding,都会持有这个View的强引用。造成内存泄漏。所以我们一定要在最后加入dispose()方法来释放。所以我推荐使用RxBinding,他已经帮我们在dispose()方法里写好了解除Listener的方法。
- 因为并没有使用publish操作符,导致多个Subscriber的时候还是有些许问题。可以考虑直接加入.share().


====上述大部分是RxJava1.0,少部分是2.x,乱,而且2.x相对1.0是独立的,改了很多=====
下面集中学习下2.0from简书nanchen系列
https://www.jianshu.com/p/a93c79e9f689
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值