使用RxSwift时应避免的8个错误-第1部分
细节可能会有所不同,但是不管您的观察对象,观察者或订阅如何,基本含义都保持订阅的含义。 要发现的关键是,忽略参考周期管理器 (又称disposable
,可以使您摆脱自己破坏参考周期的可能性。 它是进入内存配置的门户药物,一旦无法使用,就再也没有回头路了。 如果使用_ =
语法,则基本上可以说,释放observable
observer
和observer
的唯一方法是完成可观察序列。
有时候这可能正是您想要的! 例如,如果您要调用Observable.just
,则不必确保打破周期,这并不重要。 单个元素被瞬时发出,随后是.completed
事件。 但是,在许多情况下,您可能无法完全确定所讨论的可观察性的完成可能性:
- 您从另一个对象获得了
Observable
,文档没有说明它是否完成, - 您从另一个对象获得了
Observable
,并且文档确实说明该对象已完成,但是在此过程中该对象的内部实现发生了一些变化,没有人记得要更新文档, -
Observable
明显未完成(示例包括Variable
,Observable.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
对象到底是什么? 它是存储实际操作员逻辑的地方。 因此,对于该interval
, TimerSink
是安排在每个周期之后发送事件并跟踪内部状态(即已发送了多少个事件)的地方。 对于flatMap
, FlatMapSink
(及其超类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方法。 它确实符合您的期望:接收sink
和subscription
对象通过setter传递并保留在SinkDisposer
属性中。 它们被强烈引用,但被包装到Optionals中,这使得稍后可以将引用设置为nil
。
您是否已经从我们的心理模型中发现了参考周期? 它隐藏在纯净的视线中! sink
存储对SinkDisposer
的引用,而SinkDisposer
存储对sink
的引用。 这就是为什么订阅不会在范围出口处释放的原因。 两个对象在永恒的内存锁定中保持彼此的生命,直到应用程序结束。 而且由于接收sink
将SinkDisposer
保留为非可选属性,因此打破周期的唯一方法是要求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,以获取更多与移动开发相关的文章!
- 用AVAudioPlayer播放声音
- 如何在iOS 10 / Sierra / Xcode 8中导入新的日志loggingfunction?
- 调整UIScrollView中的内容视图的大小无效
- 使用iOS下载的软件包中包含的XIB / NIB文件
- APNS令牌是否应该encryption?
- TouchID调用applicationWillResignActive和applicationDidBecomeActive
- (Swift)如何在string中打印“\”字符?
- 通过Swift 3和Alamofire 4 |上传带有JSON和自定义标题的照片/文件 iOS | 迅速
- FOR命令中的expression式(for(int i = 0; i <( -1); i ++){})