开篇扯谈——响应式编程
请看以下的式子:
a = 2
b = 2
c = a + b // c is 4
d = c + 2 // d is 6
b = 3
// now what is the value of d?
在传统的编程的方式里,我就要重新计算c
,然后再去求d
出来。大概是这样:
c = a + b // c is 5
d = c + 2 // d is 7
在代码层面上,大概就是起码要再写两句代码,才能让d
获得一个新的值。
幸好,我们现在可以利用响应式的方法,将d
绑定到c
上,c
就绑定到a + b
上。然后上面的操作通通不用做,d
的值会时刻保持为当前值。
以上就是基本的原理,但实际使用肯定不是这么简单的绑定响应,而是对文字、控件大小、网络请求等,组成各种复杂的响应链。这个响应链就是每一个事件流的组合,像链条一样发生连锁响应。
iOS上的响应式编程
iOS的设计模式
这就是目前iOS应用普遍使用的主体框架的设计模式
显然不是,这实际上只是iOS上的MVC而不是MVC的完整概念。
其实MVC这一层还有一个概念,就是Model与View的绑定。但是显然,iOS原生根本不提供这种支持,所以绑定也就无从入手。
ReactiveCocoa的出现
ReactiveCocoa是由Github团队开源的一个框架级的项目。这个玩意就是将大家带进了iOS的FRP(Functional Reactive Programming)时代,上面MVC中的M和V的绑定,就可以通过RAC去轻松实现了。当然,数据绑定只是其中一个基本的特性,其中事件流的响应还可以有各种的加工处理。RAC中的流通基本单位就是Signal,通过不断流通Signal来实现了整个响应式的链条。简单总结一下,就是RAC用它这种Signal的模式,把iOS开发常用的KVO、delegate、target等常用模式,都统一起来。
说到这里,是不是开始产生了一种可以拯救世界的错觉??
ReactiveCocoa的缺点
- 和传统的开发认知相差甚远,会让使用者怀疑人生
- 学习曲线陡峭得有点猥琐
- 方法命名魔性不友好
- 调用堆栈层次深不见底,debug难度加大
- 使用时必须时刻注意引用问题
- 据说过于复杂的事件流会影响性能(未遇到过)
- 框架级代码,不是每个人都能接受
ReactiveCocoa基本概念
信号(Signal):也就是流,RAC中所有的信号都是RACStream
的子类。信号有冷热之分,信号也有中继、改变等操作。信号是让订阅者去订阅,然后触发对应事件。
- 冷信号(Cold):就是什么事情也不干的,没有订阅者的时候则是这种状态。
- 热信号(Hot):已经有了订阅者就会激活为热信号,同时持有对应的订阅block,在complete之前都不会释放。
订阅者(Subscriber):负责接收信号执行操作,都必须实现RACSubscriber
协议。订阅者之间的关系原则上是平等的,但实际操作上也是有先后顺序,而且还有副作用的情况需要考虑。
副作用(Side Effect):在每一次订阅的是时候都会触发对应的操作,导致订阅者收到信号的时候,结果会和其他订阅者不同。
RAC基本入门
创建最基本的信号
先看看这个例子:
RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
NSLog(@"信号发出了");
[subscriber sendNext:@"成功"];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
NSLog(@"取消");
}];
}];
看到这个玩意,或许根本没办法明白这玩意到底干了什么操作是吧?好的,下面一步一步来分解:
-
^ RACDisposable * (id<RACSubscriber> subscriber)
-
id<RACSubscriber> subscriber
,这就是所谓的订阅者,我们需要把结果传给它,由它来负责把结果分发到所有的订阅者去。如:[subscriber sendNext:@"成功"]
。 -
RACDisposable *
,返回这类对象是为了在取消订阅的时候可以执行相关的操作。
-
-
[subscriber sendCompleted]
,就是表示信号已经完成了任务,可以安息了。实质就是对应订阅者的block也会释放掉了。
实际上,创建了信号以后,信号是不会干任何事情的。上面那个信号还是一个冷信号(Cold),需要激活才会执行操作。
[signal subscribeNext:^(id x):^{
NSLog(@"%@", x);
}];
只有通过subscribe,或Signal Operation的时候,信号才会真正执行。好的,那现在x
的结果是什么?
数据绑定
RAC最大的特性就是优雅的数据绑定方式。在传统的KVO中API过于猥琐,用的人基本都得写- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
这个奇丑无比的方法(特别是那个context)。当然KVO的功能还是强大的,引用一下文档的描述:
This message is sent to the receiver when the value at the specified key path relative to the given object has changed.
本质也就是通过截取相关setter来实现的,所以类似_name = newName
这种直接针对成员变量赋值的操作就不要妄想可以使用KVO了。有点扯远了,拉回正题,RAC的数据绑定就是基于KVO的。因此,如果某个property不支持KVO的话,RAC也是无能为力的(目前无解决办法,至于如何得知是否支持KVO?你猜?)。
以下也是举个例子:
RAC(self, isEditing) = RACObserve(self.viewModel, isEditing);
好的,这就已经写完数据绑定了。上面是一个单向绑定的例子,目的就是self
的isEditing
的值直接绑定到self.viewModel
的isEditing
。也就是是说self.viewModel
的isEditing
改变时,会直接赋值到self
的isEditing
上去。首次绑定的时候,self
的isEditing
会被改为后者的值。
这里RAC和RACObserve干了些什么暂时不作解释了,接着说。
RACChannelTo(self, blockShowText) = RACChannelTo(self.viewModel, blockShowText);
这就是双向绑定,两边任何一边变化,都会对另一边产生影响。
看完了这种绑定方式,估计大家再也不想用原生的KVO了。
RACCommand
以下也是一个很简单的例子,
看到了按钮被disabled的样式了吧,如果正常情况下要实现,需要做些什么操作?
点击按钮–>禁用按钮–>网络请求–>启用按钮
现在我们只需要这样:
self.loginButton.rac_command = self.loginCommand;
loginCommand
的操作很可能只需要这样:
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [DASLoginServiceModel qryLoginUserName:self.userName
password:self.password];
}];
DASLoginServiceModel
里面的操作这里先不关心,我们优先理解RACCommand是怎么工作。
我们看看源码先:
static void *UIButtonRACCommandKey = &UIButtonRACCommandKey;
static void *UIButtonEnabledDisposableKey = &UIButtonEnabledDisposableKey;
@implementation UIButton (RACCommandSupport)
- (RACCommand *)rac_command {
return objc_getAssociatedObject(self, UIButtonRACCommandKey);
}
- (void)setRac_command:(RACCommand *)command {
objc_setAssociatedObject(self, UIButtonRACCommandKey, command, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// Check for stored signal in order to remove it and add a new one
RACDisposable *disposable = objc_getAssociatedObject(self, UIButtonEnabledDisposableKey);
[disposable dispose];
if (command == nil) return;
disposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self];
objc_setAssociatedObject(self, UIButtonEnabledDisposableKey, disposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self rac_hijackActionAndTargetIfNeeded];
}
- (void)rac_hijackActionAndTargetIfNeeded {
SEL hijackSelector = @selector(rac_commandPerformAction:);
for (NSString *selector in [self actionsForTarget:self forControlEvent:UIControlEventTouchUpInside]) {
if (hijackSelector == NSSelectorFromString(selector)) {
return;
}
}
[self addTarget:self action:hijackSelector forControlEvents:UIControlEventTouchUpInside];
}
- (void)rac_commandPerformAction:(id)sender {
[self.rac_command execute:sender];
}
@end
简要解释一下:
-
disposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self]
这句把button的状态和command的执行状态绑定在一起了。因此,在command执行的时候,button就会被禁用掉。只有command执行完成的时候,button才会恢复启用状态。 -
[self addTarget:self action:hijackSelector forControlEvents:UIControlEventTouchUpInside]
button响应UIControlEventTouchUpInside
事件触发command。 - 至于command什么时候才算完成执行,实现上就跟
^RACSignal *(id input)
返回的Signal有关了,只要Signal还没complete或者error,则还算是执行状态。 - 点击的时候,本质就是会调用
[self.rac_command execute:sender]
,实际上也可以自己手动调用,也可以有类似的效果。
除了结合Button,也有其他实际应用的例子:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ((indexPath.section == (_numberOfSections-1)) &&
(indexPath.row == (_numberOfRowsInLastSection - 1)) &&
!_completed) {
[_loadMoreCommand execute:self];
}
return [super tableView:tableView cellForRowAtIndexPath:indexPath];
}
RACCommand还有一个特性就是不会在当前任务没完成执行的时候,就接受一个新的任务。因此这里可以理解为,如果当前加载更多的请求没完成,则不会再次请求。
常用的几种Operation
map
算是比较简单的operation之一,举例之:
RAC(self.textNumberLabel, text) = [self.contentTextView.rac_textSignal map:^id(NSString *value) {
return [NSString stringWithFormat:@"%d", (int)value.length;
}];
通过map
就可以直接在operation里面进行数值映射,绑定的数据也就可以可以经过自己的一层处理。
takeUntil
指定一个信号在另一个信号发出sendNext
之后,就被complete掉,如下:
@weakify(self);
[[[[NSNotificationCenter defaultCenter]
rac_addObserverForName:UIKeyboardWillHideNotification
object:nil]
takeUntil:self.rac_willDeallocSignal]
subscribeNext:^(NSNotification *notification) {
@strongify(self);
self.scrollView.contentOffset = CGPointMake(0.0, 0.0);
self.bottomGapConstraint.constant = defaultBottomGap;
}];
为什么这里要执行takeUntil??
因为,[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillHideNotification object:nil]
本来是冷信号,订阅以后立即变成热信号。如果不做这一步操作,self
即使释放了,订阅的block也是不会释放的。每次通知发出的时候,还是会再次来到这个block去处理。
flattenMap
之前的map
其实也是通过flattenMap
来实现的,map
只是改变分发的值。而flattenMap
不仅可以获得上一个信号的值,还能激活下一个信号,形成这种链式反应。
RACSignal __block *queueSignal = nil;
for (AskImageModel *model in self.imageModels) {
if (model.uploadUrl.length)
continue;
NSString *imagePath = model.imagePath;
@weakify(model);
if (queueSignal) {
queueSignal = [queueSignal flattenMap:^RACStream *(id value) {
RACSignal *oneSignal = [PicUploadServiceModel qryUploadPic:imagePath];
[oneSignal subscribeNext:^(PicUploadServiceModel *x) {
@strongify(model);
model.uploadUrl = x.imageUrl;
}];
return oneSignal;
}];
} else {
queueSignal = [PicUploadServiceModel qryUploadPic:imagePath];
[queueSignal subscribeNext:^(PicUploadServiceModel *x) {
@strongify(model);
model.uploadUrl = x.imageUrl;
}];
}
}
@weakify(self);
if (queueSignal) {
[queueSignal
subscribeNext:^(id x) {
@strongify(self);
NSArray *imageUrls = [self.imageModels linq_select:^id(AskImageModel *item) {
return item.uploadUrl;
}];
[self submitQuestionWithImageUrls:imageUrls];
}
error:^(NSError *error) {
[SVProgressHUD showErrorWithStatus:@"上传失败,请重新提交"];
}];
} else {
NSArray *imageUrls = [self.imageModels linq_select:^id(AskImageModel *item) {
return item.uploadUrl;
}];
[self submitQuestionWithImageUrls:imageUrls];
}
上面的代码实际是把本地的图片一张张上传,如果其中一张错误则中断。下次上传的时候只上传还没成功上传的图片。通过flattenMap
可以组成一个链条,对于错误处理可以直接在最终的合成好的信号上执行。
combineLatestWith和combineLatest
这个就是组合信号的operation,只有当这些组合的信号都起码发出了sendNext
,才会分发到订阅者。下一次,只要其中一个信号再次sendNext
的时候,也会再次分发。
@weakify(self);
[[RACObserve(self, isEditing)
combineLatestWith:RACObserve(self, templateModel)]
subscribeNext:^(id x) {
@strongify(self);
RACTupleUnpack(NSNumber *flag, DASTemplateModel *tModel) = x;
if (!flag.boolValue) {
self.title = @"模版详情";
} else if (flag.boolValue && tModel) {
self.title = @"编辑模版";
} else {
self.title = @"添加模版";
}
}];
和网络请求的信号组合使用可以达到并发的效果,成功和失败可以进行统一处理。
由于operation的种类很多,我也只能介绍一部分,详细的还是看文档吧。
事后PS
RAC(…)是什么鬼
循例先看看源码:
#define RAC(TARGET, ...) \
metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__)) \
(RAC_(TARGET, __VA_ARGS__, nil)) \
(RAC_(TARGET, __VA_ARGS__))
/// Do not use this directly. Use the RAC macro above.
#define RAC_(TARGET, KEYPATH, NILVALUE) \
[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(TARGET) nilValue:(NILVALUE)][@keypath(TARGET, KEYPATH)]
看到这句metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))
,大家也是可以忽略了。因为里面是多个宏的嵌套,略复杂,我直接解释,metamacro_argcount(__VA_ARGS__)
是用于计算可变参数的个数(编译期),metamacro_if_eq
则是判断值是否相等(编译期)。从而做到可以选择编译成RAC_(TARGET, __VA_ARGS__, nil)
还是RAC_(TARGET, __VA_ARGS__)
。
@weakify和@strongify
我这里不作源码解释了,当遇到会出现循环引用的情况,请主动加上。这类的循环引用编译器无法检测。嵌套多个作用域的时候,也要再次加上@strongify。
赞一个!谢谢大Z
的分享!