iOS之ReactiveCocoa源码及难点分析

ReactiveCocoa是一个非常复杂的框架,在正式开始介绍它的核心组件前,我们先来看看它的类图,以便从宏观上了解它的层次结构:


iOS之ReactiveCocoa源码及难点分析_第1张图片


从上面的类图中,我们可以看出,ReactiveCocoa 主要由以下四大核心组件构成:

  • 信号源:RACStream 及其子类;
  • 订阅者:RACSubscriber 的实现类及其子类;
  • 调度器:RACScheduler 及其子类;
  • 清洁工:RACDisposable 及其子类。

其中,信号源又是最核心的部分,其他组件都是围绕它运作的。



iOS之ReactiveCocoa源码及难点分析_第2张图片




RACSignal和RACSubject的区别

RACSubject:信号提供者,自己可以充当信号,又能发送信号  使用场景: 通常用来代替代理,有了它,就不必要定义代理了

RACSubject使用步骤
1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
2.订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
3.发送信号 sendNext:(id)value


RACSubject:底层实现和RACSignal不一样。
1.调用subscribeNext订阅信号,只是把订阅者保存起来,并且订阅者的nextBlock已经赋值了。
2.调用sendNext发送信号,遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。


RACSubject实例 进行map操作之后 , 发送完毕一定要调用-sendCompleted, 否则会出现内存泄漏; 
RACSignal实例不管是否进行map操作, 不管是否调用-sendCompleted, 都不会出现内存泄漏.
原因 : 因为RACSubject是热信号, 为了保证未来有事件发生的时候, 订阅者可以收到信息, 所以需要对持有订阅者!





map 和 flattenMap 的区别

Map适用于:信号发出的是值
FlatternMap适用于:信号发出的是信号 

先来看看RACStream.h文档说明:


/// Maps `block` across the values in the receiver.

///

/// This corresponds to the `Select` method inRx.(Rx见下面备注)

///

/// Returns a new stream with the mapped values.

- (instancetype)map:(id (^)(id value))block;


/// Maps `block` across the values in the receiver and flattens the result.

///

/// Note that operators applied _after_ -flattenMap: behave differently from

/// operators _within_ -flattenMap:. See the Examples section below.

///

/// This corresponds to the `SelectMany` method in Rx.

///

/// block - A block which accepts the values in the receiver and returns a new

///         instance of the receiver's class. Returning `nil` from this block is

///         equivalent to returning an empty signal.

///

/// Examples

///

///   [signal flattenMap:^(id x) {

///       // Logs each time a returned signal completes.

///       return [[RACSignal return:x] logCompleted];

///   }];

///

///   [[signal

///       flattenMap:^(id x) {

///           return [RACSignal return:x];

///       }]

///       // Logs only once, when all of the signals complete.

///       logCompleted];

///

/// Returns a new stream which represents the combined streams resulting from

/// mapping `block`.

- (instancetype)flattenMap:(RACStream * (^)(id value))block;


什么是Rx:
“Reactive Extensions(Rx)是 微软公司的一个类库,它集成了异步、基于可观察(observable)序列的事件驱动编程和LINQ-style的查询操作。使用Rx,开发人员可以用observable对象描述异步数据流,使用LINQ操作符异步查询数据和使用Schedulers控制异步过程中的并发。简而言之,Rx = Observables + LINQ + Schedulers。”

  1. - (void)map {  
  2.       
  3.     // Map使用步骤:  
  4.     // 1.传入一个block,类型是返回对象,参数是value  
  5.     // 2.value就是源信号的内容,直接拿到源信号的内容做处理  
  6.     // 3.把处理好的内容,直接返回就好了,不用包装成信号,返回的值,就是映射的值。  
  7.       
  8.     // Map底层实现:  
  9.     // 0.Map底层其实是调用flatternMap,Map中block中的返回的值会作为flatternMap中block中的值。  
  10.     // 1.当订阅绑定信号,就会生成bindBlock。  
  11.     // 3.当源信号发送内容,就会调用bindBlock(value, *stop)  
  12.     // 4.调用bindBlock,内部就会调用flattenMap的block  
  13.     // 5.flattenMap的block内部会调用Map中的block,把Map中的block返回的内容包装成返回的信号。  
  14.     // 5.返回的信号最终会作为bindBlock中的返回信号,当做bindBlock的返回信号。  
  15.     // 6.订阅bindBlock的返回信号,就会拿到绑定信号的订阅者,把处理完成的信号内容发送出来。  
  16.       
  17.       
  18.       
  19.     // Map作用:把源信号的值映射成一个新的值  
  20.     // 创建信号  
  21.     RACSubject *subject = [RACSubject subject];  
  22.     // 绑定信号  
  23.     RACSignal *bindSignal = [subject map:^id(id value) {  
  24.           
  25.         // 返回的类型就是你需要映射的值  
  26.         return [NSString stringWithFormat:@"ws:%@", value]; //这里将源信号发送的“123” 前面拼接了ws:  
  27.     }];  
  28.     // 订阅绑定信号  
  29.     [bindSignal subscribeNext:^(id x) {  
  30.         NSLog(@"%@", x);  
  31.     }];  
  32.     // 发送信号  
  33.     [subject sendNext:@"123"];  
  34.   
  35. }  
  36.   
  37. /* 
  38.  FlatternMap和Map的区别 
  39.   
  40.  1.FlatternMap中的Block返回的是信号。 
  41.  2.Map中的Block返回的是对象。 
  42.  3.Map适用于:信号发出的是值
  43.  4.FlatternMap适用于:信号发出的是信号 
  44.  */  
  45.   
  46.   
  47.   
  48. - (void)flatMap {  
  49.       
  50.     // 创建信号  
  51.     RACSubject *subject = [RACSubject subject];  
  52.     // 绑定信号  
  53.     RACSignal *bindSignal = [subject flattenMap:^RACStream *(id value) {  
  54.         // block:只要源信号发送内容就会调用  
  55.         // value: 就是源信号发送的内容  
  56.         // 返回信号用来包装成修改内容的值  
  57.         return [RACReturnSignal return:value];  
  58.           
  59.     }];  
  60.       
  61.     // flattenMap中返回的是什么信号,订阅的就是什么信号(那么,x的值等于value的值,如果我们操纵value的值那么x也会随之而变)  
  62.     // 订阅信号  
  63.     [bindSignal subscribeNext:^(id x) {  
  64.         NSLog(@"%@", x);  
  65.     }];  
  66.       
  67.     // 发送数据  
  68.     [subject sendNext:@"123"];  
  69.       
  70. }  
  71.   
  72. - (void)flattenMap2 {
  73.     // 该例子中,flattenMap 主要用于信号中的信号  
  74.     // signalOfsignals 一般用 FlatternMap  
  75.     // 创建信号  
  76.     RACSubject *signalofSignals = [RACSubject subject];  
  77.     RACSubject *signal = [RACSubject subject];  

  78.     [[signalofSignals flattenMap:^RACStream *(id value) {  
  79.         return value;  
  80.     }] subscribeNext:^(id x) { // subscribeNext:调用bind 的@autoreleasepool{  } 会将subscribeNext:生成的subscriber 保存到RACSubject中
  81.         NSLog(@"%@", x);  
  82.     }];  
  83.       
  84.     // 发送信号  
  85.     [signalofSignals sendNext:signal];  // 会执行 80行的 return value; 
  86.     [signal sendNext:@"123"];  // 会执行 82行的 NSLog(@"%@", x);
  87. }

例子3:

- (void)flattenMap3 {
// flattenMap 主要用于信号中的信号
// 创建信号
RACSubject *signalofSignals = [RACSubject subject];
RACSubject *signal = [RACSubject subject];

// 订阅信号
//方式1
//    [signalofSignals subscribeNext:^(id x) {
//
//        [x subscribeNext:^(id x) {
//            NSLog(@"%@", x);
//        }];
//    }];
// 方式2
//    [signalofSignals.switchToLatest  ];
// 方式3
//   RACSignal *bignSignal = [signalofSignals flattenMap:^RACStream *(id value) {
//
//        //value:就是源信号发送内容
//        return value;
//    }];
//    [bignSignal subscribeNext:^(id x) {
//        NSLog(@"%@", x);
//    }];
// 方式4--------也是开发中常用的
[[signalofSignals flattenMap:^RACStream *(id value) {
return value;
}] subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

// 发送信号
[signalofSignals sendNext:signal];
[signal sendNext:@"123"];
}





信号组合
//定义2个自定义信号
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
//组合信号
[[RACSignal combineLatest:@[letters, numbers] 
                   reduce:^(NSString *letter, NSString *number){
    //把2个信号的信号值进行字符串拼接
    return [letter stringByAppendingString:number];
}] subscribeNext:^(NSString * x) {
    NSLog(@"%@", x);
}];
 //自己控制发送信号值
[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];//打印B1
[letters sendNext:@"C"];//打印C1
[numbers sendNext:@"2"];//打印C2

iOS之ReactiveCocoa源码及难点分析_第3张图片

从上图来看,第1、第2列输出很容易理解,但第3、4、5列却容易让人摸不着头脑。下面让我们看看文档说明:

iOS之ReactiveCocoa源码及难点分析_第4张图片


可以知道:

1、所有子信号必须都发送信号,才能输出组合信号(combined signal);

2、发送信号可先后发出:子信号可以先后发出信号值(在不同地方调用 [xxx sendNext:xx]);也可理解成事件监听。

3、单个子信号值可重复利用:发送信号值后,该值会一直存在reduce参数表并等待被处理,可以重复被使用,直到有新值覆盖它,可理解成java中的static变量(如上面Letters信号在发送值b后,只要没有新调用[letters sendNext:xx],在reduce:^(NSString *letter, NSString *number)中的letter值将一直会是b),其他子信号同理。


比如上面代码中@"A"没有被输出因为numbers还没有收到一个值。



zipwith一般用于一个界面有多个请求,如:要购买商品,先调用登录接口,接着调用余额查询接口,最后根据查询结果判断余额 >1000才允许购买


- (void)zipWith1 {

    //zipWith:把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元祖,才会触发压缩流的next事件。

    // 创建信号A

    RACSubject *signalA = [RACSubjectsubject];

    // 创建信号B

    RACSubject *signalB = [RACSubjectsubject];

    // 压缩成一个信号

    // **-zipWith-**: 当一个界面多个请求的时候,要等所有请求完成才更新UI

    // 等所有信号都发送内容的时候才会调用

    RACSignal *zipSignal = [signalAzipWith:signalB];

    [zipSignal subscribeNext:^(id x) {

        NSLog(@"%@", x);//所有的值都被包装成了元组

        RACTupleUnpack(NSString *stringA, NSString *stringB) = (RACTuple*)x;

        NSLog(@"我们是%@%@", stringA, stringB);

    }];

    

    // 发送信号交互顺序,元组内元素的顺序不会变,跟发送的顺序无关,而是跟压缩的顺序有关[signalA zipWith:signalB]---先是A后是B

    [signalA sendNext:@1];

    [signalB sendNext:@2];

    [signalB sendNext:@4];

    // 只一次打印输出:我们是12

}


- (void)zipWith2 {

    

    //创建信号A

    RACSignal *signalA = [RACSignalcreateSignal:^RACDisposable *(id subscriber) {

        [subscriber sendNext:@""];

        [subscriber sendNext:@""];

        returnnil;

    }];

    //创建信号B

    RACSignal *signalB = [RACSignalcreateSignal:^RACDisposable *(id subscriber) {

        [subscriber sendNext:@""];

        [subscriber sendNext:@""];

        returnnil;

    }];

    //合流后出来的是压缩包,需要解压才能取到里面的值

    [[signalA zipWith:signalB]subscribeNext:^(RACTuple* x) {

        //解压缩

        RACTupleUnpack(NSString *stringA, NSString *stringB) = x;

        NSLog(@"我们是%@%@", stringA, stringB);

    }];

    // 先后打印两次:我们是黄白的  \n我们上红紫的

    

}



RAC宏自动提示属性

  当使用诸如RAC(self, outputLabel)或RACObserve(self, name)时,发现写完逗号之后,输入第二个property的时候会出现完全正确的代码提示!这相当神奇。自动代码提示:

探究一下,关键的关键是如下一个宏:

#define keypath(...) \
    metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
这个metamacro_argcount上面说过,是计算可变参数个数,所以metamacro_if_eq的作用就是判断参数个数,如果个数是1就执行后面的keypath1,若不是1就执行keypath2。所以重点说一下keypath2:
#define keypath2(OBJ, PATH) \
    (((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
  乍一看真挺懵,先化简,由于Objc里面keypath是诸如”outputLabel.text”的字符串,所以这个宏的返回值应该是个字符串,可以简化成:
#define keypath2(OBJ, PATH) (???????, # PATH)
先不管”??????”是啥,这里不得不说C语言中一个不大常见的语法(第一个忽略):
int a = 0, b = 0;
a = 1, b = 2;
int c = (a, b);
这些都是 逗号表达式(具体介绍看这里)的合理用法,第三个最不常用了,c将被b赋值,而a是一个未使用的值,编译器会给出warning。去除warning的方法很简单,强转成void就行了:
int c = ((void)a, b);
再看上面简化的keypath2宏,返回的就是PATH的字符串字面值了(单#号会将传入值转成字面字符串)
(((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
对传入的第一个参数OBJ和第二个正要输入的PATH做了点操作,这也正是为什么输入第二个参数时编辑器会给出正确的代码提示。强转void就像上面说的去除了warning。

 但至于为什么加入与NO做&&,我不太能理解,我测试时其实没有时已经完成了功能,可能是作者为了屏蔽某些隐藏的问题吧。
  这个宏的巧妙的地方就在于使得编译器以为我们要输入“点”出来的属性,保证了输入值的合法性(输了不存在的property直接报错的),同时利用了逗号表达式取逗号最后值的语法返回了正确的keypath。


RACCommand之executing

//监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次命令

    [[command.executing skip:1] subscribeNext:^(id x) {

        // executing表示了当前RACCommand是否在执行 //(3) (8)

        if ([x boolValue] ==YES) {

            NSLog(@"正在执行");

        }else{

            NSLog(@"执行完成");

        }

    }];

iOS之ReactiveCocoa源码及难点分析_第5张图片

通过executing 可以在订阅者中捕获查看 [RACCommand execute] 执行情况,如果Singal没有被关停并且执行了execute,订阅者将返回YES(即1),反之返回NO(即0)

订阅者会不断被回调,不止执行一次。



RACComand之信号的信号

iOS之ReactiveCocoa源码及难点分析_第6张图片

订阅RACCommand我们可以使用其内部的属性executionSignals返回一个信号,然后对这个信号进行订阅。

[[aCommand executionSignals]
    subscribeNext:^(id x) {
        NSLog(@"%@",x);
    }];

在订阅的block中,我们打印了传递事件x的描述,最后会发现x原来是一个RACSignal,原因是RACCommand中的executionSignals属性是一个包裹着信号的信号,其包裹着的信号就是我们当初在创建RACCommand时进行构建的信号(即[[RACCommandalloc]initWithSignalBlock在Block中return的信号,所以当我们开始执行RACCommand时,executionSignals信号就会立即发送事件,传递出其包裹的信号,我们可以对这个信号进行订阅:

[[aCommand executionSignals]
    subscribeNext:^(RACSignal *x) {
        [x subscribeNext:^(id x) {
            //  Do something...
        }];
    }];

如果你嫌订阅两个事件麻烦的话,可以使用函数switchToLatest进行转换:

[[[aCommand executionSignals]switchToLatest]
    subscribeNext:^(id x) {
        //  Do something...
    }];

这样就比上面少写了一步信号订阅。
如果你想在RACCommand执行时做某些提示操作(弹出等待框,出现转来转去的菊花),并在执行后取消提示,你可以这样写:

[[aCommand executionSignals]
    subscribeNext:^(RACSignal *x) {
        //  开始提示
        [x subscribeNext:^(id x) {
            //  关闭提示
            //  Do something...
        }];
    }];

在对command进行错误处理的时候,我们不应该使用subscribeError:对command的executionSignals进行错误的订阅,因为executionSignals这个信号是不会发送error事件的,那当command包裹的信号发送error事件时,我们要怎样去订阅它呢?这里用到command的一个属性:errors,我们可以这样来对错误进行订阅:

[aCommand.errors
    subscribeNext:^(NSError *x) {
        NSLog(@"ERROR! --> %@",x);
}];








部分资料源自互联网,如果发现侵权,请联系博主删除。


你可能感兴趣的:(iOS)