原文 The Reactive-Streams API (part 3)
介绍
在本文中,我将讲解如何把 rx.Subscriber 管理资源的能力移植到 Reactive-Streams API 中。但是由于 RS 并未明确任何资源管理方面的要求,所以我们需要引入(把 rx.Subscription 重命名)我们自己的容器类型,并把它加入到 RS 的 Subscriber 的取消逻辑中。
为了避免造成困惑,RxJava 2.0 把 XXXSubscription 替换为了 XXXDisposable,我不会在这里详细介绍这些类,但是会讲几个资源管理的基本接口:
interface Disposable {
boolean isDisposed();
void dispose();
}
interface DisposableCollection extends Disposable {
boolean add(Disposable resource);
boolean remove(Disposable resource);
boolean removeSilently(Disposable resource);
void clear();
boolean hasDisposables();
boolean contains(Disposable resource);
}
使用的规则是一样的:线程安全、幂等性。
加入资源管理最基本的方式就是对 Subscription 进行一次包装,拦截 cancel() 调用并且调用底层容器类的 dispose():
public final class DisposableSubscription
implements Disposable, Subscription {
final Subscription actual;
final DisposableCollection collection;
public DisposableSubscription(
Subscription actual,
DisposableCollection collection) {
this.actual = Objects.requireNonNull(actual);
this.collection = Objects.requireNonNull(collection);
}
public boolean add(Disposable resource) {
return collection.add(resource);
}
public boolean remove(Disposable resource) {
return collection.remove(resource);
}
public boolean removeSilently(Disposable resource) {
return collection.remove(resource);
}
@Override
public boolean isDisposed() {
return collection.isDisposed();
}
@Override
public void dispose() {
cancel();
}
@Override
public void cancel() {
collection.dispose();
actual.cancel();
}
@Override
public void request(long n) {
actual.request(n);
}
}
由于 DisposableSubscription 也实现了 Disposable 接口,所以它也可以被加入到容器中,构成一个复杂的 dispose 网络。但是绝大多数情况下,我们都希望避免额外的内存分配,因此,上面的这些代码可能会被融入到其他的类型中,例如在 lift() 调用中创建的 Subscriber 类型。
如果你熟悉 RxJava 的规范,以及操作符实现的陷阱之一,那就不应该取消订阅下游,因为这可能会导致资源的提前释放。
(这也是目前 RxAndroid 的 LifecycleObservable 的一个 bug,当我们在中间插入一个类似于 takeUntil() 的操作符时,它不会向下游发送 onCompleted(),而是取消订阅了下游。)
在 RS 中,取消订阅下游实际上是不会发生的。每一层都要么只能原封不动的转发 Subscription(因此无法添加资源),要么只能把它包装为 DisposableSubscription 这样的类型,然后依然把下游当做一个 Subscription 进行转发。如果你在这一层调用了 cancel(),你是无法调用包装 Subscriber 的类的 cancel() 的。
当然,你非要搞破坏那肯定是可以的,但 RS 比 RxJava 做了更多的努力,而且原则是不变的:不应该 cancel/dispose 下游的资源,或者在操作符链条中共享资源。
TakeUntil
现在让我们看看怎么实现能够管理外部资源的 takeUntil() 操作符(我把代码拆分了一下,更方便阅读):
public final class OperatorTakeUntil
implements Operator {
final Publisher other;
public OperatorTakeUntil(Publisher other) {
this.other = Objects.requireNonNull(other);
}
@Override
public Subscriber super T> call(
Subscriber super T> child) {
Subscriber serial =
new SerializedSubscriber<>(child);
SubscriptionArbiter arbiter =
new SubscriptionArbiter(); // (1)
serial.onSubscribe(arbiter);
SerialDisposable sdUntil = new SerialDisposable(); // (2)
SerialDisposable sdParent = new SerialDisposable(); // (3)
到目前为止,看起来都和 RxJava 的实现类似:我们把 child 包装为一个 SerializedSubscriber,防止 Publisher 并行发出 onError() 和 onCompleted()。
我们创建了一个 SubscriptionArbiter(一个 ProducerArbiter 的变体),主要是考虑以下原因:假设我们正在 call() 函数中,另一个已经订阅的数据源发出了一个数据,那我们就需要把数据转发到 child Subscriber 中,然而在我们得到 Subscription 之前(调用 onSubscribe() 之前),我们是无法在其上调用 onXXX 函数的,所以我们只能等到操作符链条上调用 onSubscribe() 之后(拿到 Subscription 之后),才可以转发数据。我会在下一篇文章中更详细地讲解这个问题。
然而,由于取消订阅的机会在 Subscriber 那里(我们调用 Subscription.cancel() 取消订阅,而我们会调用 Subscriber.onSubscribe(Subscription) 把 Subscription 交给 Subscriber),我们需要把 Subscription 从子 Subscriber 传递到父 Subscriber 中(2),这样它们中的任意一个到达终止状态时,它都能 cancel() 另一个。由于子 Subscriber 可能比父 Subscriber 先收到 Subscription 和取消事件,我们也将需要反过来取消父 Subscriber(3)。
// ...
Subscriber parent = new Subscriber() {
DisposableSubscription dsub;
@Override
public void onSubscribe(Subscription s) {
DisposableSubscription dsub =
new DisposableSubscription(s,
new DisposableList()); // (1)
dsub.add(sdUntil); // (2)
sdParent.set(dsub);
arbiter.setSubscription(dsub); // (3)
}
@Override
public void onNext(T t) {
serial.onNext(t);
}
@Override
public void onError(Throwable t) {
serial.onError(t);
sdParent.cancel(); // (4)
}
@Override
public void onComplete() {
serial.onComplete();
sdParent.cancel();
}
};
父 Subscriber 的实现略有不同,我们需要处理后来的 Subscription,并且建立好取消订阅的链条:
- 我们创建了一个
DisposableSubscription,底层使用基于 List 的 Disposable 集合。 - 我们把包装了
Disposable(指向另一个Subscription) 的SerialDisposable加入到容器中。我们也把DisposableSubscription加入到sdParent中,这就让另一个Subscriber可以在 parent 开始之前结束自己。 - 我们把包装好的对象加入到
arbiter中。 - 当错误事件发生时,我们要确保取消掉容器。而由于容器中包含了另一个
Subscription,所以整个事件流也会被取消。
最后我们需要为另一个事件流创建 Subscriber,并且确保它和 parent 连接起来:
// ...
Subscriber until = new Subscriber() {
@Override
public void onSubscribe(Subscription s) {
sdUntil.set(Disposables.create(s::cancel)); // (1)
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(Object t) {
parent.onComplete(); // (2)
}
@Override
public void onError(Throwable t) {
parent.onError(t);
}
@Override
public void onComplete() {
parent.onComplete();
}
};
this.other.subscribe(until);
return parent;
}
}
我们从另一个数据源接收到 Subscription 时,我们就把它包装为一个 Disposable(和现在 Subscription.create() 的做法一样)。由于 Disposable 的终结状态特性,即便我们的主要事件流在另一个流接收到 Subscription 之前就已经结束,包装的 SerialDisposable 依然会被取消,而这就会立即取消刚刚接收到的 Subscription。(译者注:对于“终结状态特性”,不了解的朋友可以看看之前的文章:Operator 并发原语: subscription-containers(一))
注意,由于取消/释放资源的能力取决于时间和顺序,所以我们通常都需要为每个资源创建一个容器(例如 sdParent、sdOther),这样无论哪个 Subscription 在任何时候到达时,我们都能释放所有的资源。
TakeUntil v2
如果仔细看看上面 takeUntil() 的实现,就会发现我们对各种 Subscription 进行了重新组织,我们其实可以理清 Disposable 导致的混乱:
@Override
// ...
public Subscriber super T> call(Subscriber super T> child) {
Subscriber serial = new SerializedSubscriber<>(child);
SubscriptionArbiter sa = new SubscriptionArbiter(); // (1)
DisposableSubscription dsub =
new DisposableSubscription(sa, new DisposableList()); // (2)
serial.onSubscribe(dsub); // (3)
Subscriber parent = new Subscriber() {
@Override
public void onSubscribe(Subscription s) {
dsub.add(Disposables.create(s::cancel)); // (4)
sa.setSubscription(s); // (5)
}
// ...
};
Subscriber until =
new Subscriber() {
@Override
public void onSubscribe(Subscription s) {
dsub.add(Disposables.create(s::cancel)); // (6)
s.request(Long.MAX_VALUE);
}
// ...
它的原理如下:
- 我们创建了一个
SubscriptionArbiter。 - 然后把它包装为一个
DisposableSubscription。 - 然后把它推到下游。这种组合能保证任何的取消以及请求,都会积累到 arbiter 接收到一个“真正的”
Subscription时。 - 一旦主流收到
Subscription之后,我们就把它包装为一个Disposable并加入到dsub容器中。 - 然后我们就更新 arbiter 的
Subscription:所有积累的请求以及取消操作都会“重放”到上游的Subscription上。 - 当另一条流收到
Subscription之后,我们也把它包装为一个Disposable并加入到dsub容器中。
最后 parent 会在它的 onError() 和 onComplete() 中调用 dsub.dispose() 了。
让我们梳理一下各种取消的路径:
- 下游取消:下游会取消
dsub,dsub会取消 arbiter,arbiter 会取消任何收到的Subscription。 - 主流结束:主流会取消
dsub,dsub会取消 arbiter,arbiter 会取消上游的Subscription。此外,dsub也会取消其他存在的Subscription。 - 支流结束:支流会取消
dsub,dsub会取消 arbiter。一旦主流收到Subscription之后,dsub和 arbiter 都会立即取消这个Subscription。
总结
在本文中,我讲解了怎么在 Reactive-Stream 的 Subscriber 和 Subscription 体系中实现资源管理,以及演示了如何实现一个 takeUntil() 操作符。
尽管看起来我们创建了和 RxJava 实现中同样多(甚至更多)的对象,但是很多操作符都不需要资源管理(例如 take()),甚至都不需要包装收到的 Subscription 对象(例如 map())。
在下一篇(最后一篇)关于 RS API 的文章中,我将介绍我们对各种 arbiter 类型更大的需求(在本文的 takeUntil() 例子中已经有所涉及),因为我们必须给 Subscriber 设置一个 Subscription 并且还要保证取消功能的正常,此外即便数据源发生了变化我们也不能调用多次 onSubscribe()。
欢迎大家关注我的微信公众号,会推送最新 blog、以及短篇内容。
原文 The Reactive-Streams API (part 4 - final)
介绍
在这篇介绍 Reactive-Streams API 的最后一篇文章中,我会讲讲我们对 SubscriptionArbiter(ProducerArbiter 的一个兄弟) 的高频使用需求,这一点可能会让很多人感到惊讶,当涉及到多个数据源、调度、其他异步内容时,我们将会很频繁的用到它。
在 RxJava 1.x 中,给 Subscriber 设置 Producer 是可选的,我们可以在没有 Producer 的情况下直接调用 onError() 和 onCompleted()。这些调用最终都会调用 rx.Subscriber.unsubscribe(),并清理相关资源。
与之相反,RS 要求 Subscription 必须在 onXXX 被调用之前,通过 onSubscribe() 传递给 Subscriber。
在本文中,我将展示一些这一要求可能导致的困境,尤其是当操作符的实现方式是典型的 RxJava 结构时。
稍后订阅
defer() 是必须考虑“订阅之前的错误”(error-before-Subscription)情况的操作符之一。当订阅一个推迟的 Publisher 时,操作符会先利用用户提供的工厂方法创建一个新的 Publisher,这个新的才是之后被订阅的。由于我们必须对用户的代码进行检查,所以我们就要捕获可能的异常,并且把异常通知给下游。但是,在这种情况下(要调用 child.onError()),我们就需要 child Subscriber 已经有 Subscription 了,但如果 child 这时已经有了 Subscription,那新生成的 Producer 收到 Subscription 时就不能转交给 child 了。解决方案就是使用一个 SubscriptionArbiter,它让我们可以提前发送错误事件,或者稍后切换到“真正的”数据源。
(译者注:这里 SubscriptionArbiter 相当于一个占位符或者说管道,Subscriber 不能替换 Subscription,但 SubscriptionArbiter 可以,这种移花接木的思路很赞,像国内很多插件化/热修复的方案,使用自定义的 ClassLoader,想法上差不多)
public final class OnSubscribeDefer
implements OnSubscribe {
final Func0 extends Publisher extends T>> factory;
public OnSubscribeDefer(
Func0 extends Publisher extends T>> factory) {
this.factory = factory;
}
@Override
public void call(Subscriber super T> child) {
SubscriptionArbiter sa = new SubscriptionArbiter();
child.onSubscribe(sa); // (1)
Publisher extends T> p;
try {
p = factory.call();
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
child.onError(e); // (2)
return;
}
p.subscribe(new Subscriber() { // (3)
@Override
public void onSubscribe(Subscription s) {
sa.setSubscription(s); // (4)
}
@Override
public void onNext(T t) {
child.onNext(t);
}
@Override
public void onError(Throwable t) {
child.onError(t);
}
@Override
public void onComplete() {
child.onComplete();
}
});
}
}
这一次我们不需要管理资源,但我们需要处理 Subscription 切换:
- 首先我们创建一个空的 arbiter,把它设置给 child。
- 如果用户的工厂方法抛出了异常,我们就可以安全地给 child 发送 onError 了,因为它已经有了 Subscription(尽管是个 arbiter)。
- 我们不能直接订阅到 child(因为它已经有了 Subscription),所以我们创建一个新的 Subscriber,并重写
onSubscribe方法。 - 我们把“真正的” Subscription 设置给 arbiter,其他的事件直接转发给 child 即可。
延迟一段时间后订阅
让我们看看现在的 delaySubscription() 操作符的实现,它把实际订阅操作延迟了一定的时间。我们几乎可以把已有的实现代码拷贝过来,但由于 API 发生了变化,会发生编译问题:
public final class OnSubscribeDelayTimed
implements OnSubscribe {
final Publisher source;
final Scheduler scheduler;
final long delay;
final TimeUnit unit;
public OnSubscribeDelayTimed(
Publisher source,
long delay, TimeUnit unit,
Scheduler scheduler) {
this.source = source;
this.delay = delay;
this.unit = unit;
this.scheduler = scheduler;
}
@Override
public void call(Subscriber child) {
Scheduler.Worker w = scheduler.createWorker();
// child.add(w);
w.schedule(() -> {
// if (!child.isUnsubscribed()) {
source.subscribe(child);
// }
}, delay, unit);
}
}
我们不能直接把资源添加到 child 中,也不能检查它是否已经被取消订阅(RS 中没有这两个方法了)。为了能清除 worker,我们就需要一个 disposable 容器,为了能够取消订阅,我们还需要一个可以“重放”数据源提供的 Subscription 取消操作的东西:
@Override
public void call(Subscriber super T> child) {
Scheduler.Worker w = scheduler.createWorker();
SubscriptionArbiter sa = new SubscriptionArbiter(); // (1)
DisposableSubscription dsub =
new DisposableSubscription(
sa, new DisposableList()); // (2)
dsub.add(w); // (3)
child.onSubscribe(dsub); // (4)
w.schedule(() -> {
source.subscribe(new Subscriber() { // (5)
@Override
public void onSubscribe(Subscription s) {
sa.setSubscription(s); // (6)
}
@Override
public void onNext(T t) {
child.onNext(t);
}
@Override
public void onError(Throwable t) {
child.onError(t);
}
@Override
public void onComplete() {
child.onComplete();
}
});
}, delay, unit);
比原来的实现多了很多代码,但值得这样做:
- 我们需要一个
SubscriptionArbiter来进行占位,因为实际的Subscription会延迟到来,所以我们需要用 arbiter 先记录下取消操作。 - 如果 child 要取消整个操作,那我们就需要取消这一次调度(直接取消这个 worker)。但由于 arbiter 没有资源管理能力,所以我们需要一个 disposable 容器。当然,我们只有一个资源,用不着一个 List,所以你可以实现一个自己的单一 disposable 容器类。
- 我们把 worker 加入到容器中,它就会替我们处理取消的事宜了。
- 当我们设置好 arbiter 和 disposable 之后,我们就可以把它们交给 child 了。然后 child 就可以随意进行
request()以及cancel()操作了,arbiter 和 disposable 会在合适的时机(实际 Subscription 到来时),把记录下来的操作都转发给数据源/资源了。 - 由于我们已经给 child 设置过了 Subscription,所以我们不能在调度时直接使用 child。我们创建一个 wrapper,在收到 Subscription 时设置给 arbiter,并且转发其他的 onXXX 事件。
- 我们在把实际的 Subscription 设置给 arbiter 时,arbiter 会重放积累的 request/cancel 操作。
现在你可能觉得最终调度时的 Subscriber(5)有错误,它没有在 onNext 中调用 sa.produced(1)。这确实会导致请求量计数不一致,但是一旦实际的 Subscription 被设置之后,后续 child 的 request(n) 都会原封不动地转发给上游,而我们后面又不会再调用 setSubscription() 了。所以上游能收到正确的请求量,即便 arbiter 计数不一致,也不会导致任何问题。为了保证更安全,你可以:
- 在
onNext中调用sa.produced(1); - 或者实现一个自己的 arbiter,只接受一个
Subscription,并且在收到它之后停止计数。
总结
在本文中,我展示了两种使用 SubscriptionArbiter 的场景。幸运的是,不是所有的操作符都需要进行这样的操作,但主流的都需要,因此在处理“实际” Subscription 延迟抵达以及 cancel() 的同时释放资源的问题时,我们需要两种特殊的 Subscription(SubscriptionArbiter 和 DisposableSubscription)
由于这种情况会频繁出现,我相信 RxJava 2.0 会提供一种标准而且高效的实现方案,来帮助我们遵循 Reactive-Streams 规范。
作为 RS API 系列的总结,RS 最小的接口集合就能满足典型异步数据流场景的需求了。我们可以把 RxJava 的思想迁移到 RS 中,但基础设施以及一些辅助工具都需要从头开始实现。我已经展示了几种基本的 Subscription:SingeSubscription,SingleDelayedSubscription,SubscriptionArbiter 以及 DisposableSubscription。再加上一些其他类似的工具类,它们将是实现 RxJava 2.0 操作符的主要工具类。
在下一个系列中,我将讲解响应式编程中争议最多的一个类型(Subject),而且我不仅会讲到它们的细节,还会讲如何实现我们自己的变体。
欢迎大家关注我的微信公众号,会推送最新 blog、以及短篇内容。