ReactiveCocoa探究-编程思想


谈到ReactiveCocoa(RAC),说它比较火似乎也没有那么火,一些大厂用的也并不多,说它不火貌似又经常看到或听到一些鼓吹RAC的文章和言论。尽管它很强大,但是也并没有很普及,即便它很强大!

在了解ReactiveCocoa前,先简单粗略回顾一下block的几种常用用法及几种常见的编程思想。

block常用用法

1. block作为对象的属性

新建一个Single工程,创建一个Person类,在Person类中声明一个block。系统有个内置的block代码块

注意,在非ARC环境下block需要使用copy声明,使用copy的原因,是把block从栈区拷贝到堆区,因为栈区中的变量出了作用域之后就会被销毁,无法在全局使用,所以应该把栈区的属性拷贝到堆区中全局共享,这样就不会被销毁了,在MRC手动管理的就是堆区,不需要系统管理,MRC环境必须使用copy把变量拷贝到全局的堆区。而如果在ARC环境下就可以不使用copy修饰,因为ARC下的属性本来就在堆区。所以现在在默认是ARC环境下依然沿袭了之前的使用方式copy,使用strong也没有问题。

在Person类中声明一个block

@property (nonatomic, strong) void(^myBlock)(void);

在控制器中持有一个Person对象,并声明一个block接受Person的这个block属性

self.p = [[Person alloc] init];

void(^testBlock)(void) = ^() {

 NSLog(@"block as params");

    };

self.p.myBlock = testBlock

在点击屏幕的时候实现这个block

self.p.myBlock();

2.block作为方法的参数

同样在Person类中声明一个带有block参数的方法并实现

- (void)eat:(void(^)(NSString *someThing))block;

- (void)eat:(void(^)(NSString * someThing))block {

block(@"delicious");

}

在控制器中调用,其中block中的已知数据类型的参数是Person对象提供给外界使用的,AFNetworing中返回的block结果就是如此。

[self.p eat:^(NSString *someThing) {
       NSLog(@"eat %@",someThing);
   }];

3.作为方法的返回值

block作为方法的返回值这种用法开发中其实并不常用,但是一些第三方,最典型的常用约束库
Masonry库就将block作为方法的返回值来使用。

同样在Person中声明一个run方法并实现,将block作为方法的返回值

- (void(^)(int m))run;

- (void(^)(int m))run {
    return ^(int m) {
        NSLog(@"run %d",m);
    };
}

在控制器找那个可以这样调用

- (void)methodTree {
    void(^block)(int m) =  self.p.run;
    block(100);
}

并没需要一个block先接收再实现,可以直接调用

[self.p run](100);

有没有觉得这种调用怪怪的,其实就是调用方法返回一个block再调用 这个block,block的调用方式就是block()

有没有更简单的写法呢?答案是有!

self.p.run(100);

为什么可以使用点语法调用?

在OC中只有形如get类型的方法才可以使用点语法点出来,否则是能使用形如[self run]这种加空格的调用方法。而这种将block作为返回值的方法语法类型和get方法相同,所以可以使用点语法调用,但是如果将block作为方法的返回值,而且这个方法又有其他参数,是无法通过点语法调用的,因为系统判断这不是get方法,get方法是没有参数的。

Demo地址

编程思想

了解几种常规的编程思想,如链式编程,响应式编程,函数式编程等。

1. 链式编程思想

链式编程就是可以通过”点”语法,将需要执行的代码块连续的书写下去,使得代码简单易读,书写方便。典型的使用链式编程思想的有Masonry库,通过分析Mansory库的使用机制来了解一下链式编程。

同样新建一个Single工程,添加一个View

UIView *redView = [[UIView alloc] init];

redView.backgroundColor = [UIColor redColor];

[self.view addSubview:redView];

[redView mas_makeConstraints:^(MASConstraintMaker *make) {

    make.left.top.equalTo(@100);

    make.right.bottom.equalTo(@-100);

}];

上面的MASConstraintMaker,我们称之为约束制造者,这里面就将block作为方法的参数来使用了,为什么对redView添加约束,而block里面使用的确是约束制造者呢?

点进mas_makeConstraints这个方法看一看

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

首先这个方法关掉了老式的自动布局self.translatesAutoresizingMaskIntoConstraints = NO;,防止和Autolayout冲突,然后创建一个约束制造者并返回。而在创建这个约束制造者的时候将self作为参数传入,[[MASConstraintMaker alloc] initWithView:self];,而这个self就是redView,即这个方法的调用者。到这个方法里面看一下

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;

    self.view = view;
    self.constraints = NSMutableArray.new;

    return self;
}

可见这个约束制造者将view作为属性绑定起来了,到[constraintMaker install];这个方法里面
可以看到就是我们不愿意写的各种
[NSLayoutConstraint constraintWithItem: attribute: relatedBy: toItem: attribute: multiplier: constant:]

通过上面流程我们知道mas_makeConstraints的执行流程

1.创建约束制造者MASConstraintMaker,并且绑定控件,生成一个保存所有约束的数组

2.执行mas_makeConstraints传入的Block

3.让约束制造者安装约束!

3.1 清空之前的所有约束

3.2 遍历约束数组,一个一个安装

接下来看一下block中的代码make.left.top.equalTo(@100);,这里使用的即是链式编程,我们发现make.left后依然可以.top,.right,为什么可以这样写?点进去可以看到

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

这是一个get方法,上文有提到OC中只有形如get方法才可以使用点语法点出来。而这个get方法返回的是一个MASConstraint,你会发现MASConstraint类中这些全是get方法,而返回值全是调用者本身MASConstraint

随便点进去一个get方法

- (MASConstraint *)addConstraintWithLayoutAttribute:    (NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be     chained before defining the constraint relation");

    return [self.delegate constraint:self     addConstraintWithLayoutAttribute:layoutAttribute];
}

可以发现都是通过代理来实现,最终返回的是一个约束MASConstraint,并添加到约束数组中,这也就可以解释了为什么make.left后依然可以.top,.right继续点出来,事实上只要你愿意,你可以一直这么写下去,因为返回值都是这些方法的调用者。而这也是链式编程最重要的思想特点,即:

方法的返回值必须要有方法调用者

为什么是 “要有方法调用者”而不是“要是方法的调用者”呢?

我们接着看make.left.top.equalTo(@100)中的equalTo方法

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute,     NSLayoutRelationEqual);
    };
}

可见,这个equalTo方法返回的不是方法调用者,而是一个返回值为方法调用者的block。而我们在调用equalTo方法的时候紧跟着后面加了“(100)”,也就是说调用了这个返回的block,bclok执行后返回的又是一个方法调用者MASConstraint。也就是说,只要你愿意,在make.left.top.equalTo(@100)之后,你依然可以继续调用get方法,形如

make.left.top.equalTo(@100).right.bottom.equalTo(@-100);

只是在equalTo之后继续调用get方法,后面的约束值会覆盖掉前面的约束值。所以,理论上可以,然而并不应该这么做。

那如果我们自己也想玩一个具备链式编程特点的类该怎么写呢?记住,链式编程,方法返回值必须要有方法调用者。然后可以参考Masonary库粗略玩一下。
  1. 先创建一个计算管理器吧CalculateManager,定义简单的加减乘除方法并逐一趋实现,并声明一个属性去保存计算结果, 这些方法都有一个共同的特点,就是返回值是一个block,而block的返回值是这个类本身,这就确保了在调用方法后依然可以继续调用。
@interface CalculateManager : NSObject

@property(assign,nonatomic)double result;

+ (instancetype)sharedInstance;

-(CalculateManager *(^)(float value))add;

-(CalculateManager *(^)(float value))minus;

-(CalculateManager *(^)(float value))multi;

-(CalculateManager *(^)(float value))divis;

@end

@implementation CalculateManager

    + (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static CalculateManager *mgr = nil;
    dispatch_once(&onceToken, ^{
        mgr = [[self alloc] init];
    });
    return mgr;
}

-(CalculateManager * (^)(float value))add{
    return ^(float value){
        _result += value;
        return self;
    };
}

-(CalculateManager *(^)(float value))minus {
    return ^(float value){
        _result -= value;
        return self;
    };
}

-(CalculateManager *(^)(float value))multi {
    return ^(float value){
        _result *= value;
        return self;
    };
}

-(CalculateManager *(^)(float value))divis {
    return ^(float value){
        _result /= value;
        return self;
    };
}

2.接下来你会发现已经可以使用链式编程了

CalculateManager *manger = [[CalculateManager alloc] init];
double result = manger.add(3).minus(3).result;

只要你愿意,依然可以在返回结果前继续点下去。但是你会发现在返回结果后没法继续调用方法了。而且这种调用依然需要创建实例,和Masonry形式依然不同,那就继续改进。在使用Masonry时你会发现任何继承自UIView的控件都可以调用mas_makeConstraints或其他方法,那说明这绝壁是个UIView的分类。仿照此思路,我们创建一个NSObject的分类,使得NSObject任何子类实例都可以调用。

 @interface NSObject (Calculate)

- (double)fn_calculate:(void(^)(CalculateManager * mgr))block;

@end

@implementation NSObject (Calculate)

- (double)fn_calculate:(void(^)(CalculateManager * mgr))block {
    CalculateManager * mgr = [CalculateManager sharedInstance];
    block(mgr);
    return mgr.result;
}

上面这个方法的参数是参数为CalculateManager的block,返回结果为CalculateManager的属性值。现在就可以这么使用了:

double result = [self fn_calculate:^(CalculateManager *mgr) {
        mgr.add(10).add(10).divis(3).multi(5.8).minus(2.2);
    }];

@end

只要你愿意,你可以一直点下去。这种写法其实在OC中非常少见,但是像Masonry一些第三方库都使用了这种思想。

Demo下载

函数编程思想(FP)

函数编程是把操作尽量写成一系列嵌套的函数或者方法调用。每个方法必须要有返回值,把函数或者block当作参数。

继续上面的例子,在CalculateManager中声明并实现一个方法

- (instancetype)manager:(double(^)(double))block;


- (instancetype)manager:(double(^)(double))block {
    _result = block(_result);
        return self;
}

这个方法参数是一个block,这个block又有返回值和参数,方法的返回值又是自己的一个实例。这里方法的返回值完全可以不是自己的实例,而是一个具体的结果。但是如果是一个具体的结果,如果这个类中有很多其他的属性,那对应的就相应有很多雷同的方法,所以返回一个实例,调用的时候可以通过这个实例去获得对应的属性值。

CalculateManager * mgr = [[CalculateManager alloc] init];
    double h_Result = [[mgr manager:^double(double parameter) {
       parameter += 10;
          parameter *= 2;
          return parameter;
   }] result];
   NSLog(@"%f",h_Result);

响应式编程思想(RP)

响应式编程不需要考虑调用顺序,只需要考虑结果。 iOS中最具代表性的就是KVO的使用。看一下简单实用。

先创建一个Person类,声明一个name属性,监听Personname属性

你会发现没什么毛病。如果这时候,我们把name属性变成一个成员变量,再去使用箭头函数方式去修改name的值

这个时候你会发现并没有走进监听函数。这和上面正常使用的时候有什么区别呢?那就是修改name属性的时候,成员变量不会调用set方法, 而使用点语法总是会调用set函数。有理由相信,使用KVO的时候重写了属性的set方法。

怎么可以重写set方法?

  1. 可以使用分类的形式重写
  2. 可以使用子类去重写

想想看,苹果有没有可能去使用分类的形式去重写? 绝壁不可能!因为如果分类中去重写属性的set方法,则原有类中即便重写了该属性的set方法也不会调用,会优先使用分类中的方法。而我们在日常开发中,经常会去重写set方法去做一些其他操作。所以只可能是第二种情况,使用子类去重写了set方法。那过程应该是怎样的呢?

  1. 自定义被监听类的子类
  2. 重写被监听属性的set方法,在内部调用super恢复父类的做法,通知观察者

还有一个问题? 如何让外界调用自定义子类的方法?所以还有最重要的一点

iOS中有个isa指针, 修改当前对象的isa指针,指向自定义的子类!

验证一下猜测,查看一下在走进监听方法后对象的isa指针指向。

知道了KVO的内部机制,那我们自己就可以写一个简单的KVO

以上面为例,监听Person类的name属性,创建一个NSObject的分类,添加监听方法并实现,最后再使用添加的这个方法监听即可。

注意在使用objc_msgSend时需要在Build Setting中将Enable Strict Checking of objc_msgSend Calls设置为NO

Demo下载


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

本站总访问量 本文总阅读量