内存管理

对于一个基于图论的框架来说,节点和边是最小的部件。实际应用中,这些部件构成了各种有向图。比如一个有环图,它的数据流动就是一个环形,部件之间的持有关系如果不能很好的处理,那么可能就会存在内存问题。EasyReact 的内存管理逻辑非常简单,也非常精巧。可以让框架使用者无需关注太多的细节即可轻松的使用,而不必担心本框架涉及的内存方面的问题。

目录

中间节点

节点包含了 fork、map、filter、skip、take、ignore、select 等多种操作。对于绝大部分的节点操作,都会返回新的节点,并通过某种变换把源节点和新节点连接起来。由于返回的也是节点,这让我们可以用链式方法调用,而不必用变量保存每一次操作返回的节点。举个例子,我们可以对 someNode 先 map 再 filter,而通常我们不关心 map 返回的节点,因为这是一个中间节点,它连接到了最终 filter 操作创建出来的节点上。

生命周期

在本框架中,节点、变换、监听边和监听者组成了有向图的结构,维系着数据的响应关系。由于本框架是面向对象的响应式框架,所以节点、变换、监听边和监听者都是对象。不管是否保存了中间节点,如何维持这些对象的生命周期来让整个响应关系保持稳定,是一个重要的问题。

本框架中,节点、变换、监听边和监听者的持有规则如下:

  • 一个监听者强持有了其所有上游监听边;
  • 一个边的 from 强持有了一个节点,而 to 弱引用了下游节点或者监听者;
  • 一个节点强持有了其所有的上游变换,弱引用了其所有下游变换和下游监听边。

也就是说,在一个响应链中,始终是数据的消费者持有了数据的提供者。当数据不再有消费者时,数据的提供者也就没有必要存在了。

API使用分析

对于某个使用 EZRNode\ 的 API 接口中,通常会暴露一个 EZRNode 节点。
对于这个 API 的使用者来言,会有两种经典的场景。

  1. 对一个节点进行变换得到衍生节点,示例如下:
  1. ERNode<NSNumber *> *node = [ERNode value:@1];
  2. ERNode<NSNumber *> *filteredNode = [[node map:^id _Nullable(NSNumber * _Nullable next) {
  3. return @([next.integerValue * 2]);
  4. }] filter:^BOOL(NSNumber * _Nullable next) {
  5. return next.integerValue > 0;
  6. }];
  1. 对一个节点的值进行监听,示例如下:
  1. ERNode<NSNumber *> *node = [ERNode value:@1];
  2. NSObject *listener = [NSObject new];
  3. [[node listenedBy:listener] withBlock:^(id next) {}];

需要注意的是,在得到衍生节点的例子中,对节点的每一次变换操作都会得到一个新的衍生节点。所以实际上会有一个中间节点存在。

  1. Node ==MapTransform==> MappedNode ==FilterTransform==> FilteredNode

以上的节点衍生关系中存在 3 个节点对象和 2 个变换对象。其强引用关系正好相反,如下:

  1. Node <-- MapTransform <-- MappedNode <-- FilterTransform <-- FilteredNode

因此一旦 FilteredNode 被销毁,则其他对象自动释放(是否销毁取决于是否还有其他对象对其强引用)。

在监听节点的例子中,节点和监听者通过监听边来进行连接。

  1. Node ==BlockListenEdge==> Listener

以上的监听关系中存在 2 个节点对象和 1 个监听边对象。其强引用关系也正好相反,如下:

  1. Node <-- BlockListenEdge <-- Listener

因此一旦 Listener 被销毁,其他对象会自动释放(是否销毁取决于是否还有其他对象对其强引用)。

关于 self

需要注意的是通常我们在监听的方法里面会使用到 self,前面提到过 self 已经持有了监听边,如果监听边捕获了 self,将会出现循环引用从而引起内存泄露。例如:

  1. [[someNode listenedBy:self] withBlock:^(id next){
  2. [self doSomething];
  3. }];
  4. // someNode <-- BlockListenEdge <-- self
  5. // | ↑
  6. // └---------------┘

为此我们提供了 @ezr_weakify(...)@ezr_strongify(...) 来解决循环引用的问题。

最佳实践如下:

  1. @ezr_weakify(self)
  2. [[someNode listenedBy:self] withBlock:^(id next){
  3. @ezr_strongify(self)
  4. [self doSomething];
  5. }];
  6. // 其他对象同样需要
  7. @ezr_weakify(someObject)
  8. [[someNode listenedBy:someObject] withBlock:^(id next){
  9. @ezr_strongify(someObject)
  10. [someObject doSomething];
  11. }];

关于节点环

框架设计中已经处理了对上游链路的强引用,所以节点环就会产生循环引用,一旦产生需要记得必要的时刻进行主动的破除操作,例如下面的例子:

  1. EZRNode<NSNumber *> *nodeA = [EZRNode new];
  2. EZRNode<NSNumber *> *nodeB = [EZRNode new];
  3. EZRNode<NSString *> *nodeC = [EZRNode new];
  4. EZRTransform *transformAtoB = [[EZRMapTransform alloc] initWithMapBlock:^NSNumber *(NSNumber *next) {
  5. return @(next.integerValue * 2);
  6. }];
  7. EZRTransform *transformBtoC = [[EZRMapTransform alloc] initWithMapBlock:^NSString *(NSNumber *next) {
  8. return next.stringValue;
  9. }];
  10. EZRTransform *transformCtoA = [[EZRMapTransform alloc] initWithMapBlock:^NSNumber *(NSString *next) {
  11. return @(next.integerValue / 2);
  12. }];
  13. transformAtoB.from = nodeA;
  14. transformAtoB.to = nodeB;
  15. transformBtoC.from = nodeB;
  16. transformBtoC.to = nodeC;
  17. transformCtoA.from = nodeC;
  18. transformCtoA.to = nodeA;
  19. // 强引用关系如下:
  20. // nodeA <-- transformAtoB <-- nodeB <-- transformBtoC <-- nodeC
  21. // | ↑
  22. // └-------------------->transformCtoA---------------------┘

这样,全部的 3 个变换和 3 个节点都不会销毁了,所以 请务必记得在必要的时刻对任意的边或者点进行破除操作,方法如下:

  1. // 破除边
  2. transformAtoB.from = nil;
  3. // 或破除点
  4. [nodeA removeDownstreamNode:nodeB];

通常情况下我们会对两个节点进行同步而不是更多节点,我们提供了- (id<EZRCancelable>)syncWith:(EZRNode<T> *)otherNode- (id<EZRCancelable>)syncWith:(EZRNode *)otherNode transform:(id (^)(T source))transform revert:(T (^)(id target))revert这两个便捷的方法,它们都提供了id<EZRCancelable>这个对象,通过该对象的- (void)cancel方法,我们就可以快速的破除这两个节点的环了,示例如下:

  1. EZRMutableNode<NSNumber *> *nodeA = [EZRMutableNode new];
  2. EZRMutableNode<NSString *> *nodeB = [EZRMutableNode new];
  3. id<EZRCancelable> cancelable = [nodeA syncWith:nodeB transform:^id _Nonnull(NSNumber * _Nonnull source) {
  4. return source.stringValue;
  5. } revert:^NSNumber * _Nonnull(id _Nonnull target) {
  6. return @([target integerValue]);
  7. }];
  8. NSObject *obj = [NSObject new];
  9. [[obj listen:nodeA] withBlock:^(id _Nullable next) {
  10. NSLog(@"nodeA value = %@", next);
  11. }];
  12. [[obj listen:nodeB] withBlock:^(id _Nullable next) {
  13. NSLog(@"nodeB value = %@", next);
  14. }];
  15. nodeA.value = @1;
  16. nodeB.value = @"11";
  17. // 不在需要同步时,一定要取消同步
  18. [cancelable cancel];