臧成威的博客

不要让惯性影响你的未来

如何利用Objective-C写一个精美的DSL

背景

在程序开发中,我们总是希望能够更加简洁、更加语义化地去表达自己的逻辑,链式调用是一种常见的处理方式。我们常用的 Masonry、 Expecta 等第三方库就采用了这种处理方式。

1
2
3
4
5
6
7
// Masonry
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top);
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];
1
2
3
4
5
// Expecta
expect(@"foo").to.equal(@"foo"); // `to` is a syntactic sugar and can be safely omitted.
expect(foo).notTo.equal(1);
expect([bar isBar]).to.equal(YES);
expect(baz).to.equal(3.14159);

像这种用于特定领域的表达方式,我们叫做 DSL (Domain Specific Language),本文就介绍一下如何实现一个链式调用的 DSL.

链式调用的实现

我们举一个具体的例子,比如我们用链式表达式来创建一个 UIView,设置其 frame、backgroundColor, 并添加至某个父 View。 对于最基本的 Objective-C (在 iOS4 block 出现之前),如果要实现链式调用,只能是这个样子的:

1
UIView *aView = [[[[UIView alloc] initWithFrame:aFrame] bgColor:aColor] intoView:aSuperView];

有了 block,我们可以把中括号的这种写法改为点语法的形式

1
2
3
4
UIView *aView = AllocA(UIView).with.position(x, y).size(width, height).bgColor(aColor).intoView(aSuperView);

// 当x和y为默认值0和0或者width和height为默认值0的时候,还可以省略
UIView *bView = AllocA(UIView).with.size(width, height).bgColor(aColor).intoView(aSuperView);

可以看出,链式语法的语义性很明确,后者的语法更加紧凑,下面我们从两个角度看一下后者的实现。

1. 从语法层面来看

链式调用可以用两种方式来实现:

  1. 在返回值中使用属性来保存方法中的信息

    比如,Masonry 中的 .left .right .top .bottom 等方法,调用时会返回一个 MASConstraintMaker 类的实例,里面有 left/right/top/bottom 等属性来保存每次调用时的信息;

    make.left.equalTo(superview.mas_left).with.offset(15);
    

    再比如,Expecta 中的方法 .notTo 方法会返回一个 EXPExpect 类的实例,里面有个 BOOL 属性 self.negative 来记录是否调用了 .notTo

    expect(foo).notTo.equal(1);
    

    再比如,上例中的 .with 方法,我们可以直接 return self;

  2. 使用 block 类型的属性来接受参数

    比如 Masonry 中的 .offset(15) 方法,接收一个 CGFloat 作为参数,可以在 MASConstraintMaker 类中添加一个 block 类型的属性:

    @property (nonatomic, copy) MASConstraintMaker* (^offset)(CGFloat);
    

    比如例子中的 .position(x, y),可以给的某类中添加一个属性:

    @property (nonatomic, copy) ViewMaker* (^position)(CGFloat x, CGFloat y);
    

    在调用 .position(x, y) 方法时,执行这个block,返回 ViewMaker 的实例保证链式调用得以进行。

2. 从语义层面来看

从语义层面上,需要界定哪些是助词,哪些是需要接受参数的。为了保证链式调用能够完成,需要考虑传入什么,返回什么。

还是以上面的例子来讲:

1
UIView *aView = AllocA(UIView).with.position(x, y).size(width, height).bgColor(aColor).intoView(aSuperView);

分步来看一下,这个 DSL 表达式需要描述的是一个祈使句,以 Alloc 开始,以 intoView 截止。在 intoView 终结语之前,我们对 UIView 进行一定的修饰,利用 position size bgColor 这些。

下面我们分别从四段来看,如何实现这样一个表达式:

(1) 宾语

在 AllocA(UIView) 的语义中,我们确定了宾语是 a UIVIew。由于确定 UIView 是在 intoView 截止那时,所以我们需要创建一个中间类来保存所有的中间条件,这里我们用 ViewMaker 类。

1
2
3
4
5
6
@interface ViewMaker : NSObject
@property (nonatomic, strong) Class viewClass;
@property (nonatomic, assign) CGPoint position;
@property (nonatomic, assign) CGPoint size;
@property (nonatomic, strong) UIColor *color;
@end

另外我们可以注意到AllocA是一个函数,而UIView无法直接传递到这个函数中,语法就要变成 AllocA([UIView class]) 而失去了简洁性。所以我们需要先定义一个宏来“吞”掉中括号和 class 这个方法:

1
2
3
4
5
6
7
#define AllocA(aClass)  alloc_a([aClass class])

ViewMaker* alloc_a(Class aClass){
    ViewMaker *maker = ViewMaker.new;
    maker.viewClass = aClass;
    return maker;
}

(2) 助词

很多时候,为了让 DSL 的语法看起来更加连贯,我们需要一些助词来帮助,例如 Masonry 里面的 make.top.equalTo(superview.mas_top).with.offset(padding.top) 这句中的 with 就是这样一个助词。

而这个助词和我们学过的语法一样,通常没有什么实际效果,简单返回self就可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface ViewMaker : NSObject
@property (nonatomic, strong) Class viewClass;
@property (nonatomic, assign) CGPoint position;
@property (nonatomic, assign) CGPoint size;
@property (nonatomic, strong) UIColor *color;
@property (nonatomic, readonly) ViewMaker *with;
@end

@implementation ViewMaker

- (ViewMaker *)with
{
    return self;
}
@end

需要注意的是,返回自己,就没有办法阻止用户不断调用自己 .with.with.with ,为了避免这种情况,可以新生成一个类,每个类都拥有自己所在层次的方法,避免跃层调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@interface ViewMaker : NSObject
@property (nonatomic, strong) Class viewClass;
@property (nonatomic, assign) CGPoint position;
@property (nonatomic, assign) CGPoint size;
@property (nonatomic, strong) UIColor *color;
@end

@interface ViewClassHelper : NSObject
@property (nonatomic, strong) Class viewClass;
@property (nonatomic, readonly) ViewMaker *with;
@end

#define AllocA(aClass)  alloc_a([aClass class])

ViewClassHelper* alloc_a(Class aClass){
    ViewClassHelper *helper = ViewClassHelper.new;
    helper.viewClass = aClass;
    return helper;
}
@implementation ViewClassHelper

- (ViewMaker *)with
{
    ViewMaker *maker = ViewMaker.new;
    maker.viewClass = self.viewClass;
    return maker;
}
@end

这样就有效防止了,.with.with.with这样的语法。但是实际上,我们要根据真实的需要来进行开发,使用 DSL 的用户是为了更好的表达性,所以并不会写出.with.with.with这样的代码,这样的防护性措施就显得有点不必要了。

不过使用类来区分助词还有另外几个小好处,就是它可以确保在语法提示的时候,ViewClassHelper这个类只有.with这样一个语法提示,而ViewMaker不出现.with语法提示;并且同时确保.with一定要出现。

不过为了简化文章,我们都使用前者,既.with返回self来继续下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface ViewMaker : NSObject
@property (nonatomic, strong) Class viewClass;
@property (nonatomic, assign) CGPoint position;
@property (nonatomic, assign) CGPoint size;
@property (nonatomic, strong) UIColor *color;
@property (nonatomic, readonly) ViewMaker *with;
@end

@implementation ViewMaker

- (ViewMaker *)with
{
    return self;
}
@end

(3) 修饰部分——定语

像例子中的position size bgColor这些都是定语部分,用来修饰UIView,他们以属性的形势存在于ViewMaker的实例中,为了支持链式表达,所以实现的时候,都会继续返回self

我们来试着实现下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@interface ViewMaker : NSObject
// ...
@property (nonatomic, copy) ViewMaker* (^position)(CGFloat x, CGFloat y);
@property (nonatomic, copy) ViewMaker* (^size)(CGFloat x, CGFloat y);
@property (nonatomic, copy) ViewMaker* (^bgColor)(UIColor *color);
@end

@implementation ViewMaker

- (instancetype)init
{
    if (self = [super init]) {
        @weakify(self)
        _position = ^ViewMaker *(CGFloat x, CGFloat y) {
            @strongify(self)
            self.position = CGPointMake(x, y);
            return self;
        };
        _size = ^ViewMaker *(CGFloat x, CGFloat y) {
            @strongify(self)
            self.size = CGPointMake(x, y);
            return self;
        };
        _bgColor = ^ViewMaker *(UIColor *color) {
            @strongify(self)
            self.color = color;
            return self;
        };
    }
    return self;
}
@end

(4) 终结词

“终结词”这个实在是在现代语法里面找不到对应关系了,但是在 DSL 中,这一段尤为重要。ViewMaker的实例从头至尾收集了很多的修饰,需要最后的一个表达词语来产生最后的结果,这里就称为”终结词”。例如在 Expecta 这个开源库里面的 equal 就是把真正的行为表现出来的时候,tonotTo 都不会真正触发行为。

在我们的例子里,终结词.intoView(aSuperViwe)可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@interface ViewMaker : NSObject
// ...
@property (nonatomic, copy) UIView* (^intoView)(UIView *superView);
@end

@implementation ViewMaker

- (instancetype)init
{
    if (self = [super init]) {
        @weakify(self)
        // ...
        _intoView = ^UIView *(UIView *superView) {
            @strongify(self)
            CGRect rect = CGRectMake(self.position.x, self.position.y,
                         self.size.width, self.size.height);
            UIView *view = [[UIView alloc] initWithFrame:rect];
            view.backgroundColor = self.color;
            [superView addSubView:view];
            return view;
        };
    }
    return self;
}
@end

这样,一个终结词就写好了。

最终代码的汇总:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@interface ViewMaker : NSObject
@property (nonatomic, strong) Class viewClass;
@property (nonatomic, assign) CGPoint position;
@property (nonatomic, assign) CGPoint size;
@property (nonatomic, strong) UIColor *color;
@property (nonatomic, readonly) ViewMaker *with;
@property (nonatomic, copy) ViewMaker* (^position)(CGFloat x, CGFloat y);
@property (nonatomic, copy) ViewMaker* (^size)(CGFloat x, CGFloat y);
@property (nonatomic, copy) ViewMaker* (^bgColor)(UIColor *color);
@property (nonatomic, copy) UIView* (^intoView)(UIView *superView);
@end

#define AllocA(aClass)  alloc_a([aClass class])

ViewMaker* alloc_a(Class aClass){
    ViewMaker *maker = ViewMaker.new;
    maker.viewClass = aClass;
    return maker;
}

@implementation ViewMaker

- (instancetype)init
{
    if (self = [super init]) {
        @weakify(self)
        _position = ^ViewMaker *(CGFloat x, CGFloat y) {
            @strongify(self)
            self.position = CGPointMake(x, y);
            return self;
        };
        _size = ^ViewMaker *(CGFloat x, CGFloat y) {
            @strongify(self)
            self.size = CGPointMake(x, y);
            return self;
        };
        _bgColor = ^ViewMaker *(UIColor *color) {
            @strongify(self)
            self.color = color;
            return self;
        };
        _intoView = ^UIView *(UIView *superView) {
            @strongify(self)
            CGRect rect = CGRectMake(self.position.x, self.position.y,
                         self.size.width, self.size.height);
            UIView *view = [[UIView alloc] initWithFrame:rect];
            view.backgroundColor = self.color;
            [superView addSubView:view];
            return view;
        };
    }
    return self;
}

- (ViewMaker *)with
{
    return self;
}
@end

总结

这种链式调用能够使程序更加清晰,在特定场景下使程序的可读性更强。这种手段在Swift也是相同道理,大家可以善加利用,让自己的代码更加美观。

把玩高阶函数

如果你开始接触函数式编程,你一定听说过高阶函数。在维基百科它的中文解释是这样的:

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入

  • 输出一个函数

看起它就是ObjC语言中入参或者返回值为block的block或者函数,在Swift语言中即为入参或者返回值为函数的函数。那它们在实际的开发过程中究竟起着什么样的作用呢?我们将从入参、返回值和综合使用三部分来看这个问题:

函数作为入参

函数作为入参似乎无论在ObjC时代还是Swift时代都是司空见惯的事情,例如AFNetworking就用两个入参block分别回调成功与失败。Swift中更是加了一个尾闭包的语法(最后一个参数为函数,可以不写括号或者写到括号外面直接跟随方法名),例如下面这样:

1
2
3
[1, 2, 3].forEach { item in
    print(item)
}

我们可以将入参为函数的函数分为两类,escaping函数入参和noescape函数入参,区别在于这个入参的函数是在执行过程内被调用还是在执行过程外被调用。执行过程外被调用的一般用于callback用途,例如:

1
2
3
4
5
6
7
8
9
10
Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.request)  // original URL request  
    print(response.response) // HTTP URL response  
    print(response.data)     // server data  
    print(response.result)   // result of response serialization  

    if let JSON = response.result.value {
        print("JSON: \(JSON)")
    }
}

这个response的入参函数就作为网络请求回来的一个callback,并不会在执行responseJSON这个函数的时候被调用。另外我们来观察forEach的代码,可以推断入参的函数一定会在forEach执行过程中使用,执行完就没有利用意义,这类就是noescape函数。

callback的用法大家应该比较熟悉了,介绍给大家noescape入参的一些用法:

1. 自由构造器

看过GoF设计模式的同学不知道是否还记得构造器模式,Android中的构造器模式类似如下:

1
2
3
4
5
new AlertDialog.Builder(this)
  .setIcon(R.drawable.find_daycycle_icon)
  .setTitle("提醒")
  .create()
  .show();

如果你想要做成这样的代码,你需要将setIconsetTitlecreate等方法都实现成返回this才行。这样就无法直接利用无返回值的setter了。 为什么需要这样的方式呢?如果你同时有如下需求:

  1. 构造一个对象需要很多的参数
  2. 这些参数里面很多有默认值
  3. 这些参数对应的属性未来不希望被修改

那么用这样的模式就可以直观又精巧的展示构建过程。

如果使用noescape入参函数还可以更简单的构造出这种代码,只需要传入一个入参为builder的对象就可以了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 实现在这里  
class SomeBuilder {
    var prop1: Int
    var prop2: Bool
    var prop3: String
    init() {
        // default value  
        prop1 = 0
        prop2 = true
        prop3 = "some string"
    }
}

class SomeObj {
    private var prop1: Int
    private var prop2: Bool
    private var prop3: String
    init(_ builderBlock:(SomeBuilder) -> Void) {
        let someBuilder = SomeBuilder()
        builderBlock(someBuilder) // noescape 入参的使用  
        prop1 = someBuilder.prop1
        prop2 = someBuilder.prop2
        prop3 = someBuilder.prop3
    }
}

// 使用的时候  
let someOjb = SomeObj { builder in
    builder.prop1 = 15
    builder.prop2 = false
    builder.prop3 = "haha"
}

2. 自动配对操作

很多时候,我们开发过程中都会遇到必须配对才能正常工作的API,例如打开文件和关闭文件、进入edit模式退出edit模式等。虽然swift语言给我们defer这样的语法糖避免大家忘记配对操作,但是代码看起来还是不那么顺眼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func updateTableView1() {
    self.tableView.beginUpdates()

    self.tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .fade)
    self.tableView.deleteRows(at: [IndexPath(row: 5, section: 0)], with: .fade)

  self.tableView.endUpdates() // 容易漏掉或者上面出现异常  
  
}

func updateTableView2() {
    self.tableView.beginUpdates()
    defer {
        self.tableView.endUpdates()
    }

    self.tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .fade)
    self.tableView.deleteRows(at: [IndexPath(row: 5, section: 0)], with: .fade)
}

利用noescape入参,我们可以将要操作的过程封装起来,使得上层看起来更规整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 扩展一下UITableView  
extension UITableView {
    func updateCells(updateBlock: (UITableView) -> Void) {
        beginUpdates()
        defer {
            endUpdates()
        }
        updateBlock(self)
    }
}

func updateTableView() {
  // 使用的时候  
    self.tableView.updateCells { (tableView) in
        tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .fade)
        tableView.deleteRows(at: [IndexPath(row: 5, section: 0)], with: .fade)
    }
}

函数作为入参就简单介绍到这里,下面看看函数作为返回值。

函数作为返回值

在大家的日常开发中,函数作为返回值的情况想必是少之又少。不过,如果能简单利用起来,就会让代码一下子清爽很多。

首先没有争议的就是我们有很多的API都是需要函数作为入参的,无论是上一节提到过的escaping入参还是noescape入参。所以很多的时候,大家写的代码重复率会很高,例如:

1
2
3
4
5
6
let array = [1, 3, 55, 47, 92, 77, 801]

let array1 = array.filter { $0 > 3 * 3}
let array2 = array.filter { $0 > 4 * 4}
let array3 = array.filter { $0 > 2 * 2}
let array4 = array.filter { $0 > 5 * 5}

一段从数组中找到大于某个数平方的代码,如果不封装,看起来应该是这样的。为了简化,通常会封装成如下的两个样子:

1
2
3
4
5
6
7
8
func biggerThanPowWith(array: [Int], value: Int) -> [Int] {
    return array.filter { $0 > value * value}
}

let array1 = biggerThanPowWith(array: array, value: 3)
let array2 = biggerThanPowWith(array: array, value: 4)
let array3 = biggerThanPowWith(array: array, value: 2)
let array4 = biggerThanPowWith(array: array, value: 5)

如果用高阶函数的返回值函数,可以做成这样一个高阶函数:

1
2
3
4
5
6
7
8
9
// 一个返回(Int)->Bool的函数  
func biggerThanPow2With(value: Int) -> (Int) -> Bool {
    return { $0 > value * value }
}

let array1 = array.filter(biggerThanPow2With(value: 3))
let array2 = array.filter(biggerThanPow2With(value: 4))
let array3 = array.filter(biggerThanPow2With(value: 2))
let array4 = array.filter(biggerThanPow2With(value: 5))

你一定会说,两者看起来没啥区别。所以这里面需要讲一下使用高阶返回函数的几点好处

1. 不需要wrapper函数也不需要打开原始类

如同上面的简单封装,其实就是一个wrapper函数,把array作为入参带入进来。这样写代码和看代码的时候就稍微不爽一点,毕竟大家都喜欢OOP嘛。如果要OOP,那就势必要对原始类进行扩展,一种方式是加extension,或者直接给类加一个新的方法。

2. 阅读代码的时候一目了然

使用简单封装的时候,看代码的人并不知道内部使用了filter这个函数,必须要查看源码才能知道。但是用高阶函数的时候,一下子就知道了使用了系统库的filter

3. 更容易复用

这也是最关键的一点,更细粒度的高阶函数,可以更方便的复用,例如我们知道Set也是有filter这个方法的,复用起来就这样:

1
2
3
let set = Set<Int>(arrayLiteral: 1, 3, 7, 9, 17, 55, 47, 92, 77, 801)
let set1 = set.filter(biggerThanPow2With(value: 3))
let set2 = set.filter(biggerThanPow2With(value: 9)) 

回忆下上面的简单封装,是不是就无法重用了呢?

类似的返回函数的高阶函数还可以有很多例子,例如上面说过的builder,假如每次都需要定制成特殊的样子,但是某个字段不同,就可以用高阶函数很容易打造出来:

1
2
3
4
5
6
7
8
9
10
11
func builerWithDifferentProp3(prop3: String) -> (SomeBuilder) -> Void {
    return { builder in
        builder.prop1 = 15
        builder.prop2 = true
        builder.prop3 = prop3
    }
}

let someObj1 = SomeObj.init(builerWithDifferentProp3(prop3: "a"))
let someObj2 = SomeObj.init(builerWithDifferentProp3(prop3: "b"))
let someObj3 = SomeObj.init(builerWithDifferentProp3(prop3: "c"))

介绍完入参与返回值,还有另外的一个组合模式,那就是入参是一个函数,返回值也是一个函数的情况,我们来看看这种情况。

入参函数 && 返回值函数

这样的一个函数看起来会很恐怖,swift会声明成func someFunc<A, B, C, D>(_ a: (A) -> B)-> (C) -> D,objective-c会声明成- (id (^)(id))someFunc:(id (^)(id))block。让我们先从一个小的例子来讲起,回忆一下我们刚刚做的biggerThanPow2With这个函数,如果我们要一个notBiggerThanPow2With怎么办呢?你知道我一定不会说再写一个。所以我告诉你我会这样写:

1
2
3
4
5
6
7
func not<T>(_ origin_func: @escaping (T) -> Bool) -> (T) -> Bool {
    return { !origin_func($0) }
}

let array5 = array.filter(not(biggerThanPow2With(value: 9)))


并不需要一个notBiggerThanPow2With函数,我们只需要实现一个not就可以了。它的入参是一个(T) -> Bool,返回值也是(T) -> Bool,只需要在执行block内部的时候用个取反就可以了。这样不单可以解决刚才的问题,还可以解决任何(T) -> Bool类型函数的取反问题,比如我们有一个odd(_: int)方法来过滤奇数,那我们就可以用even=not(odd)得到一个过滤偶数的函数了。

1
2
3
4
5
6
7
8
9
func odd(_ value: Int) -> Bool {
    return value % 2 == 1
}

let array6 = array.filter(odd)
let array7 = array.filter(not(odd))

let even = not(odd)
let array8 = array.filter(even)

大家可以看下上面的biggerThanPow2With时我们讨论过的,如果biggerThanPow2With不是一个返回函数的高阶函数,那它就不太容易用not函数来加工了。

综上,如果一个入参和返回值都是函数的函数就是这样的一个转换函数,它能够让我们用更少的代码组合出更多的函数。另外需要注意一下,如果返回的函数里面闭包了入参的函数,那么入参函数就是escaping入参了。

下面再展示给大家两个函数,一个交换参数的函数exchangeParam,另一个是柯里化函数currying

1
2
3
4
5
6
7
func exchangeParam<A, B, C>(_ block: @escaping (A, B) -> C) -> (B, A) -> C {
    return { block($1, $0) }
}

func currying<A, B, C>(_ block: @escaping (A, B) -> C, _ value: A) -> (B) -> C {
    return { block(value, $0) }
}

第一个函数exchangeParam是交换一个函数的两个参数,第二个函数currying是给一个带两个参数的函数和一个参数,返回一个带一个参数的函数。那这两个函数究竟有什么用途呢?看一下下面的例子:

1
let array9 = array.filter(currying(exchangeParam(>), 9))

swift语言里面>是一个入参(a, b)的函数,所以>(5, 3) == true。我们使用exchangeParam交换参数就变成了(b, a),这时exchangeParam(>)(5, 3)就等于false了。

而currying函数又把参数b固定为一个常量9,所以currying(exchangeParam(>), 9)就是大于9的函数意思。

这个例子里就利用了全部的预制函数和通用函数,没有借助任何的命令与业务函数声明实现了一个从数组中过滤大于9的子数组的需求。试想一下,如果我们更多的使用这样的高阶函数,代码中是不是很多的逻辑可以更容易的互相组合,而这就是函数式编程的魅力。

总结

高阶函数的引入,无论是从函数式编程还是从非函数式编程都带给我们代码一定程度的简化,使得我们的API更加简易可用,复用更充分。然而本文的例子不过是冰山一角,更多的内容还需要大家的不断尝试和创新,也可以通过学习更多的函数式编程范式来加深理解。

聊一聊iOS开发中的惰性计算

首先给大家讲一个笑话:

有一只小白兔,跑到蔬菜店里问老板:“老板,有100个胡萝卜吗?”。老板说:“没有那么多啊。”,小白兔失望的说道:“哎,连100个胡萝卜都没有。。。”。
第二天小白兔又来到蔬菜店问老板:“今天有100个胡萝卜了吧?”,老板尴尬的说:“今天还是缺点,明天就能好了。”,小白兔又很失望的走了。
第三天小白兔刚一推门,老板就高兴的说道:“有了有了,从前天就进货的100个胡萝卜到货了。”,小白兔说:“太好了,我要买2根!”。。。

不晓得笑话是否博您一笑,但是这里面确有一个点是和我们的主题惰性计算相关的。试想一下,假设蔬菜店是一个电商,你是老板,你挂商品数量的时候,是100个,1000个,还是真实的备货2个?显然做过淘宝的同学都知道这其中的玄机,就是先挂大的余量,有卖出再补货。所以,如果这个老板先回答有100个胡萝卜,再等它要2个的时候把自己备货的2个拿给它,是不是就免去了100个胡萝卜的物流?

在程序开发中,我们也会经常的遇到这样的问题,明明创建了很大的一个对象,但是其实只用了一个字段;明明创建了一个500个的数组,其实只用了第0个和第1个元素。遇到这类问题,我们可以尝试使用惰性计算来解决。

关于惰性计算,或者惰性求值。想必大家第一反应就是在getter里动态返回属性了。例如有一个很大的属性,你希望在有人调用的时候才创建,就可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
- (id)someBigProperty
{
    if (_someBigProperty == nil) {
        NSMutableArray *someBigProperty = [NSMutableArray array];
        for (int i = 0; i < 100000; ++i) {
            [someBigProperty addObject:@(i)];
        }
        _someBigProperty = [someBigProperty copy];
    }

    return _someBigProperty;
}

本文当然不拘泥于大家耳熟能详的知识点进行阐述了。上述的代码虽然也能勉强叫惰性求值,但并非足够理想。为什么说是“勉强叫”呢?大家想想上面的笑话,其实这样做和老板的做法并无差别。首先店里没有100个胡萝卜,就好像这个对象没有_someBigProperty属性一样。一旦有人需要100个“胡萝卜”,就循环100000次创建这个_someBigProperty属性。然后可能使用者只需要第0个。

另外在实际项目中这样的一个手段几乎被大家严重的乱用了,为什么说是乱用呢?除了创建非常大的属性、或者创建对象的时候有一些必要的副作用不能提前创建之外,几乎不应该使用惰性求值来处理类似逻辑。原因如下:

  • 如果真的是很大的属性,一般它比较重要,很可能会被访问,要不要在getter中写出来意义不大。
  • @property的atomic、nonatomic、copy、strong等描述在有getter方法的属性上会失效,后人修改代码的时候可能只改了@property声明,并不会记得改getter,于是隐患就这样埋下了。
  • 代码含有了隐私操作,尤其getter中再混杂了各种逻辑,使得程序出现问题非常不好排查。后人哪会想到someObj.someProperty这样一个简简单单的取属性发生了很多奇妙的事。
  • 代码多,本来代码只需要在init方法中创建用上一两行,结果用了至少7行的一个getter方法才能写出来,想想一个程序轻则数百个属性,都这么搞,得多出多少行代码?另外代码格式几乎完全一样,不符合DRY原则。好的程序员不应该总是写重复的代码,不是么?(某人说一个程序里面,20-30个属性已经算非常多了,只能是眼界问题了)
  • 性能损耗,对于属性取值可能会非常的频繁,如果所有的属性取值之前都经过一个if判断,这不是平白浪费的性能?

我们回到正题。既然简单改写一下getter不但解决不了问题还有这么多隐患,那我们该如何能够正确优雅的把惰性计算写好?下面给大家一些建议。

观察上面的代码,你会发现_someBigProperty是一个非常规则的NSArray,它的item内容与下标相等。我们可以看出item的结果与index存在如下关系:

f(x) = x

类似的可以有很多,例如> 100的为@“world”0 <= x <= 100的为@“hello”;item为下标的平方;item为下标的数值转换成的字符串等。所以这类NSArray,基本需要一个count和一个函数就可以构成了。那我们现在就基于NSArray这个类簇,实现一个特殊的类吧!

关于类簇,相信很多同学都有所了解,大概的说法是不可以直接继承一个NSArrayNSNumberNSString这样的类。如果要继承需要实现全部的必要方法,在NSArray这个类簇来说,就是如下的方法:

1
2
3
4
5
6
7
8
9
@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>

@property (readonly) NSUInteger count;
- (ObjectType)objectAtIndex:(NSUInteger)index;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithObjects:(const ObjectType [])objects count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

@end

当然除了NSArray类的基本方法,还有NSCopyingNSMutableCopyingNSSecureCoding这些协议需要实现,另外NSFastEnumberation协议已经默认实现完成,不需要额外处理。与惰性计算无关的细节大家可以自己填补,对于本例,我们只需要关心这几个方法的实现:

1
2
3
4
5
6
7
8
typedef id(^ItemBlock)(NSUInteger index);

@interface ZDynamicArray : NSArray

- (instancetype)initWithItemBlock:(ItemBlock)block count:(NSUInteger)cnt;
- (id)objectAtIndex:(NSUInteger)index;
- (NSUInteger)count;
@end

按照上文的说法,对于这样一个特殊的NSArray,我们真正要储存的数据只有一个count值外加一个函数,所以我们用这两个作为init参数。实现也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@interface ZDynamicArray()

@property (nonatomic, readonly) ItemBlock block;
@property (nonatomic, readonly) NSUInteger cnt;
@end

@implementation ZDynamicArray

- (instancetype)initWithItemBlock:(ItemBlock)block count:(NSUInteger)cnt
{
    if (self = [super init]) {
        _block = block;
        _cnt = cnt;
    }
    return self;
}

- (NSUInteger)count
{
    return self.cnt;
}

- (id)objectAtIndex:(NSUInteger)index
{
    if (self.block) {
        return self.block(index);
    } else {
        return nil;
    }
}

@end

瞧,就这么简单的写好了。让我们试一下吧!

1
2
3
4
5
6
7
8
9
ZDynamicArray *array = [[ZDynamicArray alloc] initWithItemBlock:^id(NSUInteger index) {
    return @(index);
} count:100000];

for (id v in array) {
    NSLog(@"%@", v);
}

NSLog(@"%@", array[15]);

一个看似10w数据的数组,其实占用空间微乎其微,但是作用和最开始那样的代码效果一样。很不错吧。大家也可以动手实践,写一些自己需要用到的惰性计算代码,例如一个Model的数组,并非所有的Model都需要用到,我们也可以做成这样的一个数组,等用到的时候再从NSDicitonary转换成Model。就像这样:

1
2
3
4
5
6
NSArray *downloadData = @[@{}, @{}, @{}, @{}];

NSArray *modelArray = [[ZDynamicArray alloc] initWithItemBlock:^id(NSUInteger index) {
    return [SomeModel modelFromDictionary:downloadData[index]];
} count:downloadData.count];

好了,惰性计算就说到这里了。大家善加利用,一定可以写一些好玩的东西的。

致程序员的价值观

近日来受招聘与管理问题的折磨,颇有思考。程序员的价值观依然不像当初般单纯,当技术与绩效、等级、金钱挂钩,似乎越来越多的人已经忘却了当初的初心。

今天偶然间想起高中时的语文老师念给大家听的一小段文章,至今仍感动不已,也分享给大家。

故事是用第一人称写的:

暴风雨过后的一个早晨,我在海边散步。只见许多小鱼被卷上岸后,困在了浅水洼里。太阳一出来,它们就得干死。 浅水洼旁,一个小男孩不停地弯下腰去,捡起小鱼又用力地将它们扔回大海。 我便忍不住走过去说:“孩子,这水洼里有几百几千条小鱼,你救不过来的。” “我知道。”小男孩头也不抬地回答我。 我又接着说道:“那你为什么还在扔,你看大家都在忙别的,谁又在乎呢?” “但是这条小鱼在乎。”男孩一边回答,一边又拾起一条小鱼扔进大海。“这条在乎,这条也在乎!还有这一条,这一条……”

我知道现在仍然处于震撼和感动中,物欲横流的社会,谁又不是因为救不完就不救了; 谁又不是因为别人不救就不救了; 谁又不是因为没人在乎就不救了呢?

唯有这个孩子,保持着初心,在有限的时间里,能拯救一个生命就拯救一个。单纯,有时也很伟大。

回到我们的行业,大家重新审视下自己,我们是否因为某个行业前景好而去选择这个行业而不在乎是否自己有兴趣; 我们是否为了面试而储备了很多知识而非为了提高; 我们是否为了职级晋升而选择一个需求而非这个需求是客户所需; 我们是否绩效而执行一个任务而非这个任务对团队和公司有益。

我相信,每个程序员都曾经有过自己的程序像自己的孩子一样的归属感,并因此去捍卫自己的作品; 每个程序员都曾有过追求技术和专精的年代,为了兴趣和提高自己不断的尝试和探索; 每个程序员都有着来到一家公司的荣耀感与使命感,为了让产品变得更美好而倾注自己的时光。

我见过很多的优秀的行业大牛,他们大都保持着初心,我相信这也是让他们成为大牛的一个很重要的原因。

让我们重新唤起归属与探索、荣耀与使命,做一个有着正能量的骄傲的程序员吧!

iOS开发下的函数响应式编程

版权说明

本文为刊登于《程序员》杂志2016年5月刊。如需转载,请与《程序员》杂志联系。

背景和面临的问题

随着移动互联网的蓬勃发展,iOS App的复杂度呈指数增长。美团·大众点评两个App随着用户量不断增加、研发工程师数量不断增多,可用性的要求也随之不断提升。在这样的一个背景之下,我们面临了很多的问题和挑战。美团和大众点评的iOS工程师们面对挑战,想出了很多的策略和方针来应对,引入函数响应式编程就是美团App中重要的一环。

函数响应式编程简介

函数式编程想必您一定听过,但响应式编程的说法就不大常见了。与响应式编程对应的命令式编程就是大家所熟知的一种编程范式,我们先来看一段代码:

1
2
3
4
5
6
7
int a = 3;
int b = 4;
int c = a + b;
NSLog(@"c is %d", c); // => 12
a = 5;
b = 7;
NSLog(@"c is %d", c); // 仍然是12

命令式编程就是通过表达式或语句来改变状态量,例如c = a + b就是一个表达式,它创建了一个名称为c的状态量,其值为a与b的加和。下面的a = 5是另一个语句,它改变了a的值,但这时c是没有变化的。所以命令式编程中c = a + b只是一个瞬时的过程,而不是一个关系描述。在传统的开发中,想让c跟随a和b的变化而变化是比较困难的。而让c的值实时等于a与b的加和的编程方式就是响应式编程。

实际上,在日常的开发中我们会经常使用响应式编程的思想来进行开发。最典型的例子就是Excel,当我们在一个B1单元格上书写一个公式“=A1+5”时,便声明了一种对应关系,每当A1单元格发生变化时,单元格B2都会随之改变。

image
图1 Excel中的响应式

iOS开发中也有响应式编程的典型例子,例如Autolayout。我们通过设置约束描述了各个视图的位置关系,一旦其中一个变化,另一个就会响应其变化。类似的例子还有很多。

函数响应式编程(英文Functional Reactive Programming,以下简称FRP,)正是在函数式编程的基础之上,增加了响应式的支持。

简单来讲,FRP是基于异步事件流进行编程的一种编程范式。针对离散事件序列进行有效的封装,利用函数式编程的思想,满足响应式编程的需要。

区别于面向过程编程范式以过程单元作为核心组成部分,面向对象编程范式以对象单元作为核心组成部分,函数式编程范式以函数和高阶函数作为核心组成部分。FRP则以离散有序序列作为核心组成部分,也可将其定义为信号。其特点是具备可迭代特性并且允许离散事件节点有时间联系,计算机科学中称为Monad。

严格意义上来讲,下文提及的iOS开发下的函数响应式编程,并不能算完全的FRP,这一点,本文就不做学术上的讨论了。

接来下会为您介绍iOS相关的FRP内容,我们先从选型开始。

iOS项目的函数响应式编程选型

很长一段时间以来,iOS项目并没有很好的FRP支持,直到iOS 4.0 SDK中增加了Block语法才为函数式编程提供了前置条件,FRP开源库也逐步健全起来。

最先与大家见面的莫过于ReactiveCocoa这样一个库了,ReactiveCocoa是Github在制作Github客户端时开源的一个副产物,缩写为RAC。它是Objective-C语言下FRP思想的一个优秀实例,后续版本也支持了Swift语言。

Swift语言的推出为iOS界的函数式编程爱好者迎来了曙光。著名的FRP开源库Rx系列也新增了RxSwift,保持其接口与ReactiveX.net、RxJava、RxJS接口保持一致。

下面对不同厂商几个版本的FRP库进行简单的对比:

_ Objective-C 支持 Swift 支持 Cocoa框架支持 其他
RAC 2.5 × 完善 迭代周期长,稳定
RAC 3.0+ 继承2.5版本 开始全面支持Swift
RxSwift × 不完善 符合Rx标准

表1 iOS下几种FRP库的对比

美团App由于历史原因仍然沿用ReactiveCocoa 2.5版本。下文也主要会针对ReactiveCocoa 2.5版本做介绍,但各位可以根据自己项目的需要来选择FRP库,其思想和主要的API大同小异。

为什么需要在iOS项目中引入FRP这样厚重的库呢?

iOS的项目主要以客户端项目为主,主要的业务场景就是进行页面交互和与服务器拉取数据,这里面会包含多种事件和异步处理逻辑。FRP本身就是面向事件流的一种开发方式,又擅长处理异步逻辑。所以从逻辑上是满足iOS客户端业务需要的。

然而能够把一个理念融合到实际的项目中,需要一个漫长的过程。所以接下来就根据美团App在FRP上的实践,具体讲述下融入FRP的过程。希望能给大家一些参考。

一步一步进行函数响应式编程

众所周知,FRP的学习是存在一定门槛的,想必这也是大家对FRP、ReactiveCocoa这些概念比较畏惧的主要原因。美团App在推行FRP的过程中,是采用分步骤的形式,逐步演化的。其演化的过程可以分为初探、入门、提高、进阶这样四个阶段。

初探

美团App是在2014年5月第一次将ReactiveCocoa这个开源库纳入到工程中的,当时iOS工程师数量还不是很多,但是已经遇到了写法不统一、代码量膨胀等问题了。

写法不统一聚焦在回调形式的不统一上,iOS中的回调方式有非常多的种类:UIKit主要进行的事件处理target-action、跨类依赖推荐的delegate模式、iOS 4.0纳入的block、利用通知中心(Notifcation Center)进行松耦合的回调、利用键值观察(Key-Value Observe,简称KVO)进行的监听。由于场景不同,选用的规则也不尽相同,并且我们没有办法很好的界定什么场景该写什么样的回调。

这时我们发现ReactiveCocoa这样一个开源库,恰好能以统一的形式来解决跨类调用的问题,也包装了UIKit事件处理、Notifcation Center、KVO等常见的场景。使其代码风格上高度统一。

使用ReactiveCocoa进行统一后,上述的几种回调都可以写成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 代替target-action
    [[self.confirmButton rac_signalForControlEvents:UIControlEventTouchUpInside]
     subscribeNext:^(id x) {
        // 回调内容写在这里
     }];
    
// 代替delegate
    [[self.scrollView rac_signalForSelector:@selector(scrollViewDidScroll:) fromProtocol:@protocol(UIScrollViewDelegate)]
     subscribeNext:^(id x) {
        // 回调内容写在这里
     }];
    
// 代替block
    [[self asyncProcess]
     subscribeNext:^(id x) {
        // 回调内容写在这里
     } error:^(NSError *error) {
        // 错误处理写到这里
     }];
    
// 代替notification center
    [[[NSNotificationCenter defaultCenter] rac_valuesForKeyPath:@"Some-key" observer:nil]
     subscribeNext:^(id x) {
        // 回调内容写在这里
     }];

// 代替KVO
    [RACObserve(self, userReportString)
     subscribeNext:^(id x) {
        // 回调内容写在这里
     }];

代码1 回调统一

通过观察代码不难发现,ReactiveCocoa使得不同场景下的代码样式上高度统一,使我们在书写代码、维护代码、阅读代码方面的效率大大提高。

经过一定的研究,我们也发现使用RAC(target, key)宏可以更好组织代码形式,利用filter:map:来代替原有的代码,达到更好复用,例如下面两段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 旧写法
    @weakify(self)
    [[self.textField rac_newTextChannel]
     subscribeNext:^(NSString *x) {
         @strongify(self)
         if (x.length > 15) {
             self.confirmButton.enabled = NO;
             [self showHud:@"Too long"];
         } else {
             self.confirmButton.enabled = YES;
         }
         self.someLabel.text = x;
         
     }];
    
// 新写法
    RACSignal *textValue = [self.textField rac_newTextChannel];
    RACSignal *isTooLong = [textValue
                            map:^id(NSString *value) {
                                return @(value.length > 15);
                            }];
    RACSignal *whenItsTooLongMessage = [[isTooLong
                                         filter:^BOOL(NSNumber *value) {
                                             return @(value.boolValue);
                                         }]
                                        mapReplace:@"Too long"];
    [self rac_liftSelector:@selector(showHud:) withSignals:whenItsTooLongMessage, nil];
    RAC(self.confirmButton, enabled) = [isTooLong not];
    RAC(self.someLabel, text) = textValue;

代码2 逻辑优化

上述代码修改虽然代码行数有一定的增加,但是结构更加清晰,复用性也做得更好。

综上所述,在这一阶段,我们主要以回调形式的统一为主,不断尝试合适的代码形式来表达绑定这种关系,也寻找一些便捷的小技巧来优化代码。

入门

image
图2 美团App首页

单纯解决回调风格的统一和树立绑定的思维是远远不够的,代码中更大的问题在于共享变量、异步协同以及异常传递的处理。 列举几个简单的场景,就拿美团App的首页来讲,我们可以看到上面包含很多的区块,而各个区块的访问接口不尽相同,但是渲染的逻辑却又多种多样:

  • 有的需要几个接口都返回后才能统一渲染。
  • 有的需要一个接口返回后,根据返回的内容决定后续的接口访问,最终才能渲染。
  • 有的则是几个接口按照返回顺序依次渲染。

这就导致我们在处理这些逻辑的时候,需要很多的异步处理手段,利用一些中间变量来储存状态,每次返回的时候又判断这些状态决定渲染的逻辑。

更糟糕的是,有的时候对于同时请求多个网络接口,某些出现了网络错误,异常处理也变得越来越复杂。

随着对ReactiveCocoa理解的加深,我们意识到使用信号的组合等“高级”操作可以帮助我们解决很多的问题。例如merge:操作可以解决依次渲染的问题,zip:操作可以解决多个接口都返回后再渲染的问题,flattenMap:可以解决接口串联的问题。大概的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 依次渲染
    RACSignal *mergedSignal = [RACSignal merge:@[[self fetchData1],
                                                 [self fetchData2]]];
    
// 接口都返回后一起渲染
    RACSignal *zippedSignal = [RACSignal zip:@[[self fetchData1],
                                               [self fetchData3]]];
    
// 接口串联
    @weakify(self)
    RACSignal *finalSignal = [[self fetchData4]
                              flattenMap:^RACSignal *(NSString *data4Result) {
                                  @strongify(self)
                                  return [self fetchData5:data4Result];
                              }];

没有用到一个中间状态变量,我们通过这几个“魔法接口”神奇地将逻辑描述了出来。这样写的好处还有很多。

FRP具备这样一个特点,信号因为进行组合从而得到了一个数据链,而数据链的任一节点发出错误信号,都可以顺着这个链条最终交付给订阅者。这就正好解决了异常处理的问题。

image
图3 错误传递链

由于此项特性,我们可以不再关注错误在哪个环节,只需等待订阅的时候统一处理即可。我们也找到了很多的方法用来更好地支持异常的处理。例如try:catch:catchTo:tryMap:等。

简单列举下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 尝试判断并捕捉异常
    RACSignal *signalForSubscriber =
    
      [[[[[self fetchData1]
          try:^BOOL(NSString *data1,
                    NSError *__autoreleasing *errorPtr) {
              if (data1.length == 0) {
                  *errorPtr = [NSError new];
                  return YES;
              }
              return NO;
          }]
         flattenMap:^RACStream *(id value) {
             @strongify(self)
             return [self fetchData5:value];
         }]
        try:^BOOL(id value,
                  NSError *__autoreleasing *errorPtr) {
            if (![value isKindOfClass:[NSString class]]) {
                *errorPtr = [NSError new];
                return YES;
            }
            return NO;
        }]
       catch:^RACSignal *(NSError *error) {
           return [RACSignal return:error.domain];
       }];

总结一下,在这个阶段,我们主要尝试解决了异步协同的问题,包括了异常的处理。运用了异常处理模型来解决了很多的实际问题,同时继续寻找了更多的技巧来优化代码。

在初探和入门这两个阶段,美团App还只是谨慎地进行小的尝试,主旨是以代码简化为目的,使用ReactiveCocoa这个开源框架的一些便利功能来优化代码。在代码覆盖程度上尽量只在模块内部使用,避免跨层使用ReactiveCocoa。

提高

随着对ReactiveCocoa这个开源框架的理解不断加深。美团App并不满足于简单的尝试,而是开始在更多的场景下使用ReactiveCocoa,并体现一定的FRP思想。这个阶段最具代表性的实践就是与MVVM架构的融合了,它就是体现了FRP响应式的思想。

Model-View-Controller(简称MVC)是苹果Cocoa框架默认的一个架构。实际上业务场景的复杂度越来越高,而MVC架构自身也存在分层不清晰等诸多问题,最终使得MVC这一架构在实际的使用中渐渐走了样。

Model-View-ViewModel(简称MVVM)便是近几年来十分推崇的一种架构,它解决了MVC架构的一些不足,在层次定义上更为清晰。在MVVM的架构中,最为关键的一环莫过于ViewModel层与View层的绑定了,我们的主角FRP恰好可以解决绑定问题,同时还能处理跨层错误处理的问题。

先来关注下绑定,自初探阶段开始,我们就开始使用RAC(target, key)这样的一个宏来表述绑定的关系,并且使用一些简单的信号转换使得原始信号满足视图渲染的需求。在引入MVVM架构后,我们将之前的经验利用起来,并且使用了RACChannel、RACCommand等组件来支持MVVM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 单向绑定
    RAC(self.someLabel, text) = RACObserve(self.viewModel, someProperty);
    RAC(self.scrollView, hidden) = self.viewModel.someSignal;
    RAC(self.confirmButton, frame) = [self.viewModel.someChannel
                                      map:^id(NSString *str) {
                                          CGRect rect = CGRectMake(0, 0, 0, str.length * 3);
                                          return [NSValue valueWithCGRect:rect];
                                      }];
    
// 双向绑定
    RACChannelTo(self.someLabel, text) = RACChannelTo(self.viewModel, someProperty);
    [self.textField.rac_newTextChannel subscribe:self.viewModel.someChannel];
    [self.viewModel.someChannel subscribe:self.textField.rac_newTextChannel];
    RACChannelTo(self, reviewID) = self.viewModel.someChannel;
    
// 命令绑定
    self.confirmButton.rac_command = self.viewModel.someCommand;
    
    RAC(self.textField, hidden) = self.viewModel.someCommand.executing;
    [self.viewModel.someCommand.errors
     subscribeNext:^(NSError *error) {
         // 错误处理在这里
     }];

绑定只是提供了上层的业务逻辑,更为重要的是,FRP的响应式范式恰如其分地体现在MVVM中。一个MVVM中View就会响应ViewModel的变化。我们来根据一副简单的图来分析一下:

image
图4 MVVM示意图

上述简图列出了View-ViewModel-Model的大致关系,View和ViewModel间通过RACSignal来进行单向绑定,通过RACChannel来进行双向绑定,通过RACCommand进行执行过程的绑定。

ViewModel和Model间通过RACObserve进行监听,通过RACSignal进行回调处理,也可以直接调用方法。

Model有自身的数据业务逻辑,包含请求Web Service和进行本地持久化。

响应式的体现就在于View从一开始就是“声明”了与ViewModel间的关系,就如同A3单元格声明其“=A2+A1”一样。一旦后续数据发生变化,就按照之前的约定响应,彼此之间存在一层明确的定义。View在业务层面也得到了极大简化。

具体的数据流动就如同下图两种形式:

image
image
图5&图6 MVVM的数据流向示意

从两张图中可以看出,无论View收到用户修改TextField的文本框内容的事件,还是受到用户点击Button的事件。View层都不需要对此做特殊的逻辑处理,只是将之传递给ViewModel。而ViewModel自身维护逻辑,并体现在某些绑定关系上。这是与MVC中ViewController和Model的关系是截然不同的。FRP的响应式范式很好的帮助我们实现了此类需求。

之前虽然也提到过错误处理,但是也提到美团App在初探和入门阶段,只是小规模的在模块内使用,对外并不会以RACSignal的形式暴露。而这个阶段,我们也尝试了层级间通过RACSignal来进行信息的传递。这也自然可以应用上FRP异常处理的优势。

image
图7 MVVM的数据流向示意

上图体现了一个按钮绑定了RACCommand收到错误后的一个数据流向。

除了MVVM框架的应用,这个阶段美团App也利用FRP解决另外的一个大问题。那就是多线程。

如果你做过异步拉取数据主线程渲染,那么你一定很熟悉子线程的回调结果转移到主线程的过程。这种操作不但繁琐,重复,关键还容易出错。

RAC提供了很多便于处理多线程的组件,核心类为RACScheduler,使得可以方便的通过subscirbeOn:方法、deliverOn:方法进行线程控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 多线程控制
    RACScheduler *backgroundScheduler = [RACScheduler scheduler];
    RACSignal *testSignal = [[RACSignal
                             createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                                 // 本例中,这段代码会确保运行在子线程
                                 [subscriber sendNext:@1];
                                 [subscriber sendCompleted];
                                 return nil;
                             }]
                             subscribeOn:backgroundScheduler];
    
    [[RACScheduler mainThreadScheduler]
     schedule:^{
        // 这段代码会在下次Runloop时在主线程中运行
         [[testSignal
          deliverOn:[RACScheduler mainThreadScheduler]]
            subscribeNext:^(id x) {
                // 虽然信号的发出者是在子线程中发出消息的
                // 但是接收者确可以确保在主线程接收消息
                
                // 主线程可以做你想做的渲染工作了!
            }
         ];
     }];

这一个阶段也算是大跃进的一个阶段,随着MVVM的框架融入,跨层的使用RAC使得代码整体使用FRP的比重大幅提高,全员对FRP的熟悉程度和思想的理解也变得深刻了许多。同时也真正使用了响应式的一些思想和特性来解决实际的问题。使其不再是纸上空谈。

我们美团App也在这个阶段挖掘了更多的RAC高级组件,继续代码优化的持续之路。

进阶

美团App的iOS工程师们在大规模使用FRP后,也积蓄了很多的问题。很多小伙伴也问起了,既然是叫FRP,为什么一直体现的都是响应式的思想,对于函数式的思想应用体现似乎不是很明显。虽然FRP是F开头,称为函数响应式编程。但是考虑到函数式编程的复杂性,我们也将函数式编程的优化拿到了进阶这一阶段来尝试。

这一阶段面临的问题是RAC的大规模应用,使得代码中包含了大量的框架性质的代码。例如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 冗余的代码
    [[self fetchData1]
     try:^BOOL(id value,
               NSError *__autoreleasing *errorPtr) {
         if ([value isKindOfClass:[NSString class]]) {
             return YES;
         }
         *errorPtr = [NSError new];
         return NO;
     }];
    
    [[self fetchData2]
     tryMap:^id(NSDictionary *value, NSError *__autoreleasing *errorPtr) {
         if ([value isKindOfClass:[NSDictionary class]]) {
             if (value[@"someKey"]) {
                 return value[@"someKey"];
             }
             // 并没有一个名为“someKey”的key
             *errorPtr = [NSError new];
             return nil;
         }
         // 这不是一个Dictionary
         *errorPtr = [NSError new];
         return nil;
     }];
    
    [[self fetchData3]
     tryMap:^id(NSDictionary *value, NSError *__autoreleasing *errorPtr) {
         if ([value isKindOfClass:[NSDictionary class]]) {
             if (value[@"someOtherKey"]) {
                 return value[@"someOtherKey"];
             }
             // 并没有一个名为“someOtherKey”的key
             *errorPtr = [NSError new];
             return nil;
         }
         // 这不是一个Dictionary
         *errorPtr = [NSError new];
         return nil;
     }];

上述的几个代码段,我们可以看到功能非常近似,内容稍有不同的部分重复出现,很多的同学在实际的开发中也并没有太好地优化它们,甚至很多的同学表示束手无策。这时候函数式编程就可以派上用场了。

函数式编程是一种良好的编程范式,我们在这里主要利用它的几个特点:高阶函数、不变量和迭代。

先来看高阶函数,高阶函数是入参是函数或者返回值是函数的函数。说起来虽然有些拗口,但实际上在iOS开发中司空见惯,例如典型的订阅其实就是一个高阶函数的体现。

1
2
3
4
5
// 高阶函数
    [[self fetchData1]
     subscribeNext:^(id x) {
        // 这是一个block,作为一个参数
     }];

我们更关心的是返回值是函数的函数,这是上面冗长的代码解决之道。代码7的代码中会发现一些相同的逻辑,例如类型判断。我们就可以先做一个这样的小函数:

1
2
3
4
5
6
7
8
typedef BOOL (^VerifyFunction)(id value);

VerifyFunction isKindOf(Class aClass)
{
    return ^BOOL(id value) {
        return [value isKindOfClass:aClass];
    };
}

瞧,很简单对不对!只要把一个类型传进去,就会得到一个用来判断某个对象是否是这个类型的函数。细心的读者会发现我们实际要的是一个入参为对象和一个NSError对象指针的指针类型,返回值是布尔类型的block,但是这个只能返回入参是对象的,显然不满足条件。很多人第一个想到的就是把这个函数改成返回参数为两个参数返回值为布尔类型的block,但是函数式的解决方法是新增一个这样的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef BOOL (^VerifyAndOutputErrorFunction)(id value, NSError **error);

VerifyAndOutputErrorFunction verifyAndOutputError(VerifyFunction verify,
                                                  NSError *outputError)
{
    return ^BOOL(id value, NSError **error) {
        if (verify(value)) {
            return YES;
        }
        *error = outputError;
        return NO;
    };
}

一个典型的高阶函数,入参带有一个block,返回值也是一个block,组合起来就可以把刚才的几个try:代码段优化。可能你会问,为什么要搞成两个呢,一个不是更好?搞成两个的好处就在于,我们可以将任意的VerifyFunction类型的block与一个outputError相结合,来返回一个我们想要的VerifyAndOutputErrorFunction类型block,例如增加一个判断NSDictionary是否包含某个Key的VerifyFunction。下面给出一个优化后的代码,大家可以仔细思考下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// 可以高度复用的函数
typedef BOOL (^VerifyFunction)(id value);

VerifyFunction isKindOf(Class aClass)
{
    return ^BOOL(id value) {
        return [value isKindOfClass:aClass];
    };
}

VerifyFunction hasKey(NSString *key)
{
    return ^BOOL(NSDictionary *value) {
        return value[key] != nil;
    };
}

typedef BOOL (^VerifyAndOutputErrorFunction)(id value, NSError **error);

VerifyAndOutputErrorFunction verifyAndOutputError(VerifyFunction verify,
                                                  NSError *outputError)
{
    return ^BOOL(id value, NSError **error) {
        if (verify(value)) {
            return YES;
        }
        *error = outputError;
        return NO;
    };
}

typedef id (^MapFunction)(id value);

MapFunction dictionaryValueByKey(NSString *key)
{
    return ^id(NSDictionary *value) {
        return value[key];
    };
}

// 与本例关联比较大的函数
typedef id (^MapAndOutputErrorFunction)(id value, NSError **error);
MapAndOutputErrorFunction transferToKeyChild(NSString *key)
{
    return ^id(id value, NSError **error) {
        if (hasKey(key)(value)) {
            return dictionaryValueByKey(key)(value);
        } else {
            *error = [NSError new];
            return nil;
        }
    };
};

- (void)oldStyle
{
    // 冗余的代码
    [[self fetchData1]
     try:^BOOL(id value,
               NSError *__autoreleasing *errorPtr) {
         if ([value isKindOfClass:[NSString class]]) {
             return YES;
         }
         *errorPtr = [NSError new];
         return NO;
     }];
    
    [[self fetchData2]
     tryMap:^id(NSDictionary *value, NSError *__autoreleasing *errorPtr) {
         if ([value isKindOfClass:[NSDictionary class]]) {
             if (value[@"someKey"]) {
                 return value[@"someKey"];
             }
             // 并没有一个名为“someKey”的key
             *errorPtr = [NSError new];
             return nil;
         }
         // 这不是一个Dictionary
         *errorPtr = [NSError new];
         return nil;
     }];
    
    [[self fetchData3]
     tryMap:^id(NSDictionary *value, NSError *__autoreleasing *errorPtr) {
         if ([value isKindOfClass:[NSDictionary class]]) {
             if (value[@"someOtherKey"]) {
                 return value[@"someOtherKey"];
             }
             // 并没有一个名为“someOtherKey”的key
             *errorPtr = [NSError new];
             return nil;
         }
         // 这不是一个Dictionary
         *errorPtr = [NSError new];
         return nil;
     }];
}

- (void)newStyle
{
    
    VerifyAndOutputErrorFunction isDictionary = 
      verifyAndOutputError(isKindOf([NSDictionary class]),
                          NSError.new);
    VerifyAndOutputErrorFunction isString =      
      verifyAndOutputError(isKindOf([NSString class]),
                         NSError.new);
 
    [[self fetchData1]
     try:isString];
    
    [[[self fetchData2]
      try:isDictionary]
      tryMap:transferToKeyChild(@"someKey")];
    
    [[[self fetchData3]
      try:isDictionary]
     tryMap:transferToKeyChild(@"someOtherKey")];
}

虽然代码有些多,但是从newStyle函数的结果来看,我们在实际的业务代码上非常的简洁,而且还抽离出很多可复用的小函数。在实际的业务中,我们甚至通过这种范式在某些业务场景简化了超过50%的代码量。

除此之外,我们还尝试用迭代来进一步减少临时变量。为什么要减少临时变量呢?因为我们想要遵循不变量原则,这是函数式编程的一个特点。试想下如果我们都是使用一些不变量,就不再会有那么多异步锁和痛苦的多线程问题了。基于以上考虑,我们要求工程师尽量在开发的过程中减少使用变量,从而锻炼用更加函数式的方式来解决问题。

例如下面的简单问题,实现一个每秒发送值为0 1 2 3 … 100的递增整数信号,实现的方法可以是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)countSignalOldStyle
{
    RACSignal *signal =
    [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        RACDisposable *disposable = [RACDisposable new];
        __block int i = 0;
        __block void (^recursion)();
        recursion = ^{
            if (disposable.disposed || i > 100) {
                return;
            }
            [subscriber sendNext:@(i)];
            ++i;
            [[RACScheduler mainThreadScheduler]
             afterDelay:1 schedule:recursion];
        };
        recursion();
        return disposable;
    }];
}

这样的代码不但用了block自递归,还用了一个闭包的i变量。i变量也在数次递归中进行了修改。代码不易理解且block自递归会存在循环引用。使用迭代和不变量的形式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)countSignalNewStyle
{
    RACSignal *signal =
    [[[[[[RACSignal return:@1]
         repeat] take: 100]
       scanWithStart:@0 reduce:^id(NSNumber *running,
                                        NSNumber *next) {
           return @(running.integerValue + next.integerValue);
       }]
      map:^id(NSNumber *value) {
          return [[RACSignal return:value]
                  delay:1];
      }]
     concat];
}

解法是这样的,先用固定返回1的信号生成一个无限重复信号,取前100个值,然后用迭代方法,产生一个递增的迭代,再将发送的密集的递增信号转成一个延时1秒的子信号,最后将子信号进行连接。感兴趣的同学可以自己动手尝试下,也希望大家都去思考不适用变量来解决问题的思路。

这些函数式的写法不仅解决了业务上的问题,也给我们美团App的iOS工程师们开拓了代码优化的新思路。

可以看到,到这一阶段,需要对FRP的理解要求更高。为了追求更好的代码体验,我们朝着FRP的道路又迈进了许多,走到这一步是每一个美团App的iOS工程师共同努力的结果。这是一个尚未完结的阶段,我们的工程师仍然在不选找寻更好的FRP范式。对于开发人员来说,优化之路永远不会停步。

总结

单纯靠这样一篇文章来介绍全部的FRP思想是不可能的,这也仅是起到了抛砖引玉的作用。FRP不仅可以解决项目中实际遇到的很多问题,也能锻炼更好的工程师素养。希望大家能够掌握起来,用FRP的思想来解决更多实际的问题。社区和开源库也需要大家的不断投入。谢谢大家!

细说ReactiveCocoa的冷信号与热信号

版权说明

本文为 美团点评技术团队博客 特供稿件,首发地址在此。如需转载,请与 美团点评技术团队博客 联系。

背景

ReactiveCocoa(简称RAC)是一套基于Cocoa的FRP框架,在我们美团客户端中,我们大量使用了这个框架。而在使用的过程中我们发现,冷信号与热信号的概念很容易混淆并且容易造成一定的问题,相信各位在使用的过程中也可能遇到此类问题。所以我在这里与大家讨论下RAC中冷信号与热信号的相关知识点,希望可以加深大家对冷热信号的理解。

p.s. 以下代码和示例基于ReactiveCocoa v2.5

什么是冷信号与热信号

冷热信号的概念源于C#的MVVM框架Reactive Extensions中的Hot Observables和Cold Observables:

Hot Observables和Cold Observables的区别:

  1. Hot Observables是主动的,尽管你并没有订阅事件,但是它会时刻推送,就像鼠标移动;而Cold Observables是被动的,只有当你订阅的时候,它才会发布消息。

  2. Hot Observables可以有多个订阅者,是一对多,集合可以与订阅者共享信息;而Cold Observables只能一对一,当有不同的订阅者,消息是重新完整发送。

这里面的Observables可以理解为RACSignal。为了加深理解,请大家关注这样的几组代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@1];
    [subscriber sendNext:@2];
    [subscriber sendNext:@3];
    [subscriber sendCompleted];
    return nil;
}];
NSLog(@"Signal was created.");
[[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
    [signal subscribeNext:^(id x) {
        NSLog(@"Subscriber 1 recveive: %@", x);
    }];
}];

[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
    [signal subscribeNext:^(id x) {
        NSLog(@"Subscriber 2 recveive: %@", x);
    }];
}];

以上简单的创建了一个信号,并且依次发送@1,@2,@3作为值。下面分别有两个订阅者在不同的时间段进行了订阅,运行的结果如下:

1
2
3
4
5
6
7
2015-08-11 18:33:21.681 RACDemos[6505:1125196] Signal was created.
2015-08-11 18:33:21.793 RACDemos[6505:1125196] Subscriber 1 recveive: 1
2015-08-11 18:33:21.793 RACDemos[6505:1125196] Subscriber 1 recveive: 2
2015-08-11 18:33:21.793 RACDemos[6505:1125196] Subscriber 1 recveive: 3
2015-08-11 18:33:22.683 RACDemos[6505:1125196] Subscriber 2 recveive: 1
2015-08-11 18:33:22.683 RACDemos[6505:1125196] Subscriber 2 recveive: 2
2015-08-11 18:33:22.683 RACDemos[6505:1125196] Subscriber 2 recveive: 3

我们可以看到,信号在18:33:21.681时被创建,18:33:21.793依次接到1、2、3三个值,而在18:33:22.683再依次接到1、2、3三个值。说明了变量名为signal的这个信号,在两个不同时间段的订阅过程中,分别完整的发送了所有的消息。

我们再对这段代码进行一个小的改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
RACMulticastConnection *connection = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
        [subscriber sendNext:@1];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
        [subscriber sendNext:@2];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{
        [subscriber sendNext:@3];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{
        [subscriber sendCompleted];
    }];
    return nil;
}] publish];
[connection connect];
RACSignal *signal = connection.signal;

NSLog(@"Signal was created.");
[[RACScheduler mainThreadScheduler] afterDelay:1.1 schedule:^{
    [signal subscribeNext:^(id x) {
        NSLog(@"Subscriber 1 recveive: %@", x);
    }];
}];

[[RACScheduler mainThreadScheduler] afterDelay:2.1 schedule:^{
    [signal subscribeNext:^(id x) {
        NSLog(@"Subscriber 2 recveive: %@", x);
    }];
}];

稍微有些复杂,我们来一一分析下:

  • 创建了一个信号,在1秒、2秒、3秒分别发送1、2、3这三个值,4秒发送结束信号。
  • 对这个信号调用publish方法得到一个RACMulticastConnection。
  • 将connection进行连接操作。
  • 获得connection的信号。
  • 分别在0.1秒和2秒订阅获得的信号。

抛开RACMulticastConnection是个什么东东,我们先来看下结果:

1
2
3
4
2015-08-12 11:07:49.943 RACDemos[9418:1186344] Signal was created.
2015-08-12 11:07:52.088 RACDemos[9418:1186344] Subscriber 1 recveive: 2
2015-08-12 11:07:53.044 RACDemos[9418:1186344] Subscriber 1 recveive: 3
2015-08-12 11:07:53.044 RACDemos[9418:1186344] Subscriber 2 recveive: 3

首先告诉大家-[RACSignal publish]- [RACMulticastConnection connect]- [RACMulticastConnection signal]这几个操作生成了一个热信号。 我们再来关注下输出结果的一些细节:

  • 信号在11:07:49.943被创建
  • 11:07:52.088时订阅者1才收到2这个值,说明1这个值没有接收到,时间间隔是2秒多
  • 11:07:53.044时订阅者1和订阅者2同时收到3这个值,时间间隔是3秒多

参考一开始的Hot Observables的论述和两段小程序的输出结果,我们可以确定冷热信号的如下特点:

  • 一、热信号是主动的,即使你没有订阅事件,它仍然会时刻推送。(如第二个例子,信号在50秒被创建,51秒的时候1这个值就推送出来了,但是当时还没有订阅者。)而冷信号是被动的,只有当你订阅的时候,它才会发送消息。(如第一个例子。)
  • 二、热信号可以有多个订阅者,是一对多,信号可以与订阅者共享信息(如第二个例子,订阅者1和订阅者2是共享的,他们都能在同一时间接收到3这个值。)而冷信号只能一对一,当有不同的订阅者,消息会从新完整发送。(如第一个例子,我们可以观察到两个订阅者没有联系,都是基于各自的订阅时间开始接收消息的。)

为什么要区分冷信号与热信号

也许你看到这里并且看到这一章节的标题就会有疑问,为什么RAC要搞如此复杂的一个概念,直接搞成一种信号不就好了么?要解释这个问题需要绕一些弯路。(前方可能比较难懂,如果不能很好理解,请自行查阅各类文档。)

最前面提到了RAC是一套基于Cocoa的FRP框架,那就来说说FRP,FRP全写是Functional Reactive Programming,中文译作函数响应式编程,是RP(Reactive Programm,响应式编程)的FP(Functional Programming,函数式编程)实现。说起来很拗口。太多的细节不多讨论,我们先关注下它是FP的情况。

FP有几个很重要的概念是和我们的主题相关的:

纯函数是指一个函数或者一个表达式不存在任何的副作用,就如同数学中的函数:

f(x) = 5x + 1

这个函数在调用的过程中产生除了返回值以外的任何作用,也不受任何外界因素的影响。那么副作用都有哪些呢?我来列举以下几个情况:

  • 函数的处理过程中,修改了外部的变量,例如全局变量。一个特殊点的例子,就是如果把OC的一个方法看做一个函数,所有的成员变量的赋值都是对外部变量的修改。是的,从FP的角度看OOP是充满副作用的。
  • 函数的处理过程中,触发了一些额外的动作,例如发送的全局的一个Notification,在console里面输出的结果,保存了文件,触发了网络,更新的屏幕等。
  • 函数的处理过程中,受到外部变量的影响,例如全局变量,方法里面用到的成员变量。注意block中捕获的外部变量也算副作用。
  • 函数的处理过程中,受到线程锁的影响算副作用。

由此我们可以看出,在目前的iOS编程中,我们是很难的摆脱副作用的。或者换一种说法,我们iOS编程的目的其实是副作用。(基于用户触摸的外界因素,最终反馈到网络变化和屏幕变化上。)

接下来我们来分析下副作用与冷热信号的关系。既然iOS编程中少不了副作用,那么RAC在实际的使用中也不可避免的接触副作用,下面我列举个业务场景,来看下冷信号中副作用的坑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    self.sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://api.xxxx.com"]];

    self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer];
    self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];

    @weakify(self)
    RACSignal *fetchData = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        @strongify(self)
        NSURLSessionDataTask *task = [self.sessionManager GET:@"fetchData" parameters:@{@"someParameter": @"someValue"} success:^(NSURLSessionDataTask *task, id responseObject) {
            [subscriber sendNext:responseObject];
            [subscriber sendCompleted];
        } failure:^(NSURLSessionDataTask *task, NSError *error) {
            [subscriber sendError:error];
        }];
        return [RACDisposable disposableWithBlock:^{
            if (task.state != NSURLSessionTaskStateCompleted) {
                [task cancel];
            }
        }];
    }];

    RACSignal *title = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {
        if ([value[@"title"] isKindOfClass:[NSString class]]) {
            return [RACSignal return:value[@"title"]];
        } else {
            return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]];
        }
    }];

    RACSignal *desc = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {
        if ([value[@"desc"] isKindOfClass:[NSString class]]) {
            return [RACSignal return:value[@"desc"]];
        } else {
            return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]];
        }
    }];

    RACSignal *renderedDesc = [desc flattenMap:^RACStream *(NSString *value) {
        NSError *error = nil;
        RenderManager *renderManager = [[RenderManager alloc] init];
        NSAttributedString *rendered = [renderManager renderText:value error:&error];
        if (error) {
            return [RACSignal error:error];
        } else {
            return [RACSignal return:rendered];
        }
    }];

    RAC(self.someLablel, text) = [[title catchTo:[RACSignal return:@"Error"]]  startWith:@"Loading..."];
    RAC(self.originTextView, text) = [[desc catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."];
    RAC(self.renderedTextView, attributedText) = [[renderedDesc catchTo:[RACSignal return:[[NSAttributedString alloc] initWithString:@"Error"]]] startWith:[[NSAttributedString alloc] initWithString:@"Loading..."]];

    [[RACSignal merge:@[title, desc, renderedDesc]] subscribeError:^(NSError *error) {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:error.domain delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alertView show];
    }];

不晓得大家有没有被这么一大段的代码吓到,我想要表达的是,在真正的工程中,我们的业务逻辑是很复杂的,而一些坑就隐藏在如此看似复杂但是又很合理的代码之下。所以我尽量模拟了一些需求,使得代码看起来更丰富,下面我们还是来仔细看下这段代码的逻辑吧:

  1. 创建了一个AFHTTPSessionManager用来做网络接口的数据获取。
  2. 创建了一个名为fetchData的信号来通过网络获取信息。
  3. 创建一个名为title的信号从获取的data中取得title字段,如果没有该字段则反馈一个错误。
  4. 创建一个名为desc的信号从获取的data中取得desc字段,如果没有该字段则反馈一个错误。
  5. 针对desc这个信号做一个渲染,得到一个名为renderedDesc的新信号,该信号会在渲染失败的时候反馈一个错误。
  6. title信号所有的错误转换为字符串@"Error"并且在没有获取值之前以字符串@"Loading..."占位,之后与self.someLableltext属性绑定。
  7. desc信号所有的错误转换为字符串@"Error"并且在没有获取值之前以字符串@"Loading..."占位,之后与self.originTextViewtext属性绑定。
  8. renderedDesc信号所有的错误转换为属性字符串@"Error"并且在没有获取值之前以属性字符串@"Loading..."占位,之后与self.renderedTextViewtext属性绑定。
  9. titledescrenderedDesc这三个信号的任何错误订阅,并且弹出UIAlertView

看到这里我相信很多熟悉RAC的同学应该是对这些代码表示认同的,它也体现了RAC的一些优势例如良好的错误处理和各种链式处理。但是很遗憾的告诉大家这段代码是有很严重的错误的。

如果你去尝试运行这段代码,并且打开Charles查看,你会惊奇的发现,这个网络请求发送了6次。没错,是6次请求。我们也可以想象到类似的代码在其他副作用的问题,重新刷新了6次屏幕,写入6次文件,发了6个全局通知。

下面来分析下,为什么是6次网络请求呢?首先根据上面的知识,我们可以推断出名为fetchData信号是一个冷信号。那么这个信号在订阅的时候就会执行里面的过程。那这个信号是在什么时候被订阅了呢?仔细回看了代码,我们发现并没有订阅这个信号,只是调用这个信号的flattenMap产生了两个新的信号。

这里有一个很重要的概念,就是任何的信号转换即是对原有的信号进行订阅从而产生新的信号。我们可以写出flattenMap的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (instancetype)flattenMap_:(RACStream * (^)(id value))block {
{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
       return [self subscribeNext:^(id x) {
           RACSignal *signal = (RACSignal *)block(x);
           [signal subscribeNext:^(id x) {
               [subscriber sendNext:x];
           } error:^(NSError *error) {
               [subscriber sendError:error];
           } completed:^{
               [subscriber sendCompleted];
           }];
       } error:^(NSError *error) {
           [subscriber sendError:error];
       } completed:^{
           [subscriber sendCompleted];
       }];
    }];
}

除了没有高度复用和缺少一些disposable的处理以外,上述代码可以大致的给我们flattenMap的直观处理,我们可以看到其实是在调用这个方法的时候,生成了一个新的信号,在这个新的信号的执行过程中对self进行的了订阅。我们还需要注意一个细节,就是这个返回信号在未来订阅的时候,才会间接的订阅了self。后续的startWithcatchTo等都可以这样理解。

回到我们的问题,那就是说,在fetchDataflattenMap之后,它就会因为名为titledesc信号的订阅而订阅。而后续我们对desc也进行了flattenMap得到了renderedDesc,那也说明了未来renderedDesc被订阅的时候,fetchData也会被间接订阅。所以我们解释了在后续我们用RAC宏进行绑定的时候,引发的3次fetchData的订阅。由于fetchData是冷信号,所以3次订阅意味着它的过程被执行了3次,也就是网络的3次请求。

另外的3次订阅来自RACSignal类的merge方法。根据上述的描述,我们也可以猜测merge方法也一定是创建了一个新的信号,在这个信号被订阅的时候,把它包含的所有信号订阅。所以我们又得到了额外的3次网络请求。

由此我们可以深刻的看到不熟悉冷热信号对业务造成的影响。我们可以想象对用户流量的影响,对服务器负载的影响,对统计的影响,如果这是一个点赞的接口,会不会造成多次点赞?后果是不堪的。而着一些都可以通过把fetchData转换为热信号来解决。

接下来也许你会问,如果我的整个计算过程中都没有副作用,是否就不会有这个问题,答案是肯定的,试想下刚才那段代码如果没有网络请求,换成一些标准化的计算会怎样。可以肯定的是我们不会出现bug,但是不要忽视的就是其中的运算我们执行了多次。刚才在介绍纯函数的时候,还有一个概念就是引用透明,我们可以在纯函数式语言(例如Haskell)上进行一定的优化,也就是说纯函数的调用在相同参数下的返回值第二次不需要计算,所以在纯函数式语言里面的FRP并没有冷信号的担忧。然而Objective-C语言中并未对纯函数进行优化。所以拥有大规模运算的冷信号对性能也是有一定影响的。

所以如果我们想更好的掌握RAC这个框架,区分冷信号与热信号是十分重要的。

正确理解冷信号与热信号

FRP是一种声明式编程。与传统的命令式编程的区别是声明式只是描述目标性质,让计算机明确目标,而非流程。而声明式编程不一定是FRP所独有的。例如Autolayout就是一种声明式编程的表现,通过编程声明了约束,而框架来做实际的动作。我们的主角RACSignal也是声明式的。请看下面代码:

1
2
3
4
5
6
7
8
9
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
      [subscriber sendNext:@1];
        [subscriber sendCompleted];
    }];

    RACSignal *mappedSignal = [signal map:^id(NSNumber *value) {
      return [NSString stringWithFormat:@"Value is %@", value];
    }];

上述代码的声明了一个信号signalsignal指明了发送“1”这个值后发送结束事件。另外声明了一个信号mappedSignalmappedSignal指明signal的值都进行一个字符串的转换。如果仅仅写到这里,sendNext:map:后面的block其实都没有被执行。

那究竟是何时这些block会执行呢?没错,那就是在订阅之后。订阅mappedSignal之后,还会连带的把signal订阅了。因而预先声明的部分就有了动作。

在搞清楚了信号的声明和信号的订阅之后,再来理解多次订阅的问题。既然创建一个信号只是声明了一段操作,那就说明这个信号本身并无状态可言。可以换个角度来理解,在C语言中,声明了一个函数,这个函数在不同的时间被调用了很多次,函数体肯定会执行相应的次数。因为一个被声明的函数并没有状态,它并不清楚自己被谁在什么时间调用。所以冷信号也是一样,这段操作会在每次订阅的时候都执行,因为冷信号没有状态,它并不清楚自己被谁在什么时候订阅了。

当然一旦信号中存在了副作用,等同与一个修改了全局变量的函数,每次执行的时候的效果就是不一样的了,所以才会出现了前面提到的几个问题。

打个比方,冷信号好比一个剧本,它预先把要做的事情约定好。一旦一个导演说开拍,就是订阅了这个剧本,里面说描述的动作也开始一一被执行,而另一个导演拿着这个剧本开拍,显然和这个导演没有什么关系,拍摄的时期也可以不同。但是有可能有略微的关联,那就是演员可能请的相同的(访问相同的外部变量,或者触发网络请求),那可能要穿插着拍戏。另一方面观众可能也是相同的(最终都经过转换被UI订阅),那就会出现观众看两遍相同的剧情。

一旦片子拍好,放到电视上热播,就变成了热信号。它是有状态的,因为所有的观众都共享了播放的时间,大家都在同一时间观看同一片段。所以,把冷信号变为热信号的本质,就是“广播”,“广播”就是我们也在前面的代码中看到了publishRACMulticastConnection这些操作。

另外举个例子,就是视频直播与视频点播。点播是无状态的,你不需要关心别人看了多少,每次你点播后都是从你需要观看的时间开始播放。而直播是有状态的,你必须要在指定的开播时间观看,一旦错过,就没法看漏掉的节目了。

揭示热信号的本质

好的,回到代码的世界。在RAC中,究竟什么才是热信号呢?冷信号比较常见,map一下就会得到一个冷信号。在RAC的世界中,其实所有的热信号都是一个类的,那就是RACSubject。接下来我们来看看究竟它为什么这么“神奇”。

在RAC2.5文档的框架概述中,有这样一段描述:

A subject, represented by the RACSubject class, is a signal that can be manually controlled.

Subjects can be thought of as the “mutable” variant of a signal, much like NSMutableArray is for NSArray. They are extremely useful for bridging non-RAC code into the world of signals.

For example, instead of handling application logic in block callbacks, the blocks can simply send events to a shared subject instead. The subject can then be returned as a RACSignal, hiding the implementation detail of the callbacks.

Some subjects offer additional behaviors as well. In particular, RACReplaySubject can be used to buffer events for future subscribers, like when a network request finishes before anything is ready to handle the result.

在这段描述中,我们可以看出Subject这三个特点:

  1. Subject是“可变”的。
  2. Subject是非RAC到RAC的一个桥梁。
  3. Subject可以良好的附加行为,例如RACReplaySubject可以缓冲事件给未来的订阅者。

从第三个特点来看,Subject具备将事件缓冲给未来订阅者的能力,那也就说明它是自身是有状态的。由此看来Subject是符合热信号的特点的。为了验证它,我们来做个简单实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
RACSubject *subject = [RACSubject subject];
RACSubject *replaySubject = [RACReplaySubject subject];

[[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
    // Subscriber 1
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscriber 1 get a next value: %@ from subject", x);
    }];
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscriber 1 get a next value: %@ from replay subject", x);
    }];

    // Subscriber 2
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscriber 2 get a next value: %@ from subject", x);
    }];
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscriber 2 get a next value: %@ from replay subject", x);
    }];
}];

[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
    [subject sendNext:@"send package 1"];
    [replaySubject sendNext:@"send package 1"];
}];

[[RACScheduler mainThreadScheduler] afterDelay:1.1 schedule:^{
    // Subscriber 3
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscriber 3 get a next value: %@ from subject", x);
    }];
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscriber 3 get a next value: %@ from replay subject", x);
    }];

    // Subscriber 4
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscriber 4 get a next value: %@ from subject", x);
    }];
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscriber 4 get a next value: %@ from replay subject", x);
    }];
}];

[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
    [subject sendNext:@"send package 2"];
    [replaySubject sendNext:@"send package 2"];
}];

按照解读一下上述代码: 1. 0s时创建subjectreplaySubject这两个subject。 2. 0.1s时订阅者1分别订阅了subjectreplaySubject。 3. 0.1s时订阅者2也分别订阅了subjectreplaySubject。 4. 1s时分别向subjectreplaySubject发送了"send package 1"这个字符串作为。 5. 1.1s时订阅者3分别订阅了subjectreplaySubject。 6. 1.1s时订阅者4也分别订阅了subjectreplaySubject。 7. 2s时再分别向subjectreplaySubject发送了"send package 2"这个字符串作为

接下来看一下输出的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2015-09-28 13:35:22.855 RACDemos[13646:1269269] Start
2015-09-28 13:35:23.856 RACDemos[13646:1269269] Subscriber 1 get a next value: send package 1 from subject
2015-09-28 13:35:23.856 RACDemos[13646:1269269] Subscriber 2 get a next value: send package 1 from subject
2015-09-28 13:35:23.857 RACDemos[13646:1269269] Subscriber 1 get a next value: send package 1 from replay subject
2015-09-28 13:35:23.857 RACDemos[13646:1269269] Subscriber 2 get a next value: send package 1 from replay subject
2015-09-28 13:35:24.059 RACDemos[13646:1269269] Subscriber 3 get a next value: send package 1 from replay subject
2015-09-28 13:35:24.059 RACDemos[13646:1269269] Subscriber 4 get a next value: send package 1 from replay subject
2015-09-28 13:35:25.039 RACDemos[13646:1269269] Subscriber 1 get a next value: send package 2 from subject
2015-09-28 13:35:25.039 RACDemos[13646:1269269] Subscriber 2 get a next value: send package 2 from subject
2015-09-28 13:35:25.039 RACDemos[13646:1269269] Subscriber 3 get a next value: send package 2 from subject
2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 4 get a next value: send package 2 from subject
2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 1 get a next value: send package 2 from replay subject
2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 2 get a next value: send package 2 from replay subject
2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 3 get a next value: send package 2 from replay subject
2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 4 get a next value: send package 2 from replay subject

结合结果可以分析出如下内容:

  1. 22.855s时,测试启动,subjectreplaySubject创建完毕。
  2. 23.856s时,距离启动大约1s后,订阅者1订阅者2同时subject接收到了"send package 1"这个值。
  3. 23.857s时,也是距离启动大约1s后,订阅者1订阅者2同时replaySubject接收到了"send package 1"这个值。
  4. 24.059s时,距离启动大约1.2s后,订阅者3订阅者4同时replaySubject接收到了"send package 1"这个值。注意订阅者3订阅者4并没有从subject接收"send package 1"这个值。
  5. 25.039s时,距离启动大约2.1s后,订阅者1订阅者2订阅者3订阅者4同时subject接收到了"send package 2"这个值。
  6. 25.040s时,距离启动大约2.1s后,订阅者1订阅者2订阅者3订阅者4同时replaySubject接收到了"send package 2"这个值。

只关注subject,根据时间线,我们可以得到下图:

RAC冷热信号1

经过观察不难发现,4个订阅者实际上是共享subject的,一旦这个subject发送了值,当前的订阅者就会同时接收到。由于订阅者3订阅者4的订阅者时间稍晚,所以错过了第一次值的发送。这与冷信号是截然不同的反应。冷信号的图类似下图:

RAC冷热信号1

对比上面两张图,是不是可以发现,subject类似“直播”,错过了就不再处理。而signal类似“点播”,每次订阅都会从头开始。所以我们有理由锁定subject天然就是热信号。

下面再来看看replaySubject,根据时间线,我们能得到另一张图:

RAC冷热信号1

将该图与subject那张图对比会发现,订阅者3订阅者4在订阅后马上接收到了“历史值”。对于订阅者3订阅者4来说,他们只关心“历史的值”而不关心“历史的时间线”,因为实际上12是间隔1s发送的,但是他们接收到的显然不是。举个生动的例子,就好像科幻电影里面主人公穿越时间线后会把所有的回忆快速闪过来到现实一样。(见《X战警:逆转未来》、《蝴蝶效应》)所以我们也有理由锁定replaySubject天然也是热信号。

看到这里,我们终于揭开了热信号的面纱,结论便是:

  1. RACSubject及其子类是热信号
  2. RACSignal排除RACSubject类以外的是冷信号

如何将一个冷信号转化成热信号——广播

冷信号与热信号的本质区别在于是否保持状态,冷信号的多次订阅是不保持状态的,而热信号的多次订阅可以保持状态。所以一种将冷信号转换为热信号的方法就是,将冷信号订阅,取得的每一个值再通过RACSbuject发送出去。

看一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    NSLog(@"Cold signal be subscribed.");
    [[RACScheduler mainThreadScheduler] afterDelay:1.5 schedule:^{
        [subscriber sendNext:@"A"];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{
        [subscriber sendNext:@"B"];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:5 schedule:^{
        [subscriber sendCompleted];
    }];

    return nil;
}];

RACSubject *subject = [RACSubject subject];
NSLog(@"Subject created.");

[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
    [coldSignal subscribe:subject];
}];

[subject subscribeNext:^(id x) {
    NSLog(@"Subscribe 1 recieve value:%@.", x);
}];

[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscribe 2 recieve value:%@.", x);
    }];

执行顺序是这样的:

  1. 创建一个冷信号:coldSignal。该信号声明了“订阅后1.5秒发送‘A’,3秒发送’B’,5秒发送完成事件”。
  2. 创建一个RACSubject:subject
  3. 在2秒后使用这个subject订阅coldSignal
  4. 立即订阅这个subject
  5. 4秒后订阅这个subject

如果所料不错的话,通过订阅这个subject并不会引起coldSignal重复执行block的内容。我们来看下结果:

1
2
3
4
5
2015-09-28 19:36:45.703 RACDemos[14110:1556061] Subject created.
2015-09-28 19:36:47.705 RACDemos[14110:1556061] Cold signal be subscribed.
2015-09-28 19:36:49.331 RACDemos[14110:1556061] Subscribe 1 recieve value:A.
2015-09-28 19:36:50.999 RACDemos[14110:1556061] Subscribe 1 recieve value:B.
2015-09-28 19:36:50.999 RACDemos[14110:1556061] Subscribe 2 recieve value:B.

参考时间线,会得到下图: RAC冷热信号4

解读一下其中的要点: 1. subject是从一开始就创建好的,等到2s后便开始订阅coldSignal。 2. subscribe 1subject创建后就开始订阅的,但是第一个接收时间与subject接收coldSignal第一个值的时间是一样的。 3. subscribe 2subject创建4s后开始订阅的,所以只能接收到第二个值。

通过观察可以确定,subject就是coldSignal转化的热信号。所以使用RACSubject来将冷信号转化为热信号是可行的。

当然,使用这种RACSubject来订阅冷信号得到热信号的方式还是有一些小的瑕疵的。例如subject的订阅者提前终止了订阅,而subject并不能终止对coldSignal的订阅。(RACDisposable是一个比较大的话题,我计划在其他的文章中详细阐述它,也希望感兴趣的同学自己来理解。)所以RAC库中对于冷信号转化成热信号有如下标准的包装:

1
2
3
4
5
- (RACMulticastConnection *)publish;
- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACSignal *)replay;
- (RACSignal *)replayLast;
- (RACSignal *)replayLazily;

这5个方法中,最为重要的就是- (RACMulticastConnection *)multicast:(RACSubject *)subject;这个方法了,其他几个方法也是间接调用它的。我们来看看它的真相:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/// implementation RACSignal (Operations)
- (RACMulticastConnection *)multicast:(RACSubject *)subject {
  [subject setNameWithFormat:@"[%@] -multicast: %@", self.name, subject.name];
  RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject];
  return connection;
}

/// implementation RACMulticastConnection

- (id)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject {
  NSCParameterAssert(source != nil);
  NSCParameterAssert(subject != nil);

  self = [super init];
  if (self == nil) return nil;

  _sourceSignal = source;
  _serialDisposable = [[RACSerialDisposable alloc] init];
  _signal = subject;
  
  return self;
}

#pragma mark Connecting

- (RACDisposable *)connect {
  BOOL shouldConnect = OSAtomicCompareAndSwap32Barrier(0, 1, &_hasConnected);

  if (shouldConnect) {
      self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];
  }

  return self.serialDisposable;
}

- (RACSignal *)autoconnect {
  __block volatile int32_t subscriberCount = 0;

  return [[RACSignal
      createSignal:^(id<RACSubscriber> subscriber) {
          OSAtomicIncrement32Barrier(&subscriberCount);

          RACDisposable *subscriptionDisposable = [self.signal subscribe:subscriber];
          RACDisposable *connectionDisposable = [self connect];

          return [RACDisposable disposableWithBlock:^{
              [subscriptionDisposable dispose];

              if (OSAtomicDecrement32Barrier(&subscriberCount) == 0) {
                  [connectionDisposable dispose];
              }
          }];
      }]
      setNameWithFormat:@"[%@] -autoconnect", self.signal.name];
}

代码比较短,大概来说明一下: 1. 当RACSignal类的实例调用- (RACMulticastConnection *)multicast:(RACSubject *)subject时,创建一个RACMulticastConnection实例,以selfsubject作为构造参数。 2. RACMulticastConnection构造的时候,保存sourcesubject作为成员变量,创建一个RACSerialDisposable对象。 3. 当RACMulticastConnection类的实例调用- (RACDisposable *)connect这个方法的时候,判断是否是第一次,如果是的话用_signal这个成员变量来订阅sourceSignal之后返回self.serialDisposable;否则直接返回self.serialDisposable。 4. RACMulticastConnectionsignal只读属性,就是热信号,订阅它就可以。它会在- (RACDisposable *)connect第一次调用后,根据sourceSignal的订阅结果来传递事件。 5. 想要确保第一次订阅就能成功订阅sourceSignal,可以使用- (RACSignal *)autoconnect这个方法,它保证了第一个订阅者触发了sourceSignal的订阅,也保证了当返回的信号所有订阅者都关闭连接后sourceSignal被正确关闭连接。

所以,正确的使用可以像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    NSLog(@"Cold signal be subscribed.");
    [[RACScheduler mainThreadScheduler] afterDelay:1.5 schedule:^{
        [subscriber sendNext:@"A"];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{
        [subscriber sendNext:@"B"];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:5 schedule:^{
        [subscriber sendCompleted];
    }];


    return nil;
}];

RACSubject *subject = [RACSubject subject];
NSLog(@"Subject created.");

RACMulticastConnection *multicastConnection = [coldSignal multicast:subject];
RACSignal *hotSignal = multicastConnection.signal;

[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
    [multicastConnection connect];
}];

[hotSignal subscribeNext:^(id x) {
    NSLog(@"Subscribe 1 recieve value:%@.", x);
}];

[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscribe 2 recieve value:%@.", x);
    }];
}];

或者这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    NSLog(@"Cold signal be subscribed.");
    [[RACScheduler mainThreadScheduler] afterDelay:1.5 schedule:^{
        [subscriber sendNext:@"A"];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{
        [subscriber sendNext:@"B"];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:5 schedule:^{
        [subscriber sendCompleted];
    }];


    return nil;
}];

RACSubject *subject = [RACSubject subject];
NSLog(@"Subject created.");

RACMulticastConnection *multicastConnection = [coldSignal multicast:subject];
RACSignal *hotSignal = multicastConnection.autoconnect;

[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscribe 1 recieve value:%@.", x);
    }];
}];


[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscribe 2 recieve value:%@.", x);
    }];
}];

以上的两种写法都可以得到和之前相同的结果。

下面再来看看其他几个方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/// implementation RACSignal (Operations)
- (RACMulticastConnection *)publish {
  RACSubject *subject = [[RACSubject subject] setNameWithFormat:@"[%@] -publish", self.name];
  RACMulticastConnection *connection = [self multicast:subject];
  return connection;
}

- (RACSignal *)replay {
  RACReplaySubject *subject = [[RACReplaySubject subject] setNameWithFormat:@"[%@] -replay", self.name];

  RACMulticastConnection *connection = [self multicast:subject];
  [connection connect];

  return connection.signal;
}

- (RACSignal *)replayLast {
  RACReplaySubject *subject = [[RACReplaySubject replaySubjectWithCapacity:1] setNameWithFormat:@"[%@] -replayLast", self.name];

  RACMulticastConnection *connection = [self multicast:subject];
  [connection connect];

  return connection.signal;
}

- (RACSignal *)replayLazily {
  RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]];
  return [[RACSignal
      defer:^{
          [connection connect];
          return connection.signal;
      }]
      setNameWithFormat:@"[%@] -replayLazily", self.name];
}

这几个方法的时间都相当简单,只是为了简化代码,具体说明一下: 1. - (RACMulticastConnection *)publish就是帮忙创建了RACSubject。 2. - (RACSignal *)replay就是用RACReplaySubject来作为subject,并立即执行connect操作,返回connection.signal。其作用是上面提到的replay功能,既后来的订阅者可以收到历史值。 3. - (RACSignal *)replayLast就是用Capacity为1的RACReplaySubject来替换- (RACSignal *)replaysubject。其作用是使后来订阅者只收到最后的历史值。 4.- (RACSignal )replayLazily- (RACSignal )replay的区别就是replayLazily会在第一次订阅的时候才订阅sourceSignal`。

现在看下之前第二章那个业务场景的例子,其实修改的方法很简单,就是在网络获取的fetchData这个信号后面,增加一个replayLazily变换,就不会出现网络请求重发6次的问题了。

修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    self.sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://api.xxxx.com"]];

    self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer];
    self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];

    @weakify(self)
    RACSignal *fetchData = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        @strongify(self)
        NSURLSessionDataTask *task = [self.sessionManager GET:@"fetchData" parameters:@{@"someParameter": @"someValue"} success:^(NSURLSessionDataTask *task, id responseObject) {
            [subscriber sendNext:responseObject];
            [subscriber sendCompleted];
        } failure:^(NSURLSessionDataTask *task, NSError *error) {
            [subscriber sendError:error];
        }];
        return [RACDisposable disposableWithBlock:^{
            if (task.state != NSURLSessionTaskStateCompleted) {
                [task cancel];
            }
        }];
    }] replayLazily];  // modify here!!

    RACSignal *title = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {
        if ([value[@"title"] isKindOfClass:[NSString class]]) {
            return [RACSignal return:value[@"title"]];
        } else {
            return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]];
        }
    }];

    RACSignal *desc = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {
        if ([value[@"desc"] isKindOfClass:[NSString class]]) {
            return [RACSignal return:value[@"desc"]];
        } else {
            return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]];
        }
    }];

    RACSignal *renderedDesc = [desc flattenMap:^RACStream *(NSString *value) {
        NSError *error = nil;
        RenderManager *renderManager = [[RenderManager alloc] init];
        NSAttributedString *rendered = [renderManager renderText:value error:&error];
        if (error) {
            return [RACSignal error:error];
        } else {
            return [RACSignal return:rendered];
        }
    }];

    RAC(self.someLablel, text) = [[title catchTo:[RACSignal return:@"Error"]]  startWith:@"Loading..."];
    RAC(self.originTextView, text) = [[desc catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."];
    RAC(self.renderedTextView, attributedText) = [[renderedDesc catchTo:[RACSignal return:[[NSAttributedString alloc] initWithString:@"Error"]]] startWith:[[NSAttributedString alloc] initWithString:@"Loading..."]];

    [[RACSignal merge:@[title, desc, renderedDesc]] subscribeError:^(NSError *error) {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:error.domain delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alertView show];
    }];

当然,这样修改,仍然有许多计算上的浪费,例如将fetchData转换为title的block会执行多次,将fetchData转换为desc的block也会执行多次。但是由于这些block都是无副作用的,计算量又小,可以忽略不计。

至此,我们终于揭开RAC中冷信号与热信号的全部面纱,也知道如何使用了。希望此文可以让大家更好的了解RAC,减少使用RAC遇到的误区。谢谢大家。

从另一个角度介绍下Block

群里有个小伙伴问我block的理解,我想网上那么多blog都写过iOS的block的介绍,说明,用法,如果还是不能理解,那就换个角度吧。所以,今天我们来聊一聊Block的前世今生,不谈block如何定义,不谈block有哪些坑,只谈它是怎么来的。

首先block的使用,真的不必多说,想必大家google后也都会用。问题就在于,为什么要有block?有人觉得block方便,到底方便在哪里呢?这一切要从函数指针这个很老的概念谈起了。

在C时代,面向过程一度成为程序开发的主流,在没有对象化的程序设计中,我们难免写出如下的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extern const char *GetMenu0();
extern const char *GetMenu1();
extern const char *GetMenu2();
extern const char *GetMenu3();
extern const char *GetMenu4();
extern const char *GetMenu5();



const char *GetMenuShow(int pos) {
    switch (pos) {
        case 0: return GetMenu0();
        case 1: return GetMenu1();
        case 2: return GetMenu2();
        case 3: return GetMenu3();
        case 4: return GetMenu4();
        case 5: return GetMenu5();
        default:
            return NULL;
    }
}

随着程序的复杂度提高,这样的程序变得越来越长,这时函数指针可以来帮忙,于是函数就变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
extern const char *GetMenu0();
extern const char *GetMenu1();
extern const char *GetMenu2();
extern const char *GetMenu3();
extern const char *GetMenu4();
extern const char *GetMenu5();

typedef const char *(*MenuMethodType)();

static MenuMethodType g_methods[] = {
  &GetMenu0,
  &GetMenu1,
  &GetMenu2,
  &GetMenu3,
  &GetMenu4,
  &GetMenu5
};


const char *GetMenuShow(int pos) {
    if (pos < 0 || pos >= (sizeof(g_methods) / sizeof(MenuMethodType))) {
        return NULL;
    }
    return g_methods[pos]();
}

这种写法也叫做跳转表,好处是以后GetMenu*这种函数的增长可以放到GetMenuShow这个函数外,减少了耦合。函数指针的另一个妙用就是回调函数,这个非常的普遍,也不需要再举例子了。

函数指针给我们带来的新的开发思想,就是行为的变量化,因此,我们可以将不同的行为封装到统一的流程之外,作为可替换的组件。总之,它允许你把可变化的行为,注入到稳定的过程中,我们便获得了更好的扩展,把开发的中心放到变化而不是重复上。

到了OC时代,OC有了一种比函数指针还高效而简单的东西,那就是selector。这个被称为选择器的工具,不仅可以让我们得到函数指针的一切便利,还可以动态的替换其指向的内容。于是我们有了很多addTarget:action:这样的API,使得我们可以把行为注入到已经非常稳定的Cocoa或CocoaTouch框架中。

虽然有了一定的便利,但是程序员是不容易满足的。我们逐渐发现,这种基于action的写法有时很麻烦。主要是以下几点:

  1. 就是很多时候,你注入的内容可能就一次,搞一个函数或者方法,浪费了不少的时间。
  2. 你还要起名字,要知道,Phil Karlton就说过:“在计算机科学领域,有两大难题,如何验证缓存和如何给各种东西命名。”
  3. 使得你原本可以在一个函数里实现的逻辑,分散到不同的部分,你难以专注的一次把你的逻辑写完。

这时,我们的主角block就来帮助大家了。

其实block还有很多别名,其中一个就是匿名函数。利用block,你可以在一个函数中,写上一小段代码,不用起名字,就可以传递过去。函数指针的用法,几乎都可以用匿名函数来替换。一举解决了碎片化,命名等问题。我们便有了这样的代码:

1
2
3
4
5
6
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:@"http://example.com/resources.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"JSON: %@", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    NSLog(@"Error: %@", error);
}];

贪婪的程序员不仅仅满足于少起个名字和把代码写到一起,一旦程序员们发现,把一个函数写在另一个函数里很爽,就开始频繁的尝试。这时,又发现了几个小问题(以下仅是匿名函数的问题,block解决了这些问题):

  1. 匿名函数,到底还是一个函数,这个括号外面世界的变量,是不可以在里面使用的。
  2. 由于只能传递一个函数,我们就没有了可操作的对象(即没有了self)。

其实这两个是一样的问题,都是因为没有变量的传递。如果可以把self传递下来,第二个问题也解决了。

想要解决这个问题,最简单的方法就是搞到全局域,于是代码就写成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int g_sum = 0;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    g_sum = 0;
    [@[@1, @2, @3] enumerateObjectsUsingBlock:^(NSNumber *obj, NSUInteger idx, BOOL *stop) {
        int val = [obj intValue];
        g_sum += val;
    }];
    NSLog(@"sum = %d", g_sum);

    // Override point for customization after application launch.
    return YES;
}

把想要在匿名函数中的部分先存到全局变量,然后再到匿名函数中取出来。我们原本想解决的碎片化问题,又回来了。这样情况下,我们不得不搞出好多的全局变量,给全局变量起名字的问题也回来了。聪明的工程师们很快发现了,这种传递有着很规律的行为,于是把这些行为封装起来,做成库。这种手段叫做“闭包”,block的又一个名字。

把外面的对象,包在匿名函数中,封装起来,以备不时之需。OC利用编译器,在静态检查的时候,把用到的外部变量,都封装到block中。

这使得匿名函数,有了新的活力。在大量的尝试后,发现这简直就是一种神奇,闭包不单使得你的逻辑更加的紧凑,还使得开发变得越来越有趣。nodejs尝试用大量的闭包铸成了一种单线程异步的神奇的库。ReactiveCocoa也用block改变了大家开发iOS的思路。

有了block,我们可以更好的把变化抽取出来,可以更专注的实现逻辑,将异步的,碎片化的需求,快速的整合到一起。相比这些优点,block稍许复杂的语法,和一些可能出现的问题,是可以被原谅的。swift中,我们看到更多的闭包,可以看出block的写法对于开发有着多么深远的影响。

此篇只是一个引子,block有很多需要学习的地方,用好容易,用得精巧,还需要大家更多的开阔思维。

关于一个小问题引发的感慨

首先先来看这样一个问题,大家在向后翻之前,先思考一下:

现在有一本书,里面纸的颜色不同,现在让你想办法找出,这里面有多少种颜色,每种颜色多少张纸?

这个小问题,我问过很多个人,我发现这个问题程序员回答的结果五花八门。我先来列举一下:

  1. 将书看成数组,放到set里面,就可以知道有多少种颜色了,然后用两个for循环找出每个颜色有多少张
  2. 我没有懂这个问题,你再说明一下?
  3. 我想想啊~恩,要有个数组,来存颜色,不对,应该是个字典,用颜色做key,个数做value,然后……………………
  4. ………………不知道
  5. 额,好像我没有见过这样的例子啊。
  6. 这是数据结构的问题么?是问最优解么?我来想一想…………额……
  7. 首先写一个程序………………,然后……

上面的回答都不大令人满意,下面的回答有趣很多:

  1. 把书一页页扯下来,然后一样颜色的堆一起,最后查一查数
  2. 测一页纸的厚度,然后从书的侧面来测各个颜色的厚度
  3. 用个小本子记下出现的颜色,一页页翻过去,然后在颜色上画正字

最后的答案应该说相当准确和简单了,它很好的解答了这个问题。可实施性很高,而复杂度很低。

大家有没有想过,为什么我们程序员在思考这个问题的时候,会这么复杂?那么我就来一一分析一下:

首先是回答不知道,或者半天没有憋出来的人,这种人不在少数。这类人的问题就是,老是把问题想的很复杂,面对这样的一个问题,老是觉得不够明确,没有开发环境,没有工具,没有API,还可以没有好多。所以无从下手,似乎解决问题就一定要有趁手API,有设备,有算法,有开发环境。当这些没有的时候,很多的程序员就解决不了问题。

然后再来说一说那些专门寻求最优解的人,这些人往往最后没有回答了问题,只是陷在求解的这样一个过程中。他们想建立更好的模型,具有更好的扩展性,却很少注意到问题的本质和场景。你可以做一个拾取颜色的机器人,然后计数,但是真的有必要么?很多时候,我们直观的解决问题就好了。等待问题扩大,再来想复用和扩展,不是更好么?

最后是这些,用set,字典给出答案的人,首先不能说他们是错的。但是,面对这样一个简单的问题,你真的有必要建立一个工程,写上几行代码来搞?而且,这要怎么实施呢?你的程序怎么把书当输入弄进去?程序员首先要学会思考,然后再编程,如果这样的问题,因为是我问的,就去找数据结构,找算法。并没有把解决问题放在首要,而是拘泥于已学的知识。

至于那些没有找到类似的例子,回去google和百度也没找到答案的同学,已经算是重度病症了。似乎开发的久了,很多程序员已经不会了独立思考,对他们来说,解决问题不过是google和百度,不过是找以前的例子。这到底是人类的进步,还是一种退化了呢?

那个测量厚度的回答,很有意思。这个答案完全没有提及程序,的确是分析之后的结果。但是有几点小问题,第一个问题就是,这个回答是不可实施的,因为没有办法很精确的测量纸的厚度,另外,纸的厚度也不尽相同。第二,这书如果是新的还好,中间空隙少,要是旧的呢,那不是测量出来的比实际多好多页么?第三,一切的假设都在同一颜色的页连在一起上,实际上题目中并没有说是连在一起啊,或者说,这个答案,解决不了随机颜色的问题。

这也侧面反映了我们程序员有时分析问题,不够全面,对可能的输入情况判断不全的缺点毛病。

那个把书撕下来是我问完这个问题,最先想到的答案,直观,简单。是因为我故意没有说这个书让不让破坏。可以在现有的条件下,用撕开书页来解决问题的,应该算另辟蹊径了。这不是一个标准答案,不过我还是觉得这样回答很有趣。

程序员是一群使用计算机来解决问题的人,大致可以分两种,一种是以知识来解决问题的,一种是以思维来解决问题的。而前者占大多数,这也让这个浮躁的IT圈蒙上了一层灰暗,这也是为什么公司甄选人才的时候,更看重经验、学历、背景公司的主要原因。因为大多数的人,是靠着做过,了解过很多的知识来解决问题的。知识的多寡正好体现了一个人的价值。然后,真正改变程序界的,是后一小撮人,他们用思维来解决问题,旁征博引,创新,并且根据问题来分析解决方案,找到很多简单且有效的方法。

我相信,很多的程序员一开始都是很聪明的,但是这个浮躁的圈子,这些复杂的概念,让我们渐渐忘却了思考,忘却了自己的能力,分析问题,解决问题,动手的能力。当我们遇到了问题,只要抛开我们固有的知识,抛开google百度,抛开API,抛开数据结构,就用心去思考,一定可以得到更好的答案的。

那么把这个问题延伸开来,你会发现很多相似的问题:

  1. 班上要选举班长,每个人写了一个名字,最后要统计谁最高和大家的票数
  2. 你有一个字符串,试着统计里面每个字符出现的次数
  3. 你有一个关于老师数据库,里面存着老师户籍的数据,试着分析老师的来源地区的分布

其实我们还可以举很多例子,能把复杂的问题,想象得简单,这本身就是大家有的一种能力。只是因为我们的知识在增长,工具在进化,而我们变得懒惰了。很多时候,我们都沦为了罗列API的产业工人。大家不觉得悲哀么?

编程思想来源于思想,希望大家不要忘了本质,我们真的可以更多的发挥自己的潜质!

2014-8-28 周四 晴

不要忽视C语言

看到过很多人发表过对C语言的抨击,称其看起来很难看,面向过程是一种旧时代的产物。

我想说的是,请不要忽视C语言。

几乎所有的学校都会以C语言作为一个入门的语言。我觉得有几点需要思考:

  1. C的确是很“入门”的语言,因为他的学习曲线较为轻松,初学的时候既没有大量的库和框架,也没有复杂的语法概念(指针稍微麻烦一些),还没有很复杂的开发环境。面向过程的程序是最直观和易了解的。

  2. C语言很利于锻炼一个人的开发思想,实际解决问题的能力,C语言的考核应该是纯粹的能力考核,而不是对各种库和框架的熟悉程度。

  3. C语言是实现操作系统和数据结构的最佳语言,首先它没有太多的库的包袱,其次它可以方便的访问硬件。它不会让你觉得实现动态数组是很无聊的事(Ruby、Python党绝对不会想自己实现可变数组)。

  4. C语言被老师给认为太简单了,很多的教师和教学机构只让最菜的老师来教C语言,这直接导致大家没有学好C语言。

  5. C语言最精髓的部分被砍掉了,很多的学校C语言的学时通常不够,所以结构体、位段、高级指针、预编译等通常都被阉割了。第4条所说的老师们,又碍于面子,对学生提出的”指针的指针应该怎么用?”“为什么int a[3][5]和int **b不能互相转化?”的问题回答通常是“这个用不到。”。这就导致大家对C的普遍认知存在问题。

  6. C语言的教学目的是让入门者可以更快的掌握计算机开发的一些原理,并快速实践。但C语言绝不是只能面向过程设计的,几乎所有的C核心代码都是面向对象设计的,例如Linux、Win32内核、大家在iOS中的各种核心库如CoreGraphics、CoreText等。对于iOS开发者来说,如果你稍多了解就可以知道你是可以用C来生成ObjC的类和方法的,而这些几乎没有老师交代过。

首先背负着沉重的教育背景,C已经让绝大部分人忽视了,接下来的就业环节又让C被更加的忽视:

  1. 没有多少公司用C

  2. 很多的公司和老板觉得C没啥用,又干不了什么

  3. 搞Java、Ruby、Objective-C的同学都看起来不错,搞C的同学都在嵌入式公司赚少量的钱(呵呵,我当年就是)

最后我来说一说了解C的必要性:

  1. 对内存和硬件的操作,是目前绝大部分高级语言所隐藏的,只有坚实C背景的同学,才可以考虑的更多,如果你想对原理有更多的了解,那么C其实是不可绕过的一个环节。

  2. 想要缔造一个新的系统和语言,没有C是不行滴(或许C++勉强)。

  3. 不管是OC的消息系统,Java的自动回收,Ruby的动态类扩充,这些花哨且时髦的东西,其实都可以用C来实现,而且很多都是用C来实现的,如果你想自己实施,看C吧。

  4. 破解和分析程序,C可以起到很大的作用。C是可以人为创建Bug来破坏一个系统滴。

最后我引用我的一个大牛朋友的话:“请不要忽视C语言,它上可九天摘月,下可五洋捉鳖”。

工具链

每一个老开发者,都有很中意的一些工具,对于新的开发者来说,应该是一个很好的指引。我虽然不算什么老开发者,但是也有一定的心得。所以今天就分享一下我对于iOS所用到的工具吧。

最先介绍的是Mac下开发相关的App:

  1. 同步助手——-iPhone同步
  2. AirServer—–用于将苹果设备投射到Mac屏幕上
  3. Alfred 2——好用的全局快捷菜单,比较爱用它当计算器
  4. Android File Transfer—-Android同步,偶尔需要用到
  5. BetterZip—–压缩解压
  6. Genymotion—–Android虚拟机,用来对比Android效果
  7. iTerm—–终端的最佳替换
  8. MarkMan——量图工具(Adobe Air环境)
  9. Mou ——Markdown编辑工具,正在用
  10. PaintCode——OS X、iOS控件自绘工具
  11. plistedit pro—–plist编辑工具
  12. Reveal——iOS视图调试工具
  13. Simpholders——iOS模拟器应用目录快速查询
  14. Source Tree—–Git客户端
  15. TextMate—–文本编辑
  16. VirtualBox—–虚拟机 用作Genymotion支持
  17. LICEcap——–Git录屏软件,用来跟别人解释发生了什么
  18. Docs for Xcode—–一些开源项目的文档集成
  19. Charles——-用来调试网络情况和Hook客户端调用结果的

然后是命令行工具:

  1. Homebrew—–一切都靠他了
  2. RVM—–Ruby版本管理工具
  3. CocoaPods—-iOS、OS X包管理器
  4. Oh-my-zsh——漂亮的Zsh shell
  5. xctool—-自动化集成
  6. vim—–文本编辑器
  7. calabash—–集成、UI测试工具
  8. mogenerator——-用来生成CoreData的子类,比Xcode实用

Xcode的插件:

  1. Alcatraz—-插件管理器,其他的都靠它了
  2. AdjustFontSize—–文本快速放大缩小
  3. FuzzyAutocomplete——补全工具,不过这个一开,有点补全太多了
  4. HOStringSense—–NSString的长度测量,文本编码
  5. KSImageNamed—-[UIImage imageNamed]的时候,自动出现图片选择
  6. Lin——-NSLocalizedString的快速增删改查 7.OMColorSense——颜色插件
  7. QuickLocalization—–NSLocalizedString快速生成
  8. RevealPlugin—–快速打开Reveal
  9. Singleton—-单例源代码模板
  10. Specta—–specta测试代码模板
  11. VVDocumenter-Xcode———代码注释生成器
  12. XVim——Xcode的Vim支持
  13. SCXcodeSwitchExpander——为枚举自动生成switch-case对

最后是我在iOS中喜欢用的库:

  1. ReactiveCocoa——-著名的RAC框架
  2. ReactiveViewModel——RAC支持MVVM的辅助类
  3. libextobjc—–OC运行时扩展
  4. WYPopoverController—-iPhone的Popover弹出框
  5. pop—–Facebook的动画库
  6. MBProgressHUD—-HUD弹出信息
  7. PinYin4Objc—-反解拼音库
  8. MNCalendarView@aceontech—-日历
  9. MTDates—-NSDate的N多扩展
  10. Mantle—-好的简易Model
  11. SDWebImage—–图片缓存
  12. XHImageViewer—-图片浏览器,挺多bug,有空给改改
  13. UI7Kit—-iOS5 iOS6的iOS7扁平化界面快速替换,注意有坑
  14. AFNetworking——网络库
  15. Tweaks——Facebook的调试工具,晃一晃微调参数
  16. UALogger—–Log工具
  17. Calabash—–自动化测试工具
  18. Nocilla—-网络模拟工具
  19. Reveal-iOS-SDK—–视图调试工具
  20. Expecta—–单元测试断言宏
  21. Specta—-单元测试的DSL
  22. OCMock——OC的Mock测试工具
  23. MagicalRecord——CoreData高级辅助工具
  24. FastAnimationWithPOP——我自己写的动画库

希望能对大家有所帮助,也欢迎和我交流。