概述:
ReactiveCocoa是github開源的一個函數式響應式編程框架,是在iOS平臺上對FRP的實現。FRP的核心是信號,信號在ReactiveCocoa(以下簡稱RAC)中是通過RACSignal
來表示的,信號是數據流,可以被綁定和傳遞。
ReactiveCocoa比較復雜,在正式開始介紹它的核心組件前,我們先來看看它的類圖,以便從宏觀上了解它的層次結構:
ReactiveCocoa主要包含四個組件:
- 信號源:RACStream 及其子類;
- 訂閱者:RACSubscriber 的實現類及其子類;
- 調度器:RACScheduler 及其子類;
- 清潔工:RACDisposable 及其子類。
而信號源是最核心的部分,其它所有組件都是圍繞它運作的。
ReactiveCocoa最簡單的工作過程就是
創建信號——訂閱信號——發送信號。
所以首先我們就來介紹一下信號。
一、信號源
冷信號與熱信號:
信號分為冷信號與熱信號,理解冷信號與熱信號的區別,對于RAC的理解有非常大的幫助,所以我們這篇文章也重點講解這里。
- Hot Observable是主動的,盡管你并沒有訂閱事件,但是它會時刻推送,就像鼠標移動;而Cold Observable是被動的,只有當你訂閱的時候,它才會發布消息。
- Hot Observable可以有多個訂閱者,是一對多,集合可以與訂閱者共享信息;而Cold Observable只能一對一,當有不同的訂閱者,消息是重新完整發送。
而在RAC中除了RACSubject及其子類是熱信號外,其它都是冷信號。subject類似“直播”,錯過了就不再處理。而signal類似“點播”,每次訂閱都會從頭開始。所以我們有理由認定subject天然就是熱信號。
Subject具備如下三個特點:
- Subject是“可變”的。
- Subject是非RAC到RAC的一個橋梁。
- Subject可以附加行為,例如RACReplaySubject具備為未來訂閱者緩沖事件的能力。
我們平常使用RACSignal最簡單的步驟如下:
//創建信號
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//發送信號
[subscriber sendNext:@"發送的數據"];
[subscriber sendCompleted];
return nil;
}];
//接收信號
[signal subscribeNext:^(id x) {
NSLog(@"這里是接收到的數據:%@",x);
}];
為了了解熱信號與冷信號的區別,我們用兩段代碼來展示一下:
//創建熱信號
RACSubject *subject = [RACSubject subject];
[subject sendNext:@1]; //立即發送1
[[RACScheduler mainThreadScheduler] afterDelay:0.5 schedule:^{
[subject sendNext:@2]; //0.5秒后發送2
}];
[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
[subject sendNext:@3]; //2秒后發送3
}];
[[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
[subject subscribeNext:^(id x) {
NSLog(@"subject1接收到了%@",x); //0.1秒后subject1訂閱了
}];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
[subject subscribeNext:^(id x) {
NSLog(@"subject2接收到了%@",x); //1秒后subject2訂閱了
}];
}];
//創建冷信號
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[[RACScheduler mainThreadScheduler] afterDelay:0.5 schedule:^{
[subscriber sendNext:@2];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
[subscriber sendNext:@3];
}];
return nil;
}];
[[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
[signal subscribeNext:^(id x) {
NSLog(@"signal1接收到了%@", x);
}];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
[signal subscribeNext:^(id x) {
NSLog(@"signal2接收到了%@", x);
}];
}];
猜想一下上面兩段代碼的輸出會是什么
。。。
。。。
。。。
。。。
。。。
2018-02-20 11:02:24.980462+0800 RACTest[14912:16295891] subject1接收到了2
2018-02-20 11:02:26.480232+0800 RACTest[14912:16295891] subject1接收到了3
2018-02-20 11:02:26.480408+0800 RACTest[14912:16295891] subject2接收到了3
2018-02-20 11:20:53.952995+0800 RACTest[15075:16311621] signal1接收到了1
2018-02-20 11:20:54.456881+0800 RACTest[15075:16311621] signal1接收到了2
2018-02-20 11:20:54.457046+0800 RACTest[15075:16311621] signal1接收到了3
2018-02-20 11:20:54.853391+0800 RACTest[15075:16311621] signal2接收到了1
2018-02-20 11:20:55.356641+0800 RACTest[15075:16311621] signal2接收到了2
2018-02-20 11:20:55.356851+0800 RACTest[15075:16311621] signal2接收到了3
兩段代碼很簡單,我也做了注釋,就不再多做解釋,從輸出中我們可以發現:
- 0.1秒后訂閱的subject1接收到了0.5秒后2秒后發送的信號,沒有接收到之前發送的新號。
- 1秒后訂閱的subject2接收到了2秒后發送的信號,也沒有接收到之前發送的新號。
- signal1和signal2都接收到了所有信號。
從中我們可以得出結論:
- 熱信號是主動的,即使你沒有訂閱事件,它仍然會時刻推送。如上面沒有接收到的信號都是因為在沒有訂閱者的時候,它也會推送出去。而冷信號是被動的,只有當你訂閱的時候,它才會發送消息。如第二段代碼,訂閱后才把信號推送出去。
- 熱信號可以有多個訂閱者,是一對多,信號可以與訂閱者共享信息。如第一段代碼,訂閱者1和訂閱者2是共享的,他們都能在同一時間接收到3這個值。而冷信號只能一對一,當有不同的訂閱者,消息會從新完整發送。如第一個例子,我們可以觀察到兩個訂閱者沒有聯系,都是基于各自的訂閱時間開始接收消息的。
將冷信號轉變為熱信號
RAC庫中對于冷信號轉化成熱信號有如下標準的封裝:
- (RACMulticastConnection *)publish;
- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACSignal *)replay;
- (RACSignal *)replayLast;
- (RACSignal *)replayLazily;
如上面的第一段代碼,我們可以用如下來達到同樣的效果:
RACMulticastConnection *connection = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[[RACScheduler mainThreadScheduler] afterDelay:0.5 schedule:^{
[subscriber sendNext:@2];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
[subscriber sendNext:@3];
}];
return nil;
}] publish];
[connection connect];
RACSignal *signal = connection.signal;
[[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
[signal subscribeNext:^(id x) {
NSLog(@"這里是熱信號1,接收到了%@", x);
}];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
[signal subscribeNext:^(id x) {
NSLog(@"這里是熱信號2,接收到了%@", x);
}];
}];
輸出如下:
2018-02-20 11:45:38.331464+0800 RACTest[15171:16331870] 這里是熱信號1,接收到了2
2018-02-20 11:45:39.830300+0800 RACTest[15171:16331870] 這里是熱信號1,接收到了3
2018-02-20 11:45:39.830870+0800 RACTest[15171:16331870] 這里是熱信號2,接收到了3
可以看到,現在已經是熱信號了,和前面的RACSubject相同。
感興趣的同學可以去看看- (RACMulticastConnection *)multicast:(RACSubject *)subject
這個方法的實現,它是將冷信號轉換為熱信號的核心。其實它的本質就是使用一個Subject來訂閱原始信號,并讓其他訂閱者訂閱這個Subject,這個Subject就是熱信號。
使用信號中常見的問題:
1、多次訂閱
對RAC的信號進行轉換的時候,其實就是對原有的信號進行訂閱從而產生新的信號。如下代碼所示:
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"來了");
//網絡請求,產生model
[subscriber sendNext:model];
return nil;
}];
RACSignal *name = [signal flattenMap:^RACStream *(Person *model) {
return [RACSignal return:model.name];
}];
RACSignal *age = [signal flattenMap:^RACStream *(Person *model) {
return [RACSignal return:model.age];
}];
RAC(self.userNameTextFiled,text) = [[name catchTo:[RACSignal return:@"error"]] startWith:@"name:"];
RAC(self.passwordTextField,text) = [[age catchTo:[RACSignal return:@"error"]] startWith:@"age:"];
上面分別對model進行了map,也就是產生了兩個新的信號,然后再對兩個信號進行訂閱,對這兩個信號訂閱的時候,也會對間接對原信號進行訂閱,從而造成對原信號的多次訂閱,如上所示來了就輸出了三次,如果是網絡請求的話,也會輸出三次,所以一定在信號轉換的時候一定要注意這些情況。
要解決也很簡單,把signal轉換成熱信號就行了
RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"來了");
[subscriber sendNext:model];
return nil;
}] replayLazily]; //轉換為熱信號
RACSignal *name = [signal flattenMap:^RACStream *(Person *model) {
return [RACSignal return:model.name];
}];
RACSignal *age = [signal flattenMap:^RACStream *(Person *model) {
return [RACSignal return:model.age];
}];
RAC(self.userNameTextFiled,text) = [[name catchTo:[RACSignal return:@"error"]] startWith:@"name:"];
RAC(self.passwordTextField,text) = [[age catchTo:[RACSignal return:@"error"]] startWith:@"age:"];
2、內存泄漏
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { //1
Person *model = [[Person alloc] init];
[subscriber sendNext:model];
[subscriber sendCompleted];
return nil;
}];
self.flattenMapSignal = [signal flattenMap:^RACStream *(Person *model) { //2
return RACObserve(model, name);
}];
[self.flattenMapSignal subscribeNext:^(id x) { //3
NSLog(@"recieve - %@", x);
}];
如上代碼,看起來工作正常,但你使用內存檢測工具會發現,這里會造成內存泄漏,原因就是
#define RACObserve(TARGET, KEYPATH) \
({ \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \
__weak id target_ = (TARGET); \
[target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
_Pragma("clang diagnostic pop") \
})
這段代碼,所以這里的Block引用了self,就造成了循環引用。
解決辦法也很簡單,使用@weakify和@strongify即可:
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
Person *model = [[Person alloc] init];
[subscriber sendNext:model];
[subscriber sendCompleted];
return nil;
}];
@weakify(self);
self.flattenMapSignal = [signal flattenMap:^RACStream *(Person *model) {
@strongify(self);
return RACObserve(model, name);
}];
[self.flattenMapSignal subscribeNext:^(id x) {
NSLog(@"recieve - %@", x);
}];
本來還想介紹一下另外三個組件的,但是由于時間匆忙,馬上要去趕火車了,暫時就寫到這里。剩下三個我就簡單介紹一下吧
訂閱者:在 ReactiveCocoa 中,訂閱者是一個抽象的概念,所有實現了 RACSubscriber 協議的類都可以作為信號源的訂閱者。
@protocol RACSubscriber <NSObject>
@required
/// Sends the next value to subscribers.
///
/// value - The value to send. This can be `nil`.
- (void)sendNext:(id)value;
/// Sends the error to subscribers.
///
/// error - The error to send. This can be `nil`.
///
/// This terminates the subscription, and invalidates the subscriber (such that
/// it cannot subscribe to anything else in the future).
- (void)sendError:(NSError *)error;
/// Sends completed to subscribers.
///
/// This terminates the subscription, and invalidates the subscriber (such that
/// it cannot subscribe to anything else in the future).
- (void)sendCompleted;
/// Sends the subscriber a disposable that represents one of its subscriptions.
///
/// A subscriber may receive multiple disposables if it gets subscribed to
/// multiple signals; however, any error or completed events must terminate _all_
/// subscriptions.
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
@end
即實現了這四個方法的類。
調度器:RACScheduler 在 ReactiveCocoa 中就是扮演著調度器的角色,本質上,它就是用 GCD 的串行隊列來實現的,并且支持取消操作。是的,在 ReactiveCocoa 中,并沒有使用到 NSOperationQueue 和 NSRunloop 等技術,RACScheduler 也只是對 GCD 的簡單封裝而已。
清潔工:RACDisposable 在 ReactiveCocoa 中就充當著清潔工的角色,它封裝了取消和清理一次訂閱所必需的工作。它有一個核心的方法 -dispose ,調用這個方法就會執行相應的清理工作,這有點類似于 NSObject 的 -dealloc 方法。
好了好了,趕火車去了~~~ 祝大家新年快樂!