对象间的通讯

对象之间需要通信,这也是所有软件的基础。再非凡的软件也需要通过对象通信来完成复杂的目标。本章将深入讨论一些设计概念,以及如何依据这些概念来设计出良好的架构。

Block

Block 是 Objective-C 版本的 lambda 或者 closure(闭包)。使用 block 定义异步接口:

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion;

当你定义一个类似上面的接口的时候,尽量使用一个单独的 block 作为接口的最后一个参数。把需要提供的数据和错误信息整合到一个单独 block 中,比分别提供成功和失败的 block 要好。

以下是你应该这样做的原因:

  • 通常这成功处理和失败处理会共享一些代码(比如让一个进度条或者提示消失);
  • Apple 也是这样做的,与平台一致能够带来一些潜在的好处;
  • block 通常会有多行代码,如果不作为最后一个参数放在后面的话,会打破调用点;
  • 使用多个 block 作为参数可能会让调用看起来显得很笨拙,并且增加了复杂性。

看上面的方法,完成处理的 block 的参数很常见:第一个参数是调用者希望获取的数据,第二个是错误相关的信息。这里需要遵循以下两点:

  • objects 不为 nil,则 error 必须为 nil
  • objects 为 nil,则 error 必须不为 nil

因为调用者更关心的是实际的数据,就像这样:

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion {
    if (objects) {
        // do something with the data
    }
    else {
        // some error occurred, 'error' variable should not be nil by contract
    }
}

此外,Apple 提供的一些同步接口在成功状态下向 error 参数(如果非 NULL) 写入了垃圾值,所以检查 error 的值可能出现问题。

深入 Block

一些关键点:

  • block 是在栈上创建的
  • block 可以复制到堆上
  • Block会捕获栈上的变量(或指针),将其复制为自己私有的const(变量)。
  • (如果在Block中修改Block块外的)栈上的变量和指针,那么这些变量和指针必须用__block关键字申明(译者注:否则就会跟上面的情况一样只是捕获他们的瞬时值)。

如果 block 没有在其他地方被保持,那么它会随着栈生存并且当栈帧(stack frame)返回的时候消失。仅存在于栈上时,block对对象访问的内存管理和生命周期没有任何影响。

如果 block 需要在栈帧返回的时候存在,它们需要明确地被复制到堆上,这样,block 会像其他 Cocoa 对象一样增加引用计数。当它们被复制的时候,它会带着它们的捕获作用域一起,retain 他们所有引用的对象。

如果一个 block引用了一个栈变量或指针,那么这个block初始化的时候会拥有这个变量或指针的const副本,所以(被捕获之后再在栈中改变这个变量或指针的值)是不起作用的。(译者注:所以这时候我们在block中对这种变量进行赋值会编译报错:Variable is not assignable(missing __block type specifier),因为他们是副本而且是const的.具体见下面的例程)。

当一个 block 被复制后,__block 声明的栈变量的引用被复制到了堆里,复制完成之后,无论是栈上的block还是刚刚产生在堆上的block(栈上block的副本)都会引用该变量在堆上的副本。

// 下面代码是译者加的
CGFloat blockInt = 10;
void (^playblock)(void) = ^{
    NSLog(@"blockInt = %zd", blockInt);
};
blockInt++;
playblock();
...
// 结果为:blockInt = 10

用 LLDB 来展示 block 是这样子的:

最重要的事情是 __block 声明的变量和指针在 block 里面是作为显示操作真实值/对象的结构来对待的。

block 在 Objective-C 的 runtime(运行时) 里面被当作一等公民对待:他们有一个 isa 指针,一个类也是用 isa 指针在Objective-C 运行时来访问方法和存储数据的。在非 ARC 环境肯定会把它搞得很糟糕,并且悬挂指针会导致 crash。__block 仅仅对 block 内的变量起作用,它只是简单地告诉 block:

嗨,这个指针或者原始的类型依赖它们在的栈。请用一个栈上的新变量来引用它。我是说,请对它进行双重解引用,不要 retain 它。 谢谢,哥们。

如果在定义之后但是 block 没有被调用前,对象被释放了,那么 block 的执行会导致 crash。 __block 变量不会在 block 中被持有,最后... 指针、引用、解引用以及引用计数变得一团糟。

self 的循环引用

当使用代码块和异步分发的时候,要注意避免引用循环。 总是使用 weak 来引用对象,避免引用循环。(译者注:这里更为优雅的方式是采用影子变量@weakify/@strongify 这里有更为详细的说明) 此外,把持有 block 的属性设置为 nil (比如 self.completionBlock = nil) 是一个好的实践。它会打破 block 捕获的作用域带来的引用循环。

  • 例子:
    • __weak __typeof(self) weakSelf = self;
      [self executeBlock:^(NSData *data, NSError *error) {
          [weakSelf doSomethingWithData:data];
      }];
      
  • 不要这样:
    • [self executeBlock:^(NSData *data, NSError *error) {
          [self doSomethingWithData:data];
      }];
      
  • 多个语句的例子:
    • __weak __typeof(self)weakSelf = self;
      [self executeBlock:^(NSData *data, NSError *error) {
          __strong __typeof(weakSelf) strongSelf = weakSelf;
          if (strongSelf) {
              [strongSelf doSomethingWithData:data];
              [strongSelf doSomethingWithData:data];
          }
      }];
      
  • 不要这样:
    • __weak __typeof(self)weakSelf = self;
      [self executeBlock:^(NSData *data, NSError *error) {
          [weakSelf doSomethingWithData:data];
          [weakSelf doSomethingWithData:data];
      }];
      

你应该把这两行代码作为 snippet 加到 Xcode 里面并且总是这样使用它们。

__weak __typeof(self)weakSelf = self;
__strong __typeof(weakSelf)strongSelf = weakSelf;

这里我们来讨论下 block 里面的 self 的 __weak__strong 限定词的一些微妙的地方。简而言之,我们可以参考 self 在 block 里面的三种不同情况。

  1. 直接在 block 里面使用关键词 self
  2. 在 block 外定义一个 __weak 的 引用到 self,并且在 block 里面使用这个弱引用
  3. 在 block 外定义一个 __weak 的 引用到 self,并在在 block 内部通过这个弱引用定义一个 __strong 的引用。

方案 1. 直接在 block 里面使用关键词 self

如果我们直接在 block 里面用 self 关键字,对象会在 block 的定义时候被 retain,(实际上 block 是 copied 但是为了简单我们可以忽略这个)。一个 const 的对 self 的引用在 block 里面有自己的位置并且它会影响对象的引用计数。如果这个block被其他的类使用并且(或者)彼此间传来传去,我们可能想要在 block 中保留 self,就像其他在 block 中使用的对象一样. 因为他们是block执行所需要的.

dispatch_block_t completionBlock = ^{
    NSLog(@"%@", self);
}
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:completionHandler];

没啥大不了。但是如果通过一个属性中的 self 保留 了这个 block(就像下面的例程一样),对象( self )保留了 block 会怎么样呢?

self.completionHandler = ^{
    NSLog(@"%@", self);
}
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:self.completionHandler];

这就是有名的 retain cycle, 并且我们通常应该避免它。这种情况下我们收到 CLANG 的警告:

Capturing 'self' strongly in this block is likely to lead to a retain cycle (在 block 里面发现了 `self` 的强引用,可能会导致循环引用)

所以 __weak 就有用武之地了。

方案 2. 在 block 外定义一个 __weak 的 引用到 self,并且在 block 里面使用这个弱引用

这样会避免循坏引用,也是通常情况下我们的block作为类的属性被self retain 的时候会做的。

__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
    NSLog(@"%@", weakSelf);
};
MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:self.completionHandler];

这个情况下 block 没有 retain 对象并且对象在属性里面 retain 了 block 。所以这样我们能保证了安全的访问 self。 不过糟糕的是,它可能被设置成 nil 的。问题是:如何让 self 在 block 里面安全地被销毁。

考虑这么个情况:block 作为属性(property)赋值的结果,从一个对象被复制到另一个对象(如 myController),在这个复制的 block 执行之前,前者(即之前的那个对象)已经被解除分配。

下面的更有意思。

方案 3. 在 block 外定义一个 __weak 的 引用到 self,并在在 block 内部通过这个弱引用定义一个 __strong 的引用

你可能会想,首先,这是避免 retain cycle 警告的一个技巧。这不是重点,这个 self 的强引用是在block 执行时 被创建的,但是否使用 self 在 block 定义时就已经定下来了, 因此self (在block执行时) 会被 retain。

Apple 文档 中表示 "为了 non-trivial cycles ,你应该这样" :

MyViewController *myController = [[MyViewController alloc] init...];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler = ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;
    if (strongMyController) {
        // ...
        [strongMyController dismissViewControllerAnimated:YES completion:nil];
        // ...
    }
    else {
        // Probably nothing...
    }
};

首先,我觉得这个例子看起来是错误的。如果 block 本身在 completionHandler 属性中被 retain 了,那么 self 如何被 delloc 和在 block 之外赋值为 nil 呢? completionHandler 属性可以被声明为 assign 或者 unsafe_unretained 的,来允许对象在 block 被传递之后被销毁。

我不能理解这样做的理由,如果其他对象需要这个对象(self),block 被传递的时候应该 retain 对象,所以 block 应该不被作为属性存储。这种情况下不应该用 __weak/__strong

总之,其他情况下,希望 weakSelf 变成 nil 的话,就像第二种情况解释那么写(在 block 之外定义一个弱应用并且在 block 里面使用)。

还有,Apple的 "trivial block" 是什么呢。我们的理解是 trivial block 是一个不被传送的 block ,它在一个良好定义和控制的作用域里面,weak 修饰只是为了避免循环引用。

虽然有 Kazuki Sakamoto 和 Tomohiko Furumoto) 讨论的 在线 参考, Matt Galloway 的 (Effective Objective-C 2.0Pro Multithreading and Memory Management for iOS and OS X ,大多数开发者始终没有弄清楚概念。

在 block 内用强引用的优点是,抢占执行的时候的鲁棒性。在 block 执行的时候, 再次温故下上面的三个例子:

方案 1. 直接在 block 里面使用关键词 self

如果 block 被属性 retain,self 和 block 之间会有一个循环引用并且它们不会再被释放。如果 block 被传送并且被其他的对象 copy 了,self 在每一个 copy 里面被 retain

方案 2. 在 block 外定义一个 __weak 的 引用到 self,并且在 block 里面使用这个弱引用

不管 block 是否通过属性被 retain ,这里都不会发生循环引用。如果 block 被传递或者 copy 了,在执行的时候,weakSelf 可能已经变成 nil。

block 的执行可以抢占,而且对 weakSelf 指针的调用时序不同可以导致不同的结果(如:在一个特定的时序下 weakSelf 可能会变成nil)。

__weak typeof(self) weakSelf = self;
dispatch_block_t block = ^{
    [weakSelf doSomething]; // weakSelf != nil
    // preemption, weakSelf turned nil
    [weakSelf doSomethingElse]; // weakSelf == nil
};

方案 3. 在 block 外定义一个 __weak 的 引用到 self,并在在 block 内部通过这个弱引用定义一个 __strong 的引用。

不管 block 是否通过属性被 retain ,这里也不会发生循环引用。如果 block 被传递到其他对象并且被复制了,执行的时候,weakSelf 可能被nil,因为强引用被赋值并且不会变成nil的时候,我们确保对象 在 block 调用的完整周期里面被 retain了,如果抢占发生了,随后的对 strongSelf 的执行会继续并且会产生一样的值。如果 strongSelf 的执行到 nil,那么在 block 不能正确执行前已经返回了。

__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
    __strong typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
      [strongSelf doSomething]; // strongSelf != nil
      // preemption, strongSelf still not nil(抢占的时候,strongSelf 还是非 nil 的)
      [strongSelf doSomethingElse]; // strongSelf != nil
    }
    else {
        // Probably nothing...
        return;
    }
};

在ARC条件中,如果尝试用 -> 符号访问一个实例变量,编译器会给出非常清晰的错误信息:

Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (对一个 __weak 指针的解引用不允许的,因为可能在竞态条件里面变成 null, 所以先把他定义成 strong 的属性)

可以用下面的代码展示

__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
    id localVal = weakSelf->someIVar;
};

在最后

  • 方案 1 : 只能在 block 不是作为一个 property 的时候使用,否则会导致 retain cycle。
  • 方案 2 : 当 block 被声明为一个 property 的时候使用。
  • 方案 3 : 和并发执行有关。当涉及异步的服务的时候,block 可以在之后被执行,并且不会发生关于 self 是否存在的问题。

委托和数据源

委托模式 是 Apple 的框架里面使用广泛的模式,同时它是四人帮的书“设计模式”中的重要模式之一。委托代理模式是单向的,消息的发送方(委托方)需要知道接收方(代理方)是谁,反过来就没有必要了。对象之间耦合较松,发送方仅需知道它的代理方是否遵守相关 protocol 即可。

本质上,委托代理模式仅需要代理方提供一些回调方法,即代理方需要实现一系列空返回值的方法。

不幸的是 Apple 的 API 并没有遵守这个原则,开发者也效仿 Apple 进入了一个误区。典型的例子就是 UITableViewDelegate 协议。

它的一些方法返回 void 类型,就像我们所说的回调:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath;

但是其他的就不是那么回事:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender;

当委托者询问代理者一些信息的时候,这就暗示着信息是从代理者流向委托者而非相反的过程。 这(译者注:委托者 ==Data==> 代理者)是概念性的不同,须用另一个新的名字来描述这种模式:数据源模式。

可能有人会说 Apple 有一个 UITableViewDataSouce protocol 来做这个(虽然使用委托模式的名字),但是实际上它的方法是用来提供真实的数据应该如何被展示的信息的。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;

此外,以上两个方法 Apple 混合了展示层和数据层,这显的非常糟糕,但是很少的开发者感到糟糕。而且我们在这里把空返回值和非空返回值的方法都天真地叫做委托方法。

为了分离概念,我们应该这样做:

  • 委托模式(delegate pattern):事件发生的时候,委托者需要通知代理者。
  • 数据源模式(datasource pattern): 委托者需要从数据源对象拉取数据。

这个是实际的例子:

@class ZOCSignUpViewController;
@protocol ZOCSignUpViewControllerDelegate <NSObject>
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end
@protocol ZOCSignUpViewControllerDataSource <NSObject>
- (ZOCUserCredentials *)credentialsForSignUpViewController:(ZOCSignUpViewController *)controller;
@end
@interface ZOCSignUpViewController : UIViewController
@property (nonatomic, weak) id<ZOCSignUpViewControllerDelegate> delegate;
@property (nonatomic, weak) id<ZOCSignUpViewControllerDataSource> dataSource;
@end

代理方法必须以调用者(即委托者)作为第一个参数,就像上面的例子一样。否则代理者无法区分不同的委托者实例。换句话说,调用者(委托者)没有被传递给代理,那就没有方法让代理处理两个不同的委托者,所以下面这种写法人神共怒:

- (void)calculatorDidCalculateValue:(CGFloat)value;

默认情况下,代理者需要实现 protocol 的方法。可以用@required@optional 关键字来标记方法是否是必要的还是可选的(默认是 @required: 必需的)。

@protocol ZOCSignUpViewControllerDelegate <NSObject>
@required
- (void)signUpViewController:(ZOCSignUpViewController *)controller didProvideSignUpInfo:(NSDictionary *)dict;
@optional
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end

对于可选的方法,委托者必须在发送消息前检查代理是否确实实现了特定的方法(否则会 crash):

if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) {
    [self.delegate signUpViewControllerDidPressSignUpButton:self];
}

继承

有时候你可能需要重写代理方法。考虑有两个 UIViewController 子类的情况:UIViewControllerA 和 UIViewControllerB,有下面的类继承关系。

UIViewControllerB < UIViewControllerA < UIViewController

UIViewControllerA 遵从 UITableViewDelegate 并且实现了 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.

你可能会想要在 UIViewControllerB 中提供一个不同的实现,这个实现可能是这样子的:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat retVal = 0;
    if ([super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
        retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
    }
    return retVal + 10.0f;
}

但是如果超类(UIViewControllerA)没有实现这个方法呢?此时调用[super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]方法,将使用 NSObject 的实现,在 self 上下文深入查找并且明确 self 实现了这个方法(因为 UITableViewControllerA 遵从 UITableViewDelegate),但是应用将在下一行发生崩溃,并提示如下错误信息:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewControllerB tableView:heightForRowAtIndexPath:]: unrecognized selector sent to instance 0x8d82820'
*** 由于未捕获异常 `NSInvalidArgumentException(无效的参数异常)`导致应用终止,理由是:向实例 ox8d82820 发送了无法识别的 selector `- [UIViewControllerB tableView:heightForRowAtIndexPath:]`

这种情况下我们需要来询问特定的类实例是否可以响应对应的 selector。下面的代码提供了一个小技巧:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat retVal = 0;
    if ([[UIViewControllerA class] instancesRespondToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
        retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
    }
    return retVal + 10.0f;
}

就像上面丑陋的代码,通常它会是更好的设计架构的方式,因为这种方式代理方法不需要被重写。

多重委托

多重委托是一个非常基础的概念,但是,大多数开发者对此非常不熟悉而使用 NSNotifications。就像你可能注意到的,委托和数据源是对象之间的通讯模式,但是只涉及两个对象:委托者和委托。

数据源模式强制一对一的关系,当发送者请求信息时有且只能有一个对象来响应。对于代理模式而言这会有些不同,我们有足够的理由要去实现很多代理者等待(唯一委托者的)回调的场景。

一些情况下至少有两个对象对特定委托者的回调感兴趣,而后者(即委托者)需要知道他的所有代理。这种方法在分布式系统下更为适用并且广泛使用于大型软件的复杂信息流程中。

多重委托可以用很多方式实现,但读者更在乎找到适合自己的个人实现。Luca Bernardi 在他的 LBDelegateMatrioska中提供了上述范式的一个非常简洁的实现。

这里给出一个基本的实现,方便你更好地理解这个概念。即使在Cocoa中也有一些在数据结构中保存 weak 引用来避免 引用循环的方法, 这里我们使用一个类来保留代理对象的 weak 引用(就像单一代理那样):

@interface ZOCWeakObject : NSObject
@property (nonatomic, readonly, weak) id object; 
// 译者注:这里原文并没有很好地实践自己在本书之前章节所讨论的关于property属性修饰符的
// 人体工程学法则: 从左到右: 原子性 ===》 读写权限 (别名) ===》 内存管理权限符
+ (instancetype)weakObjectWithObject:(id)object;
- (instancetype)initWithObject:(id)object;
@end
@interface ZOCWeakObject ()
@property (nonatomic, weak) id object;
@end
@implementation ZOCWeakObject
+ (instancetype)weakObjectWithObject:(id)object {
    return [[[self class] alloc] initWithObject:object];
}
- (instancetype)initWithObject:(id)object {
    if ((self = [super init])) {
        _object = object;
    }
    return self;
}
- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }
    if (![object isKindOfClass:[object class]]) {
        return NO;
    }
    return [self isEqualToWeakObject:(ZOCWeakObject *)object];
}
- (BOOL)isEqualToWeakObject:(ZOCWeakObject *)object {
    if (!object) {
        return NO;
    }
    BOOL objectsMatch = [self.object isEqual:object.object];
    return objectsMatch;
}
- (NSUInteger)hash {
    return [self.object hash];
}
@end

使用 weak 对象来实现多重代理的简单组件:

@protocol ZOCServiceDelegate <NSObject>
@optional
- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries;
@end
@interface ZOCGeneralService : NSObject
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate;
- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate;
@end
@interface ZOCGeneralService ()
@property (nonatomic, strong) NSMutableSet *delegates;
@end
@implementation ZOCGeneralService
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate {
    if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
        [self.delegates addObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
    }
}
- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate {
    if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
        [self.delegates removeObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
    }
}
- (void)_notifyDelegates {
    ...
    for (ZOCWeakObject *object in self.delegates) {
        if (object.object) {
            if ([object.object respondsToSelector:@selector(generalService:didRetrieveEntries:)]) {
                [object.object generalService:self didRetrieveEntries:entries];
            }
        }
    }
}
@end

registerDelegate:deregisterDelegate: 方法的帮助下,连接/解除组件之间的联系很简单:在某些时间点上,如果代理不需要接收委托者的回调,仅仅需要'unsubscribe'.

当不同的 view 等待同一个回调来更新界面展示的时候,这很有用:如果 view 只是暂时隐藏(但是仍然存在),它仅仅需要取消对回调的订阅。