使用RxSwift时应避免的8个错误​​-第1部分

细节可能会有所不同,但是不管您的观察对象,观察者或订阅如何,基本含义都保持订阅的含义。 要发现的关键是,忽略参考周期管理器 (又称disposable ,可以使您摆脱自己破坏参考周期的可能性。 它是进入内存配置的门户药物,一旦无法使用,就再也没有回头路了。 如果使用_ =语法,则基本上可以说,释放observable observerobserver的唯一方法是完成可观察序列。

有时候这可能正是您想要的! 例如,如果您要调用Observable.just ,则不必确保打破周期,这并不重要。 单个元素被瞬时发出,随后是.completed事件。 但是,在许多情况下,您可能无法完全确定所讨论的可观察性的完成可能性:

  • 您从另一个对象获得了Observable ,文档没有说明它是否完成,
  • 您从另一个对象获得了Observable ,并且文档确实说明该对象已完成,但是在此过程中该对象的内部实现发生了一些变化,没有人记得要更新文档,
  • Observable明显未完成(示例包括VariableObservable.interval ,subject),
  • 可观察的实现中存在错误,例如忘记在Observable.create闭包中发送.completed事件。

由于您几乎无法控制应用程序中的所有可观察对象,即使那样也有可能出错,因此经验法则是确保自己打破参考周期。 要么保留对disposable的引用,并在时间到时调用.dispose()方法,要么使用方便的助手(例如DisposeBag来帮您完成。 您还可以使用.takeUntil运算符提供一个单独的可观察到的循环中断。 选择哪种方式取决于您的具体情况,但请始终记住:

既然我们已经解决了所有问题,我觉得我欠您一点解释。 我上面绘制的心理模型很好,这是一种心理模型,因此并不严格正确。 当前的RxSwift实现(在撰写本文时版本3.x / 4.x)发生了一些复杂的事情。 要了解实际行为,让我们更深入地研究RxSwift内部。

在哪里实现subscribe方法? 毫无疑问,搜索的第一位将是ObservableType.swift文件。 它包含订阅方法的声明,作为ObservableType协议的一部分:

 func subscribe( _ observer: O ) -> Disposable where OE == E 

什么实现了此协议? 基本上,所有各种类型的可观察物。 让我们集中讨论称为Observable的主要实现,因为它是RxSwift中定义的所有可观察对象(除了其中之一)的基类。 其subscribe方法版本简短而简单:

 public func subscribe( _ observer: O ) -> Disposable where OE == E { rxAbstractMethod() } 

哦,抽象方法。 然后,我们需要研究Observable子类。 快速搜索发现,在编写本文时,RxSwift源代码中有14种不同的重写subscribe方法。 我们可以将它们分别放入三个存储桶之一:

  • 主题中的实现,由于它们在RxSwift知识中所占的特殊位置,它们提供了自己的订阅逻辑,
  • 可连接可观察对象的实现,由于其具有组播功能,因此必须以特殊方式处理订阅,
  • 生产者中的Observable实现,它是Observable的子类,它为您成长为喜欢和使用的大多数操作员提供了订阅逻辑。

让我们集中讨论Producer类型,因为它代表了可观察到的变体,这种变体最容易推论:从单个源到单个接收者的事件序列的发射器。 这绝对是最常见的用例。 几乎所有运算符都源自Producer基类。 尽管其中一些提供了专用的订阅逻辑,并针对其特定需求进行了优化(有关基本示例,请参见Just,Empty或Error),但绝大多数使用了以下Producer订阅的实现(为了更好地剥离了一些与调度程序相关的逻辑)可读性):

 override func subscribe( _ observer: O ) -> Disposable where OE == Element { let disposer = SinkDisposer() let sinkAndSubscription = run(observer, cancel: disposer) disposer.setSinkAndSubscription( sink: sinkAndSubscription.sink, subscription: sinkAndSubscription.subscription ) return disposer } 

那么,这里发生了什么? 首先,可观察对象创建一个SinkDisposer对象。 然后,它使用SinkDisposer实例创建两个其他对象:接收器和订阅。 它们都具有相同的类型:Disposable,这是一个公开单个Dispose方法的协议。 这两个对象通过setter方法传递回SinkDisposer ,该方法正确地建议将保留它们的引用。 完成所有设置后,将返回SinkDisposer 。 因此,当我们在从subscription方法返回的对象上调用.dispose()来中断预订时,实际上是在SinkDisposer实例上调用它。

到目前为止,一切都很好。 悬而未决的还有一个谜团。 让我们深入到这里执行的两个关键步骤:让sinkAndSubscription = run(observer,cancel:disposer)和disposer.setSinkAndSubscription(sink:sinkAndSubscription.sink,subscription:sinkAndSubscription.subscription)方法。 正如您将看到的,它们是创建使订阅保持活动的参考周期的重要部分。

在可观测的海洋中沉没

run方法由Producer提供,但仅在一个抽象变体中提供:

 func run( _ observer: O, cancel: Cancelable ) -> (sink: Disposable, subscription: Disposable) where OE == Element { rxAbstractMethod() } 

实际逻辑特定于特定的Producer子类。 在检查它们之前,至关重要的是了解整个RxSwift运算符实现中非常常见的模式: sink 。 RxSwift通过这种方式来处理可观察流的复杂性,以及如何将可观察流的创建与subscribe立即运行的逻辑分开。

这个想法很简单:当您使用特定的运算符(例如您映射现有的可观察对象)时,它返回专用于手头任务的特定可观察类型的实例。 因此,调用Observable.just(1)会返回Just类的实例,该类是Producer的子类,已优化为仅返回一个元素然后完成。 调用Observable.just(1).map { $0 == 42 } ,您将得到Map类的实例,该类是Producer的子类,已优化为将闭包应用于对象中的每个元素.next事件。 但是,在您创建可观察对象的那一刻,实际上还没有任何东西发送给任何人,因为没有人订阅。 传递事件的实际工作始于subscribe方法,更确切地说是在我们非常感兴趣的run方法中。

那就是水槽图案发光的地方。 每个可观察类型都有其专用的Sink子类。 对于以Timer可观察者表示的间隔运算符,存在TimerSink。 对于由FlatMap observable表示的flatMap运算符,有FlatMapSink。 对于以catch可见表示的catchErrorJustReturn运算符,有CatchSink。 我想你应该已经明白了!

但是,这个Sink对象到底是什么? 它是存储实际操作员逻辑的地方。 因此,对于该intervalTimerSink是安排在每个周期之后发送事件并跟踪内部状态(即已发送了多少个事件)的地方。 对于flatMapFlatMapSink (及其超类MergeSink)是订阅从flatmapping闭包返回的可观察对象,跟踪它们并进一步传递其事件的地方。 您基本上可以将Sink视为观察者的包装。 它侦听可观察的事件,应用与操作员相关的逻辑,然后将这些转换后的事件进一步传递到流中。

这就是RxSwift如何将可观察对象的创建与基于Producer可观察对象的订阅逻辑的执行隔离开来的方法。 前者封装在Observable子类中,后者由Sink子类提供。 职责分离大大简化了实际对象的实现,并使得可以编写针对不同场景优化的Sink多个变体。

Sink满的知识

现在我们知道了接收模式是什么,让我们回到run方法。 每个Producer子类都提供自己的run实现。 尽管细节可能有所不同,但通常可以将其抽象为三个步骤:

  • 创建一个接收sink对象作为从Sink类型派生的类的实例,
  • 通常通过运行sink.run方法来创建订阅实例,
  • 返回包装在元组中的两个实例。

为了进一步说明问题,请查看FlatMap.run示例:

 override func run( _ observer: O, cancel: Cancelable ) -> (sink: Disposable, subscription: Disposable) where OE == SourceSequence.E { let sink = FlatMapSink( selector: _selector, observer: observer, cancel: cancel ) let subscription = sink.run(_source) return (sink: sink, subscription: subscription) } 

从内存管理的角度来看,最重要的是,在订阅的那一刻,接收sink将获得完成该工作所需的一切:

  • 事件源(也称为Observable ),
  • 事件接收者(称为observer ),
  • 与运营商相关的数据(例如,平面映射闭包),
  • SinkDisposer实例(名称为cancel )。

sink可以自由存储尽可能多的这些引用,以提供所需的操作员行为。 至少,它将存储observer ,以及稍后将至关重要的SinkDisposer 。 可能更多! 查看内存图,接收sink迅速成为与订阅相关的对象群中的北极星。

但是,observable的run方法还返回了一个对象。 它是subscription 。 该对象负责处理在处理预订时应运行的逻辑。 还记得创建运算符吗? 它需要返回一个Disposable的闭包, Disposable是负责执行清理的对象。 这与从AnonymousObservableSink的run方法作为subscription返回的Disposable相同。 对于每个操作员,可能要取消一些任务,要释放一些资源,要处置一些内部订阅。 它们都包含在subscription对象中,并且通过subscription.dispose方法公开了执行清除的功能。

Producer的参考周期:Sink和SinkDisposer

知道了这一点,让我们回到subscribe方法实现的最后一个组成部分。 在返回SinkDisposer之前,将调用setSinkAndSubscription方法。 它确实符合您的期望:接收sinksubscription对象通过setter传递并保留在SinkDisposer属性中。 它们被强烈引用,但被包装到Optionals中,这使得稍后可以将引用设置为nil

您是否已经从我们的心理模型中发现了参考周期? 它隐藏在纯净的视线中! sink存储对SinkDisposer的引用,而SinkDisposer存储对sink的引用。 这就是为什么订阅不会在范围出口处释放的原因。 两个对象在永恒的内存锁定中保持彼此的生命,直到应用程序结束。 而且由于接收sinkSinkDisposer保留为非可选属性,因此打破周期的唯一方法是要求SinkDisposer将接收sink Optional引用设置为nil 。 你猜怎么着? 这正是SinkDisposer.dispose方法中发生的事情。 它在disink上调用dispose,然后在subscribe上调用dispose,然后取消引用以打破保留周期。 因此,对于基于Producer的可观察对象, SinkDisposer是我们之前介绍的心理模型的参考周期管理器

在所有这些细节之后,您可能想知道,当可观察的完成时,参考周期如何自我中断? 好吧,我们刚刚说过它需要SinkDisposer.dispose()方法,所以答案很简单。 订阅过程的中心点是接收sink对象,它保留对SinkDisposer的引用,并且还从可观察对象接收所有事件。 因此,一旦它遇到.completed.error事件,并且一旦它自己的逻辑确定这是序列完成,就只需在其SinkDisposer引用上调用dispose方法。 这样,周期从内部被打破了。

为了概括该过程,下面是普通的基于Producer的可观察订阅中的实际参考周期图:

这条路不断

您是否很好奇在非基于Producer的情况下(例如主题或可连接的可观察对象)会发生什么? 这个概念非常相似。 始终存在由某种参考周期管理器控制的参考周期,并且始终存在通过dispose方法调用来打破此周期的方法。 我鼓励您深入RxSwift源代码并亲自看看!

现在很清楚心理模型的来源。 特定订阅的详细信息各不相同,每种可观察类型都有特定的优化,以实现更好的性能和更简洁的体系结构。 但是,基本思想占了上风:存在一个参考周期,打破此周期的唯一方法是要么完成可观察的工作,要么通过参考周期管理器

依赖于可观察性的完成,尽管在许多现实情况中很有用,但始终应谨慎行事。 如果不确定如何处理预订的内存管理,或者只是想让代码对将来的更改更具弹性,那么最好始终默认提供一种明确打破引用周期的机制。

这就是所有的时间。 即将有更多使用RxSwift射击自己的方法。 下次,我们将从另一个角度看待内存管理,而不是关注订阅过程,而是关注传递给操作员的内容。 在此之前,不要忘记在Twitter上关注Polidea,以获取更多与移动开发相关的文章!