什么异步编程模型
同步和异步的区别如下:
在同步执行时,当 CPU 执行到红色的 IO 任务时,直至 IO 任务完成,才继续执行接下来的普通任务;而在异步任务中,耗时操作在其他线程中,主线程不会被挂起。
在编程语言或者系统中,为了防止主线程(业务线程)阻塞,就一定会为你提供异步的调用方法,编程语言/框架会提供一套能够让你不用关心底层线程操作和通信的问题,可以很方便轻松的写出异步执行代码编程方案。这就异步编程模型。
Java语言的异步操作模型
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(new Callable() {
System.out.println("running");
Thread.sleep(3000);
return "提交之后立即执行";
});
try {
System.out.println(future.get()); // 这是一个阻塞的调用
} catch (Exception ex) {
} finally {
executor.shutdown();
}
主线程调用 future.get(),尝试获取异步线程的结果,此时异步线程执行的任务未必完成,因此主线程在等结果会阻塞。
Android 系统提供的异步模型
在 Android 系统中,提供了 AsyncTask 这个异步编程模型( Android 中也提供了 Handler 机制帮助我们很方便的自己实现一套异步操作),这些异步编程模型帮助我们处理异步的任务,而不用担心线程之间的在的一些问题。
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
// 这个方法会在异步线程中执行
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
if (isCancelled()) break;
}
return totalSize;
}
// 在异步线程执行,会定时在主线程中调用
protected void onProgressUpdate(Integer... progress) {
// 展示进度条
setProgressPercent(progress[0]);
}
// 在异步线程执行完成之后,在主线程中被调用
protected void onPostExecute(Long result) {
// 展示对话框,提示用户下载完成
showDialog("Downloaded " + result + " bytes");
}
// 在异步线程被调用之前,会在主线程调用这个方法
protected void onPreExecute(){
}
}
//开启这个异步任务很简单,获取DownloadFilesTask对象实例,执行execute方法
new DownloadFilesTask().execute(url1, url2, url3);
主线程调用 execute() -> onPreExecute() -> 启动异步线程(doInBackground)-> 异步线程通过 onProgressUpdate() 回调结果给主线程 -> 异步任务执行完毕通过 onPostExecute() 调结果给主线程。
可以看到,Android 提供的异步编程模型相对于 Java 更加完善,主线程没有阻塞的风险,可以说,这是一个不错的异步编程模式。
异步编程模型存在的问题
- 某些模型不完善,依然会导致阻塞的风险
- 对于线程,同步等问题的抽象不够,导致无法应对复杂业务逻辑
- 在异步代码的编写上,也不如同步代码那样容易理解
异步编程的本质是什么?
在我们的编程场景里,UI 就是最常见的消费者,而生产者则比如本地数据库,网络等等。
在这个模式里面,最困难的是什么呢?如何生产数据?还是如何处理数据?都不是,而是如何把生产者和消费者联系起来,协同完成工作。
在如何联系生产者和消费者的问题上,我们至少可以想到以下几种问题:
- 缓冲数据结构如何定义
- 消费者和生产者如何在互不干扰的同时感知对方的需求
- 消费者和生产者的速度如何协同
- 如果多个生产者和消费者共同工作,如何协同?
看到这里,你应该就明白了,其实异步编程的本质就是生产消费模型,你不信的话我们稍作变换:
所以,生产消费模型中存在的问题,也就是异步编程中的问题。因此,ReactiveX思考异步编程模型的时候,当然着眼于生产消费模型。
ReactiveX 如何设计异步编程?
ReactiveX 认为,Rx中重要的是数据流(或者叫事件流也行),所有的代码执行逻辑本质上都是围绕着一系列的数据做获取,传递,转换,合并等各种操作。
那我们不禁要问,ReactiveX 到底是如何设计操作符的,才能保证操作符之间组合使用可以创建一条数据流呢?
对操作符的设计
对 RxJava 有一些了解看过一些源码的人都知道,实际上,每个操作符是一个继承了 Observable 的实现类,,然后又有一个内部类实现了 Observer 接口。
以下只是继承的一个示意图,具体的类名在 RxJava2 中可能有所不同。
数据是通过 Observable 流向 Observer 的,那么想要实现整个调用链的数据流转,那么每个操作符只需要给上游操作符提供 Observer,并给下游提供一个 Observable,从而可以在整个调用链中发挥承上启下的作用。
Rx 中的异步编程体验
Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> emitter) throws Exception {
emitter.onNext(getFilePath());
emitter.onComplete();
}
})
.subscribeOn(Schedulers.newThread()) // 指定了getFilePath()执行的线程环境
.observeOn(Schedulers.io()) // 将接下来指定 map() 运行在 io 线程
.map(new Func1<String, Bitmap>() {
@Override
public Bitmap call(String s) {
return createBitmapFromPath(s);
}
})
.filter(new Func1<Bitmap, Boolean>() {
@Override
public Boolean call(Bitmap s) {
return s!=null;
}
})
.observeOn(AndroidSchedulers.mainThread()) // 将接下来执行的线程环境指定为为主线程
.subscribe(
new Subscriber<Bitmap>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Bitmap s) {
showBitmap(s)
}
);
通过 observeOn 和 subscribeOn 这两个操作符,可以轻松的指定对应的线程,切换代码的执行环境,完全不会影响我们对整个代码逻辑的理解,我们就像写同步执行的代码块一样写了我们的异步代码,这不就是我们对异步编程模型的终极追求么?我们希望像写同步代码一样写异步代码。
ReactiveX 中的线程操作符
以 observeOn 这个线程操作符为例,首先,想要实现整个调用链的数据传递,显然也需要和其他操作符一样实现整套基于 Push 的接口体系(也就是需要实现Observable 和 Observer)。这样才能承上启下,保证在代码调用上,是可以任意组合的。
线程操作符会将上游传入的数据缓存起来,然后开启一个线程,在线程中传递出去。
public class ObserverOn<T> extends Observable<T> {
// 这个指向上游传入的 Observable
private Observable observable;
private Scheduler schedulers;
// 初始化上游传入的 Observable 实例,和线程调度类
public ObserverOn(Observable observable, Scheduler scheduler){
this.observable = observable;
this.schedulers = scheduler;
}
// 当真正的订阅发生时,这个方法会被调用,传入下游的 Observer
@Override
protected void subscribeActual(Observer observer) {
// 先获得一个线程操作类,Work 封装了具体的线程
Scheduler.Worker worker = schedulers.createWorker();
// 创建一个内部类
ObserveronInner inner = new ObserveronInner(observer, worker);
// 当上游有数据时,会将数据传递给这个内部类
observable.subscribe(inner);
}
static final class ObserveronInner<T> implements Observer<T>, Runnable, Disposable {
// 持有下游的 observer
private Observer<T> actual;
// 切换的线程
private Scheduler.Worker curWork;
// 缓存的队列
private Queue<T> cache;
private boolean hasCalled = false;
private boolean cancelled = false;
private boolean done = false;
private Throwable error = null;
public ObserveronInner(Observer<T> observer, Scheduler.Worker worker){
this.curWork = worker;
actual = observer;
cache = new ConcurrentLinkedQueue<>();
}
@Override
public void onSubscribe(Disposable d) {
actual.onSubscribe(this);
}
// 上游的 Observable 有数据时,会传入数据进来
@Override
public void onNext(T value) {
// 因为需要切换线程,因此暂时把数据缓存起来
cache.offer(value)
// 调起新线程
schedule()
}
@Override
public void onError(Throwable e) {
// 上游发生错误时的处理
error = e;
done = true;
schedule()
}
@Override
public void onComplete() {
// 完成时的处理
done = true;
schedule()
}
void schedule() {
// 这个方法只需要调用一次,因为只需要拉起一次线程
if (!hasCalled) {
hasCalled = true;
// curWork 背后的线程会执行这个 runnable,调用 run 方法
curWork.schedule(this);
}
}
@Override
public void run() {
// 此时,代码已经在新的线程环境中执行了
// 开始一个死循环,不停的从队列中取数据
for (;;) {
if (cancelled) {
return;
}
// onError(),onComplete()方法的处理
if (done) {
if (error!=null){
actual.onError(error);
return;
}
actual.onComplete();
return;
}
// 从队列中获取数据
T t = cache.poll();
if (t != null) {
// 将数据传递给下游的 Observer
actual.onNext(t);
}
}
}
// 这个接口用于取消订阅
@Override
public void dispose() {
cancelled = true;
}
}
}
这段代码基本上是 observeOn 这个线程操作符的核心内容,它的使命就是缓存上游传入的数据,开启一个线程,在这个线程中把数据传递给下游。
为什么在 RxJava 中实现异步编程这么简单,无论是外部调用,还是内部源码,它的实现都足够简单?
主要原因在于,线程不需要执行具体的复杂的逻辑操作,不参与对数据的处理,也不需要关心和其他线程的通信等问题,它只需要提供线程环境。
Observable<Data> observableLocal = getObservableFromLocal();
Observable<Data> observableRemote = getObservableFromReomte();
Observable<Data> observabRecommend = getObservableFromRecommend();
Observable.concat(observableLocal, observableRemote, observabRecommend)
.takeUntil((data)->{ isNotEmpty(data) })
.observeOn(AndroidSchedulers.mainThread())
.subscribe((data)->{ showInUI(data) });
ReactiveX 的代码结构非常清晰,创建三条数据流,并有序的组合在一起,然后订阅到 UI 上,当数据到来时,UI 负责展示。
ReactiveX 也称为 响应式编程(我认为也可以叫声明式),注意和 命令式编程 区分。
ReactiveX 设计的缺陷
在整个异步的调用链中,上游生产数据的速度是有可能远快于下游消费者的,这样一来,短时间内整个调用链会堆积大量的数据,最终会使整个调用链崩溃,也就是说,上下游是不会充分沟通的,对于下游的消费者而言,只要数据来了,我就消费。就像含着水龙头喝水,当水流很小的时候,只要水来了,你就喝,可是,总有一天,水流会大到你完全来不及喝。每当发生这种情况,就会出问题。
// Observable 是被观察者, 它在主线程中,每 1ms 送一个事件
Observable.interval(1, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.newThread()) //将观察者的工作放在新线程环境中
.subscribe(new Action1<Long>() {
@Override
public void call(Long aLong) {
try {
Thread.sleep(1000); // 观察者处理每 1000ms 才处理一个事件
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.w("TAG","---->" + aLong);
}
});
在上面的代码中,被观察者发送事件的速度是观察者处理速度的1000倍,这段代码运行之后:
...
Caused by: rx.exceptions.MissingBackpressureException
...
这个 MissingBackpressureException 异常里面就包含了 Backpressure 这个单词,看来背压肯定和这种异常情况有关系。
关于背压(Backpressure)
背压是流速控制的一种策略。
需要强调两点:
-
背压策略的一个前提是 异步环境,也就是说,被观察者和观察者处在不同的线程环境中。
-
背压(Backpressure)并不是一个像
flatMap一样可以在程序中直接使用的操作符,他只是一种控制事件流速的策略。
那么背压(Backpressure)策略具体是哪如何实现流速控制的呢?
观察者可以根据自身实际情况按需拉取数据,而不是被动接收(也就相当于告诉上游观察者把速度慢下来),最终实现了上游被观察者发送事件的速度的控制,实现了背压的策略。
// 被观察者将产生100000个事件
Observable observable = Observable.range(1, 100000);
class MySubscriber extends Subscriber<T> {
@Override
public void onStart() {
// 一定要在 onStart 中通知被观察者先发送一个事件
request(1);
}
@Override
public void onCompleted() {
...
}
@Override
public void onError(Throwable e) {
...
}
@Override
public void onNext(T n) {
...
...
// 处理完毕之后,在通知被观察者发送下一个事件
request(1);
}
}
observable.observeOn(Schedulers.newThread())
.subscribe(MySubscriber);
如果你想取消这种 backpressure 策略,调用 quest(Long.MAX_VALUE) 即可。
如果你足够细心,会发现,在开头展示异常情况的代码中,使用的是 interval 这个操作符,但是在这里使用了 range 操作符,为什么呢?
这是因为 interval 操作符本身并不支持背压策略,它并不响应 request(n),也就是说,它发送事件的速度是不受控制的,而 range 这类操作符是支持背压的,它发送事件的速度可以被控制。所以在 range 这类操作符中你也可以不需要调用 request(n) 方法去拉取数据,程序依然能完美运行,这是因为 range --> observeOn,这一段中间过程本身就是响应式拉取数据,observeOn这个操作符内部有一个缓冲区,Android 环境下长度是16,它会告诉 range 最多发送16个事件,充满缓冲区即可。不过话说回来,在观察者中使用 request(n) 这个方法可以使背压的策略表现得更加直观,更便于理解。
那么到底什么样的 Observable 是支持背压的呢?
Hot and Cold Observables
-
Cold Observables:指的是那些在 订阅之后 才开始发送事件的 Observable(每个 Subscriber 都能接收到完整的事件)。
-
Hot Observables:指的是那些在 创建了
Observable之后,(不管是否订阅)就开始发送事件的 Observable
那么,不支持背压的 Observevable 如何做流速控制呢?
过滤(抛弃)
相关类似的操作符:Sample,ThrottleFirst.... 以 sample 为例,
Observable.interval(1, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.newThread())
.sample(200, TimeUnit.MILLISECONDS) // 每隔 200ms 发送时间点里最近那个事件,其他的事件抛弃
.subscribe(new Action1<Long>() {
@Override
public void call(Long aLong) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.w("TAG","---->"+aLong);
}
});
缓存
Observable.interval(1, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.newThread())
.buffer(100, TimeUnit.MILLISECONDS) // 把 100毫秒 内的事件打包成 list 发送
.subscribe(new Action1<List<Long>>() {
@Override
public void call(List<Long> aLong) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.w("TAG","---->" + aLong.size());
}
});
两个特殊操作符
对于不支持背压的 Observable 除了使用上述两类生硬的操作符之外,还有更好的选择:onBackpressurebuffer,onBackpressureDrop。
-
onBackpressurebuffer:把 observable 发送出来的事件做缓存,当request方法被调用的时候,给下层流发送一个item(如果给这个缓存区设置了大小,那么超过了这个大小就会抛出异常)。 -
onBackpressureDrop:将 observable 发送的事件抛弃掉,直到subscriber 再次调用 request(n)方法的时候,就发送给它这之后的 n 个事件。
Observable.interval(1, TimeUnit.MILLISECONDS)
.onBackpressureDrop()
.observeOn(Schedulers.newThread())
.subscribe(new Subscriber<Long>() {
@Override
public void onStart() {
Log.w("TAG", "start");
request(1);
}
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Long aLong) {
Log.w("TAG","---->" + aLong);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
这段代码的输出:
W/TAG: start
W/TAG: ---->0
W/TAG: ---->1
W/TAG: ---->2
W/TAG: ---->3
W/TAG: ---->4
W/TAG: ---->5
W/TAG: ---->6
W/TAG: ---->7
W/TAG: ---->8
W/TAG: ---->9
W/TAG: ---->10
W/TAG: ---->11
W/TAG: ---->12
W/TAG: ---->13
W/TAG: ---->14
W/TAG: ---->15
W/TAG: ---->1218
W/TAG: ---->1219
W/TAG: ---->1220
...
之所以出现 0-15 这样连贯的数据,就是是因为 observeOn 操作符内部有一个长度为16的缓存区,它会首先请求16个事件缓存起来....
这两个操作符提供了更多的特性,那就是可以响应下游观察者的 request(n) 方法了,也就是说,使用了这两种操作符,可以让原本不支持背压的Observable“支持”背压了。
Reactive Stream 的 RxJava 实现
在 Rxjava2.x 中,除了Observable/Observer这套核心类之外,还有一套Flowable/Subscriber 核心类,这其中,Flowable/Subscriber 就实现了 Reactive Stream 的 API 接口,全面支持上下游的速度的协调,也就是支持背压。
Flowable.range(0,10)
.subscribe(new Subscriber<Integer>() {
Subscription sub;
@Override
public void onSubscribe(Subscription s) {
//当订阅后,会首先调用这个方法,其实就相当于 onStart()
sub = s;
sub.request(1); // 这个时候,上游才会发送对应数量的数据
Log.w("TAG", "onsubscribe end");
}
@Override
public void onNext(Integer o) {
Log.w("TAG", "onNext--->" + o);
// 当我们消费完数据之后,再次调用request()方法,上游才会继续发送数据
sub.request(1);
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onComplete() {
}
});
什么是响应式编程
wiki上对它的定义是,一种面向数据流和变化传播的编程范式。
接下来,我们就以 APP初始化 的业务为例来理解一下响应式编程的概念以及数据流,异步在其中扮演的角色。
响应式的代码应该是这样的:
Observable obserInitSDK = Observable.create((context)->{ initSDK(context) }).subscribeOn(Schedulers.newThread())
Observable obserInitDB = Observable.create((context)->{ initDatabase(context) }).subscribeOn(Schedulers.newThread())
Observable obserLogin = Observable.create((context)->{ login(getUserId(context)) })
.map((isLogin) -> { returnContext() })
.subscribeOn(Schedulers.newThread())
// 使用zip也行
Observable observable = Observable.merge(obserInitSDK, obserInitDB, obserLogin)
observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe(()->{ startActivity() })
从代码的层面来讲,initSDK,initDB,Login 都是耗时较长的操作,响应式的代码可以极大的提高程序的执行效率,降低阻塞。
在响应式的编程方式中,可能会用到大量的操作符来构建业务对象之间的关系,对于操作符的运用,常见的 map,flatmap 等大家可能都很熟悉,但是其他比较生僻的操作符可能就不太理解了,我们先先理清楚业务之间的关系,确定每一个业务之间到底是什么样的关系之后,我们就能大致确认我们需要怎样的操作符;当涉及到需要和并逻辑的时候,就去查合并类的操作符,需要条件判断来分流的逻辑时去找条件判断类的操作符。
flatmap 研究
flatMap,它的主要功能是接受上游传入的数据 ,通过外部暴露的接口将数据包裹为一个 Observable,然后内部再把所有的 Observable 的数据统一传递到下游。
你可能会有疑问,能对 Observable 进行操作的操作符有很多,比如 merge,为什么只有 flatmap 是独特的呢?
从 map 和 flatmap 的回调函数 apply() 中的输入和输出可以看到,我们确实做了一个类型转换的工作,但这只是表象,因为 Observable 不仅仅表示一个简单的数据类型,而是一条数据流。而 flatmap 则可以把外部传入的数据流并入到原来的流中。
Observable.create((e)->{
e.onNext("1");
e.onNext("2");
e.onComplete();
}).map((s)->{
return Integer.parseInt(s)
}).flatMap((s)->{
// 创建一个逻辑复杂的 Observable
return Observable.just(s)
.filter((s) -> {
return s!=null;
})
.map((s) -> {
// do something
})
...
;
})
.subscribe((s) -> {
showData(s)
})
我们在 flatmap 中插入另一条逻辑十分复杂的流,最后并入到原来的流中,一般来讲,我们创建的这个 Observable 可以完成任何事情。所以,我们不能简单的将 flatmap 和 map 混为一谈。
flatMap 的内部构造
flatMap 类会实现 Observable,其内部会有一个内部类实现 Observer 接口,以保证可以承上启下,但是它的内部还多了一个内部类,多出来的一个内部类用于订阅我们通过 apply() 方法创建的 Observable,获取这个 Observable 的数据流,传递给下游。
那 flatmap 在整个上下游的调用关系中数据流向是怎样的呢?
根据 flatmap 本身的构造示意图和在上下游中的数据流向图我们基本可以确定,flatmap 的真正功能就是:在我们正常的数据流中再插入一条新的数据流。
关于合并流
在 RxJava2.x 中,merge 又是通过 flatMap 组合实现的。
@SuppressWarnings({ "unchecked", "rawtypes" })
@SchedulerSupport(SchedulerSupport.NONE)
public static <T> Observable<T> merge(ObservableSource<? extends T> source1, ObservableSource<? extends T> source2, ObservableSource<? extends T> source3) {
// 去掉不重要的代码
return fromArray(source1, source2, source3).flatMap((Function)Functions.identity(), false, 3);
}
那么问题来了,既然都是流的合并操作,为什么会存在这两种操作符?看来他们还是有很多不同之处的。
flatmap 是 “流内合并”,而 merge 是独立的流和并(或者叫流外合并?)
怎么说呢?一图以蔽之:
谈到通过 flatmap 去实现 merge 的问题,其实我们就不用分析枯燥的源码了,通过示意图就能明白flatmap 是如何实现 merge 的。
无序性
和 flatmap 关系亲密的操作符中,大家可能更熟悉 concatMap,他俩确实是货真价实的亲密,基本运行机制都是相同的。唯一不同的,就是我们要说的无序性,flatmap 是无序的。而 concatmap 是有序的。
flatMap 并不保证被你包装出来的一系列 Observable 的顺序和数据最终输出的顺序是一致的,这不是一个bug,而是它的特性,而如果我们想要保证顺序性的话,就需要使用 concatMap 这个操作符,这个操作符的使用方法和效果是一致的,唯一的区别就是 concatMap 输入的顺序和输出的顺序是一致的。
错误延迟
什么叫错误延迟?就是当某个 Observable 发生了错误之后,是否需要让其他的 Observable 把数据发送完毕之后,再提示错误?
为什么需要错误延迟?我们知道,上游每传递一个数据到 flatmap,我们都会创建一个 Observable 对象,短时间内就可能存在一系列的 Observable,即存在不同的数据流,假如有一条数据流出现了错误,此时,如果不做处理,整个调用链都会立即停止,这就会导致其他的流所做的工作前功尽弃。
比如下载,直播类的APP通常每个APP会下载一些帧动画到本地,如果是多个流同时下载多个礼物的话,我们不希望因为其中一个下载任务的失败导致其他任务结束。
有了错误延迟,我们就可以让其他的流继续完成自己的工作。最后再发送错误。因此错误延迟是必要的。但是我们默认使用 flatmap 的时候,并没有开启错误延迟,因此可能会忽略这个重要的特性。
@SchedulerSupport(SchedulerSupport.NONE)
public final <R> Observable<R> flatMap(Function<? super T, ? extends ObservableSource<? extends R>> mapper) {
return flatMap(mapper, false);
}
@SchedulerSupport(SchedulerSupport.NONE)
public final <R> Observable<R> flatMap(Function<? super T, ? extends ObservableSource<? extends R>> mapper, boolean delayErrors) {
return flatMap(mapper, delayErrors, Integer.MAX_VALUE);
}
最大并发控制
根据我们提到的 flatmap 的内部构造,我们创建的 Observable 实际上订阅了 flatmap 的另一个内部类,然后把数据发送到内部类,接着传递给下游,形成数据流。如果我们不做处理的话,那么我们创建了多少个Observable,就会有多少个流在 flatmap 中工作。
最大并发请求就是用于指定最多允许同时多少个流同时工作。
// maxConcurrency 用于指定最大并发请求
@SchedulerSupport(SchedulerSupport.NONE)
public final <R> Observable<R> flatMap(Function<? super T, ? extends ObservableSource<? extends R>> mapper, boolean delayErrors, int maxConcurrency) {
return flatMap(mapper, delayErrors, maxConcurrency, bufferSize());
}
我们创建的 Observable 很可能是占用资源的操作,这样会导致短时间内大量的资源被占用。因此,想要用好 flatmap,一定要注意对最大并发量的控制。
flatMap 内部实现的难点
1,同步的问题
flatMap 内部是需要应对复杂的多线程同步的问题的。因为 flatmap 会通过 apply 产生大量的 Observable 对象,这些对象都可能处于不同的线程环境中,传出来的数据就可能会错乱。 而且方法调用还要符合 onNext, (onError|onCompleted) 这种调用顺序。
2,队列的维护
因为需要提供一个最大并发控制,那么通过 apply 产生的多出来的 Observable 就需要缓存在队列中,等待有 Observable 完成工作,再取出一个...如果要考虑性能优化,可能还要对队列中的 Observable 进行定时的检查,确保它们都是有效的...
RxJava 线程调度源码剖析
当调用了 observeOn(Schedulers.io()) 之后,会创建一个 ObservableObserveOn 对象,对象内部还会创建一个内部类承接上游的数据,缓存起来,然后通过传入的线程参数开启新的线程,把数据从缓存中读取出来,传递到下游。这是 observeOn 这个操作符的基本机制,Schedulers.io() 是如何获取到一个可用的线程的?
在 observeOn(Schedulers.io()) 方法内部,会发生一下线程相关的操作:
...
Scheduler.Worker worker = scheduler.createWorker();
...
worker.schedule(runnable);
...
public final class Schedulers {
@NonNull
static final Scheduler SINGLE;
@NonNull
static final Scheduler COMPUTATION;
//
@NonNull
static final Scheduler IO;
@NonNull
static final Scheduler TRAMPOLINE;
@NonNull
static final Scheduler NEW_THREAD;
...
...
static {
...
...
// 简单理解,就是IOTask内部包裹了一个IoScheduler对象
IO = RxJavaPlugins.initIoScheduler(new IOTask());
}
static final class IOTask implements Callable<Scheduler> {
@Override
public Scheduler call() throws Exception {
return IoHolder.DEFAULT;
}
}
static final class IoHolder {
static final Scheduler DEFAULT = new IoScheduler();
}
}
public final class IoScheduler extends Scheduler {
static final RxThreadFactory WORKER_THREAD_FACTORY;
// 这是一个可以对对象进行原子操作的类
// 保证 CachedWorkerPool 的操作是线程安全的
// CachedWorkerPool 是一个缓存线程工作类的缓存池(可以直接视作线程池)
final AtomicReference<CachedWorkerPool> pool;
private static final String KEY_IO_PRIORITY = "rx2.io-priority";
static {
// 这里有一个隐藏的小惊喜,关于线程优先级
// rx 会获取 key 为 "rx2.io-priority"的系统属性值,作为默认线程优先级
// 我们可以通过这一点来设置io线程的优先级,防止它和主线程抢占资源
int priority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY,
Integer.getInteger(KEY_IO_PRIORITY, Thread.NORM_PRIORITY)));
// RxThreadFactory,线程工厂类,用于生成具体线程对象
WORKER_THREAD_FACTORY = new RxThreadFactory(WORKER_THREAD_NAME_PREFIX, priority);
// 另一个线程工厂类,CachedWorkerPool 中用于定时扫描清除废弃的线程
EVICTOR_THREAD_FACTORY = new RxThreadFactory(EVICTOR_THREAD_NAME_PREFIX, priority);
// 创建一个缓存工作池
NONE = new CachedWorkerPool(0, null, WORKER_THREAD_FACTORY);
NONE.shutdown();
}
public IoScheduler() {
this(WORKER_THREAD_FACTORY);
}
public IoScheduler(ThreadFactory threadFactory) {
this.threadFactory = threadFactory;
// 它会持有缓存线程池对象,保证对这个对象的操作是线程安全的。
this.pool = new AtomicReference<CachedWorkerPool>(NONE);
start();
}
@Override
public void start() {
// 创建一个工作池
CachedWorkerPool update = new CachedWorkerPool(KEEP_ALIVE_TIME, KEEP_ALIVE_UNIT, threadFactory);
// 如果已经存在相同的对象,则关闭update
if (!pool.compareAndSet(NONE, update)) {
update.shutdown();
}
}
// 划重点啊!!!
// ObservableObserveOn中调用的方法,创建一个Worker
@NonNull
@Override
public Worker createWorker() {
// pool.get()可以获取到一个CachedWorkerPool对象
// 我们可以通过CachedWorkerPool获取一个线程工作类
return new EventLoopWorker(pool.get());
}
// EventLoopWorker内部会持有CachedWorkerPool对象
static final class EventLoopWorker extends Scheduler.Worker {
private final CachedWorkerPool pool;
private final ThreadWorker threadWorker;
EventLoopWorker(CachedWorkerPool pool) {
...
this.pool = pool;
// 通过CachedWorkerPool获取一个线程工作类
this.threadWorker = pool.get();
}
@Override
public void dispose() {
}
@Override
public boolean isDisposed() {
...
}
// 上层调用 schedule, 传入 Runnable,
// threadWorker 线程就会执行这个任务
@NonNull
@Override
public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) {
if (tasks.isDisposed()) {
// don't schedule, we are unsubscribed
return EmptyDisposable.INSTANCE;
}
return threadWorker.scheduleActual(action, delayTime, unit, tasks);
}
}
}
static final class CachedWorkerPool implements Runnable {
// 保持线程活跃的时间
private final long keepAliveTime;
// 用于缓存没有任务,赋闲的线程
private final ConcurrentLinkedQueue<ThreadWorker> expiringWorkerQueue;
final CompositeDisposable allWorkers;
private final ScheduledExecutorService evictorService;
private final Future<?> evictorTask;
// 线程工程,生产线程
private final ThreadFactory threadFactory;
CachedWorkerPool(long keepAliveTime, TimeUnit unit, ThreadFactory threadFactory) {
this.keepAliveTime = unit != null ? unit.toNanos(keepAliveTime) : 0L;
// 缓存队列
this.expiringWorkerQueue = new ConcurrentLinkedQueue<ThreadWorker>();
this.allWorkers = new CompositeDisposable();
// 用于创建工作线程的工厂
this.threadFactory = threadFactory;
ScheduledExecutorService evictor = null;
Future<?> task = null;
if (unit != null) {
// 这段代码的意思,就是根据 keepAliveTime 设定一个定时的线程,来清扫超过生命周期的线程
// 这也是为什么它需要继承自 Runnable,因为它本身也需要执行清理的任务
evictor = Executors.newScheduledThreadPool(1, EVICTOR_THREAD_FACTORY);
task = evictor.scheduleWithFixedDelay(this, this.keepAliveTime, this.keepAliveTime, TimeUnit.NANOSECONDS);
}
evictorService = evictor;
evictorTask = task;
}
@Override
public void run() {
// 到点了,定时线程会开始清扫废弃的线程
evictExpiredWorkers();
}
// 这个方法主就是最核心的,获取线程的方法
// scheduler.createWorker()最后会相应到这里
ThreadWorker get() {
if (allWorkers.isDisposed()) {
return SHUTDOWN_THREAD_WORKER;
}
// 先从已经完成任务,但是有没有被回收的线程队列中去找
while (!expiringWorkerQueue.isEmpty()) {
ThreadWorker threadWorker = expiringWorkerQueue.poll();
if (threadWorker != null) {
return threadWorker;
}
}
// 找不到的话,那就再重新创建一个线程
// No cached worker found, so create a new one.
// 传入线程工厂,创建一个线程
ThreadWorker w = new ThreadWorker(threadFactory);
// 加入CompositeDisposable好取消订阅
allWorkers.add(w);
return w;
}
void release(ThreadWorker threadWorker) {
// Refresh expire time before putting worker back in pool
threadWorker.setExpirationTime(now() + keepAliveTime);
// 释放该工作对象(包含了一个线程)之后,缓存到队列中
expiringWorkerQueue.offer(threadWorker);
}
void evictExpiredWorkers() {
// 清除所有的已经完成任务的线程
...
...
}
long now() {
return System.nanoTime();
}
// 关闭任务
void shutdown() {
...
}
}
看看下面的代码:
// 初始化sdk
Observable obserInitSDK=Observable.create((context)->{initSDK(context)}).subscribeOn(Schedulers.newThread())
// 初始化db
Observable obserInitDB=Observable.create((context)->{initDatabase(context)}).subscribeOn(Schedulers.newThread())
// 登陆接口
Observable obserLogin=Observable.create((context)->{login(getUserId(context))})
.map((isLogin)->{returnContext()})
.subscribeOn(Schedulers.newThread())
// 融合操作 使用 zip 也行
Observable observable = Observable.merge(obserInitSDK, obserInitDB, obserLogin)
observable.subscribe(()->{startActivity()})
当我们 startActivity 跳转新的页面的时候,我们到底处在那个线程环境呢?
答案是:不确定。
这并不是一个bug,因为
merge这样的操作符其实不关心自己上游的Observable处于哪个线程,它只是做好同步的工作,保证onNext..按照正确的顺序调用即可。也就是说,merge 之后,其实我们依然处于多线程的环境,只不过,这是一种数据有序的多线程(这可能就和单线程其实没有什么区别了)
关于 RxJava 的开销问题
我们每创建一个对象,都需要消耗一点内存,而一个 RxJava 基本的调用链会产生几个对象呢?
创建 Observable,如果使用
create的话,会创建3个对象,还有缓存队列。而如果使用just()创建 Observable的话,大概会创建2个对象。当然,操作符也有区分,有的操作符本身会创建多个对象,而且内部需要维护一些状态和数组等等,开销只会更大。