上一篇文章已经简单的介绍了ReactiveCocoa框架的思想和优势。本文初步研究一下ReactiveCocoa框架的使用方法。
传统的编程思想,大概是用户产生某个事件,然后得到相应的参数,传入事先已经实现的方法中,处理完成后把结果在UI界面上反馈出来。ReactiveCocoa框架中大量的使用了block,这意味着,很多block内的代码,是在将来某一个合适的时刻被执行的。如果你看到block里某个参数并没有被赋值,也没有传入参数,不要奇怪,程序运行到这里的时候还不会执行这个block,至于等到需要执行block的时候,会有参数传入的。这是新手在使用block时非常容易产生的误区,如果没有理解这一点,在看代码的时候会产生相当大的麻烦。
ReactiveCocoa最基本也是最关键的一个概念叫做信号(Signal)。官方给出的文档中对于信号如此定义:
[RACSignal] is a push-driven stream with a focus on asynchronous event
delivery through subscriptions.
当然这个解释是非常抽象的,一会儿再谈。
个人感觉,linyawen的博客中对信号的解释非常生动形象
信号是数据流,可以被绑定和传递。可以把信号想象成水龙头,只不过里面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况)。水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开。这样只要有新的玻璃球进来,就会自动传送给接收方。
请牢牢记住“信号就像一个水龙头”这个概念。它允许各种各样的数据从它这里流过。一个信号可以捕捉当前和未来的值。
回到最初的官方给出的signal的定义,push-driven stream表示,signal是一个由推动力驱动的数据流。即产生一个数据就push一次并进行一系列处理。反过来说,pull-driven表示处理完一个数据才会要求产生一个数据。
对于一个signal来说,刚刚创建的时候,它还是一个冷信号(Cold signal),只有在有了订阅者(Subscriber)之后,才会变为热信号(Hot signal)。订阅者就好比水龙头最下方的水盆,只有放好了水盆,水龙头才能打开。不然,水(value)不都浪费了么?
如果目前对于信号和订阅者还不了解,这是正常情况。接下来通过代码一起了解他们的工作机制。
ReactiveCocoa的框架的安装已经在前文谈过,这里不再细讲。具体操作方法参见这里。
创建一个空白的工程,从storyboard里面拖入一个UITextField,不妨命名为searchText。
在viewDidLoad方法里面添加以下代码:
[self.searchText.rac_textSignal subscribeNext: ^(id text){
NSLog(@"%@", text);
}];
运行一下程序,在textfield中输入“hello”。输出结果应该是这样的
2015-05-28 14:55:57.785 TwitterInstant[716:114961] h
2015-05-28 14:55:58.135 TwitterInstant[716:114961] he
2015-05-28 14:55:58.303 TwitterInstant[716:114961] hel
2015-05-28 14:55:58.415 TwitterInstant[716:114961] hell
2015-05-28 14:55:58.663 TwitterInstant[716:114961] hello
可以发现,每一次textfield中文字发生变化,都执行了block中的代码。
一切脱离原理讲效果的行为都是耍流氓。相信任何第一次接触这个框架的读者看到这里一定是一头雾水。很可能你会问这些问题:
下面就是时候看看具体的原理了。千万不要着急学习更多的知识,有时候打好基础,彻底理解一个框架的原理对以后的深入学习更有帮助。请记住这四个问题,如果你能不假思索的回答出来,那么就可以开始进一步的学习了。
不妨把上述代码自己敲一遍。searchText后面输入.r的时候已经会出现Xcode的智能提示了。
这里的rac_textSignal就是一个信号。执行了subscribeNext方法后,这个信号被订阅(水龙头下面摆好水盆了)。每当有新的数据(textfiled中内容)出现,信号就捕捉到这个值。把这个值传入block中,并且执行block里的代码,于是就打印出了我们看到的数据。为了搞懂为什么新数据出现时,block被调用,我们需要看看subscribeNext方法的具体实现。
- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
NSCAssert(NO, @"This method must be overridden by subclasses");
return nil;
}
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock {
NSCParameterAssert(nextBlock != NULL);
RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
return [self subscribe:o];
}
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock completed:(void (^)(void))completedBlock {
NSCParameterAssert(nextBlock != NULL);
NSCParameterAssert(completedBlock != NULL);
RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:completedBlock];
return [self subscribe:o];
}
这里从源代码中简单的节选了几个方法的实现。不难发现,subscribeNext是一个方法簇,除了不带block的那个方法(第一个方法)之外,每一个方法中都创建了一个RACSubscriber(订阅者)对象。subscribeNext方法返回一个RACDisposable(销毁者)对象,这个对象可以用来销毁一个信号,大部分时候没有必要这么做。因为没有被持有的信号会被自动释放。
看到这里,有一个问题的答案已经很明显了:谁是订阅者(subscriber)?
很显然,我们不可能像找到signal那样说rac_textSignal就是一个信号对象,但是订阅者对象就藏在subscribeNext方法中。任何简化的不完全的subscribeNext方法都可以拓展成一个完整的subscribeNext方法:
self.searchText.rac_textSignal subscribeNext:^(id x) {
<#code#>
} error:^(NSError *error) {
<#code#>
} completed:^{
<#code#>
}
而这里我们指定的三个block将会被用来构建一个订阅者(subscriber)。
还记得第一个demo的四个问题么?回想一下,是不是已经解决问题三和四了呢?为了弄明白前两个问题,我们来看一看信号的工作原理。rac_textSignal是一个已经被创建好的信号,为了不是一般性,我们自己创建一个信号:使用createSignal方法。
- (RACSignal *)createSignal{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"signal created");
return nil;
}];
}
有了这个方法,我们就可以创建一个最简单的自定义的信号了。在viewDidLoad方法中加入这样一行代码:
RACSignal *signal = [self createSignal];
运行程序看一看效果吧。并没有什么卵用。。。。。。。这是必然的。说得形象一点,我们搞出来一个水龙头,但是下面还没有放上盆子。说得专业一点,这还是一个冷信号(Cold signal)。还没有被订阅(Subscribe)。归根结底,我们不了解这个createSignal方法的实现原理。
Ok,按住command键点击方法名,跳过去看看createSignal到底是个啥玩意儿。
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
return [RACDynamicSignal createSignal:didSubscribe];
}
这个方法有一个block。block需要一个RACSubscriber(订阅者)对象作为传入的参数,会传出一个销毁者(RACDisposable)对象。block名叫做didSubscribe,显然这个block会在block被subscribe之后触发。现在明白冷信号是什么意思了么?没有订阅者(Subscriber)的时候,用于创建signal的block都没有被执行,这个信号当然没有任何卵用。
在viewDidLoad方法中再加一行代码呢:
[signal subscribeNext:^(id x) { ; }];
即使只是一个空的block,但是因为signal现在被订阅(Subscribe)了,之前的名为didSubscribe的block就被执行了。此时会输出
2015-05-28 15:36:42.661 TwitterInstant[823:253903] signal created
修改一下刚刚的代码,增加一个NSLog的功能。修改后的subscribeNext方法应该像这样:
RACSignal *signal = [self createSignal];
[signal subscribeNext:^(id x) {
NSLog(@"aaa");
}];
运行程序。Oh,Shit!依然没有任何卵用。说好的会执行subscribeNext的block内的代码呢?aaa怎么没有打出来?
其实,仔细一想,这样的结果反而是合理的。我们之前讨论的信号(Signal)被订阅(Subscribe)之后,信号这个水龙头的阀门被打开,水流过信号,落入盆子里。可是为啥落入盆子里就会调用subscribeNext的block中的代码?文章的最初写了,block只是一段预先定义好的代码,在将来的某个时间被调用。可我们至今没有在任何代码中看到这样的调用。我们还是再好好看一看订阅者(Subscriber)吧。
之前我们简单的看了订阅者(Subscriber)对象的工作原理:在subscribeNext方法中我们创建了一个订阅者(Subscriber)对象。这个对象是怎么被创建的呢?关键在于那个出现了无数次的subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed方法:
+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed {
RACSubscriber *subscriber = [[self alloc] init];
subscriber->_next = [next copy];
subscriber->_error = [error copy];
subscriber->_completed = [completed copy];
return subscriber;
}
看到这里,我想已经非常清楚了。订阅者(Subscriber)对象持有三个属性,都是block。在订阅者的创建过程中,对这三个进行了赋值。等待在将来的某一刻被调用。这就好比这个盆子(订阅者)有许多功能,对于不同的滴水状态,具备不同的处理方法。
同时订阅者(Subscriber)对象还对外提供了三个方法的调用接口:
- (void)sendNext:(id)value; - (void)sendError:(NSError *)error; - (void)sendCompleted;
以- (void)sendNext:(id)value方法为例看一看它的实现:
- (void)sendNext:(id)value {
@synchronized (self) {
void (^nextBlock)(id) = [self.next copy];
if (nextBlock == nil) return;
nextBlock(value);
}
}
其实最核心的功能即时调用了自己的nextBlock并传入相应的参数而已。
那么订阅者(Subscriber)对象的这三个方法什么时候被调用呢?又应该调用哪一个呢?答案是:由信号决定。试想一下:水龙头告诉底下的盆子:“我要滴水了,你存一下吧。”。这就是让订阅者(盆子)执行nextBlcok。如果水龙头说:“我滴完水了,你可以歇一会了。”这就是让订阅者(盆子)执行completeBlock。如果水龙头说:“我滴不了水了,坏掉了,你可以歇一会了。”这就是让订阅者(盆子)执行errorBlock。显然,在一个信号(Signal)的生命周期中,可以发送无数次next事件,和唯一一次complete或者error事件。
改造一下最初创造signal的方法:
- (RACSignal *)createSignal{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"signal created");
[subscriber sendNext:nil];
return nil;
}];
}
再次运行,就会发现aaa已经被打印出来了。关键就在于这一次我们调用了订阅者(Subscriber)的sendNext方法。
说了这么多,回头看看问题一。重点在于这个rac_textSignal。如果有兴趣可以看看它的实现。其实本质上还是监听了textfield的text属性。当它发生变化时,就把它的值传出来。至于为什么每次传出的都是text值,这是由这个signal的创建方法决定的。
至于问题二,我们执行了subscribeNext方法创建了一个订阅者(Subscriber),这个订阅者的nextBlcok方法已经被赋值。而rac_textSignal这个信号的实现中,在每次text发生变化的时候,就会调用订阅者的sendNext方法,从而调用nextBlcok中的代码。
这就是一个信号(Signal)与订阅者(Subscriber)的简单实用。当然,ReactiveCocoa框架的功能远不止这些。下一章,我们来讨论一下更多的关于信号(Signal)的使用方法——《ReactiveCocoa框架菜鸟入门——信号(Signal)详解》