《禅与Objective-C编程艺术》读书笔记(二)

五、Categories

我们应该要在我们的 category 方法前加上自己的小写前缀以及下划线,比如- (id)zoc_myCategoryMethod。 这种实践同样被苹果推荐。这是非常必要的。因为如果在扩展的 category 或者其他 category 里面已经使用了同样的方法名,会导致不可预计的后果。实际上,实际被调用的是最后被实现的那个方法。如果想要确认你的分类方法没有覆盖其他实现的话,可以把环境变量 OBJC_PRINT_REPLACED_METHODS 设置为 YES,这样那些被取代的方法名字会打印到 Console 中。现在 LLVM 5.1 不会为此发出任何警告和错误提示,所以自己小心不要在分类中重载方法。一个好的实践是在 category 名中使用前缀。

* 例子 *

@interface NSDate (ZOCTimeExtensions)
- (NSString *)zoc_timeAgoShort;
@end

* 不要这样做 *

@interface NSDate (ZOCTimeExtensions)
- (NSString *)timeAgoShort;
@end

六、Protocols

在 Objective-C 的世界里面经常错过的一个东西是抽象接口。接口(interface)这个词通常指一个类的 .h 文件,但是它在 Java 程序员眼里有另外的含义: 一系列不依赖具体实现的方法的定义。在 Objective-C 里是通过 protocol 来实现抽象接口的。我们会解释 protocol 的强大力量(用作抽象接口),用具体的例子来解释:把非常糟糕的设计的架构改造为一个良好的可复用的代码。这个例子是在实现一个 RSS 订阅的阅读器(它可是经常在技术面试中作为一个测试题呢)。

要求很简单明了:把一个远程的 RSS 订阅展示在一个 tableview 中。

最小的步骤是遵从单一功能原则,创建至少两个组成部分来完成这个任务:

(a)一个 feed 解析器来解析搜集到的结果

(b)一个 feed 阅读器来显示结果

这些类的接口可以是这样的:

@interface ZOCFeedParser : NSObject

@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate;
@property (nonatomic, strong) NSURL *url;

- (id)initWithURL:(NSURL *)url;

- (BOOL)start;
- (void)stop;

@end

@interface ZOCTableViewController : UITableViewController

- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser;

@end

ZOCFeedParser 用一个 NSURL 来初始化来获取 RSS 订阅(在这之下可能会使用 NSXMLParser 和 NSXMLParserDelegate 创建有意义的数据),ZOCTableViewController 会用这个 parser 来进行初始化。 我们希望它显示 parser 接受到的指并且我们用下面的 protocol 实现委托:

@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedItem:(ZOCFeedItemDTO *)item;
- (void)feedParserDidFinish:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didFailWithError:(NSError *)error;
@end

用合适的 protocol 来来处理 RSS 非常完美。view controller 会遵从它的公开的接口:

@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>

最后创建的代码是这样子的:

NSURL *feedURL = [NSURL URLWithString:@"http://bbc.co.uk/feed.rss"];

ZOCFeedParser *feedParser = [[ZOCFeedParser alloc] initWithURL:feedURL];

ZOCTableViewController *tableViewController = [[ZOCTableViewController alloc] initWithFeedParser:feedParser];
feedParser.delegate = tableViewController;

七、NSNotification

当你定义你自己的 NSNotification 的时候你应该把你的通知的名字定义为一个字符串常量,就像你暴露给其他类的其他字符串常量一样。你应该在公开的接口文件中将其声明为 extern 的, 并且在对应的实现文件里面定义。因为你在头文件中暴露了符号,所以你应该按照统一的命名空间前缀法则,用类名前缀作为这个通知名字的前缀。同时,用一个 Did/Will 这样的动词以及用 “Notifications” 后缀来命名这个通知也是一个好的实践。

// Foo.h
extern NSString * const ZOCFooDidBecomeBarNotification

// Foo.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";

八、美化代码

1.空格

(a)缩进使用 4 个空格。 永远不要使用 tab, 确保你在 Xcode 的设置里面是这样设置的。

(b)方法的大括号和其他的大括号(if/else/switch/while 等) 总是在同一行开始,在新起一行结束。

推荐:

if (user.isHappy) {
    //Do something
}
else {
    //Do something else
}

//不推荐:

if (user.isHappy)
{
  //Do something
} else {
  //Do something else
}

(c)方法之间应该要有一个空行来帮助代码看起来清晰且有组织。 方法内的空格应该用来分离功能,但是通常不同的功能应该用新的方法来定义。 优先使用 auto-synthesis。但是如果必要的话, @synthesize and @dynamic。

(d)在实现文件中的声明应该新起一行。

(e)应该总是让冒号对其。有一些方法签名可能超过三个冒号,用冒号对齐可以让代码更具有可读性。总是用冒号对其方法,即使有代码块存在。

推荐:

[UIView animateWithDuration:1.0
                 animations:^{
                     // something
                 }
                 completion:^(BOOL finished) {
                     // something
                 }];

不推荐:

[UIView animateWithDuration:1.0 animations:^{
    // something
} completion:^(BOOL finished) {
    // something
}];

如果自动对齐让可读性变得糟糕,那么应该在之前把 block 定义为变量,或者重新考虑你的代码签名设计。

2.Line Breaks 换行

本指南关注代码显示效果以及在线浏览的可读性,所以换行是一个重要的主题。

举个例子:

self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];

一个像上面的长行的代码在第二行以一个间隔(2个空格)延续

self.productsRequest = [[SKProductsRequest alloc]
  initWithProductIdentifiers:productIdentifiers];

3.括号

在以下的地方使用 Egyptian风格 括号 (译者注:又称 K&R 风格,代码段括号的开始位于一行的末尾,而不是另外起一行的风格。关于为什么叫做 Egyptian Brackets,可以参考 http://blog.codinghorror.com/new-programming-jargon/ )

控制语句 (if-else, for, switch)

非 Egyptian 括号可以用在:

类的实现(如果存在)

方法的实现

九、代码组织

1.利用代码块

一个 GCC 非常模糊的特性,以及 Clang 也有的特性是,代码块如果在闭合的圆括号内的话,会返回最后语句的值

NSURL *url = ({
    NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];
    [NSURL URLWithString:urlString];
});

这个特性非常适合组织小块的代码,通常是设置一个类。他给了读者一个重要的入口并且减少相关干扰,能让读者聚焦于关键的变量和函数中。此外,这个方法有一个优点,所有的变量都在代码块中,也就是只在代码块的区域中有效,这意味着可以减少对其他作用域的命名污染。

2.Pragma

2.1 Pragma Mark

#pragma mark - 是一个在类内部组织代码并且帮助你分组方法实现的好办法。 我们建议使用  #pragma mark - 来分离:

(1) 不同功能组的方法

(2) protocols 的实现

(3) 对父类方法的重写

- (void)dealloc { /* ... */ }
- (instancetype)init { /* ... */ }

 #pragma mark - View Lifecycle (View 的生命周期)

- (void)viewDidLoad { /* ... */ }
- (void)viewWillAppear:(BOOL)animated { /* ... */ }
- (void)didReceiveMemoryWarning { /* ... */ }

 #pragma mark - Custom Accessors (自定义访问器)

- (void)setCustomProperty:(id)value { /* ... */ }
- (id)customProperty { /* ... */ }

 #pragma mark - IBActions  

- (IBAction)submitData:(id)sender { /* ... */ }

 #pragma mark - Public 

- (void)publicMethod { /* ... */ }

 #pragma mark - Private

- (void)zoc_privateMethod { /* ... */ }

 #pragma mark - UITableViewDataSource

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { /* ... */ }

 #pragma mark - ZOCSuperclass

// ... 重载来自 ZOCSuperclass 的方法

 #pragma mark - NSObject

- (NSString *)description { /* ... */ }

上面的标记能明显分离和组织代码。你还可以用 cmd+Click 来快速跳转到符号定义地方。 但是小心,即使 paragma mark 是一门手艺,但是它不是让你类里面方法数量增加的一个理由:类里面有太多方法说明类做了太多事情,需要考虑重构了。

2.2 关于Pragma

大多数 iOS 开发者平时并没有和很多编译器选项打交道。一些选项是对控制严格检查(或者不检查)你的代码或者错误的。有时候,你想要用 pragma 直接产生一个异常,临时打断编译器的行为。当你使用ARC的时候,编译器帮你插入了内存管理相关的调用。但是这样可能产生一些烦人的事情。比如你使用 NSSelectorFromString 来动态地产生一个 selector 调用的时候,ARC不知道这个方法是哪个并且不知道应该用那种内存管理方法,你会被提示 performSelector may cause a leak because its selector is unknown(执行 selector 可能导致泄漏,因为这个 selector 是未知的).如果你知道你的代码不会导致内存泄露,你可以通过加入这些代码忽略这些警告

 #pragma clang diagnostic push

 #pragma clang diagnostic ignored "-Warc-performSelector-leaks"

[myObj performSelector:mySelector withObject:name];

 #pragma clang diagnostic pop

注意我们是如何在相关代码上下文中用 pragma 停用 -Warc-performSelector-leaks 检查的。这确保我们没有全局禁用。如果全局禁用,可能会导致错误。

3.忽略没用使用变量的编译警告

这对表明你一个定义但是没有使用的变量很有用。大多数情况下,你希望移除这些引用来(稍微地)提高性能,但是有时候你希望保留它们。为什么?或许它们以后有用,或者有些特性只是暂时移除。无论如何,一个消除这些警告的好方法是用相关语句进行注解,使用 #pragma unused():

- (void)giveMeFive
{
    NSString *foo;
    #pragma unused (foo)

    return 5;
}

现在你的代码不用任何编译警告了。注意你的 pragma 需要标记到未定义的变量之下。

4.明确编译器警告和错误

编译器是一个机器人,它会标记你代码中被 Clang 规则定义为错误的地方。但是,你总是比 Clang 更聪明。通常,你会发现一些讨厌的代码 会导致这个问题,而且不论怎么做,你都解决不了。你可以这样明确一个错误:

- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor
{
    #error Whoa, buddy, you need to check for zero here!
    return (dividend / divisor);
}

类似的,你可以这样标明一个 警告

- (float)divide:(float)dividend by:(float)divisor
{
    #warning Dude, don‘t compare floating point numbers like this!
    if (divisor != 0.0) {
        return (dividend / divisor);
    }
    else {
        return NAN;
    }
}

5.字符串文档

所有重要的方法,接口,分类以及协议定义应该有伴随的注释来解释它们的用途以及如何使用。简而言之:有长的和短的两种字符串文档。

短文档适用于单行的文件,包括注释斜杠。它适合简短的函数,特别是(但不仅仅是)非 public 的 API:

// Return a user-readable form of a Frobnozz, html-escaped.

文本应该用一个动词 (“return”) 而不是 “returns” 这样的描述。

如果描述超出一行,你应该用长的字符串文档: 一行斜杠和两个星号来开始块文档 (/*, 之后是总结的一句话,可以用句号、问号或者感叹号结尾,然后空一行,在和第一句话对齐写下剩下的注释,然后用一个 (/)来结束。

/**
 This comment serves to demonstrate the format of a docstring.

 Note that the summary line is always at most one line long, and
 after the opening block comment, and each line of text is preceded
 by a single space.
*/

一个函数必须有一个字符串文档,除非它符合下面的所有条件:

(a) 非公开

(b) 很短

(c) 显而易见

字符串文档应该描述函数的调用符号和语义,而不是它如何实现。

6.注释

当它需要的时候,注释应该用来解释特定的代码做了什么。所有的注释必须被持续维护或者干脆就删除。块注释应该被避免,代码本身应该尽可能就像文档一样表示意图,只需要很少的打断注释 例外: 这不能适用于用来产生文档的注释

7.头文档

一个类的文档应该只在 .h 文件里用 Doxygen/AppleDoc 的语法书写。 方法和属性都应该提供文档。

例子:

/**
 *  Designated initializer.
 *
 *  @param  store  The store for CRUD operations.
 *  @param  searchService The search service used to query the store.
 *
 *  @return A ZOCCRUDOperationsStore object.
 */
- (instancetype)initWithOperationsStore:(id<ZOCGenericStoreProtocol>)store
                          searchService:(id<ZOCGenericSearchServiceProtocol>)searchService;

十、对象间的通讯

1.Blocks

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

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

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

(a) 通常这成功处理和失败处理会共享一些代码(比如让一个进度条或者提示消失);

(b) Apple 也是这样做的,与平台一致能够带来一些潜在的好处;

(c) block 通常会有多行代码,如果不是在最后一个参数的话会打破调用点;

(d) 使用多个 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 的值可能出现问题。

2.深入 Blocks

一些关键点:

(1) block 是在栈上创建的

(2) block 可以复制到堆上

(3) block 有自己的私有的栈变量(以及指针)的常量复制

(4) 可变的栈上的变量和指针必须用 __block 关键字声明

如果 block 没有在其他地方被保持,那么它会随着栈生存并且当栈帧(stack frame)返回的时候消失。当在栈上的时候,一个 block 对访问的任何内容不会有影响。如果 block 需要在栈帧返回的时候存在,它们需要明确地被复制到堆上,这样,block 会像其他 Cocoa 对象一样增加引用计数。当它们被复制的时候,它会带着它们的捕获作用域一起,retain 他们所有引用的对象。如果一个 block指向一个栈变量或者指针,那么这个block初始化的时候它会有一份声明为 const 的副本,所以对它们赋值是没用的。当一个 block 被复制后,__block 声明的栈变量的引用被复制到了堆里,复制之后栈上的以及产生的堆上的 block 都会引用这个堆上的变量。

最重要的事情是 __block 声明的变量和指针在 block 里面是作为显示操作真实值/对象的结构来对待的。block 在 Objective-C 里面被当作一等公民对待:他们有一个 isa 指针,一个类也是用 isa 指针来访问 Objective-C 运行时来访问方法和存储数据的。在非 ARC 环境肯定会把它搞得很糟糕,并且悬挂指针会导致 Crash。__block 仅仅对 block 内的变量起作用,它只是简单地告诉 block:嗨,这个指针或者原始的类型依赖它们在的栈。请用一个栈上的新变量来引用它。我是说,请对它进行双重解引用,不要 retain 它。 谢谢,哥们。如果在定义之后但是 block 没有被调用前,对象被释放了,那么 block 的执行会导致 Crash。 __block 变量不会在 block 中被持有,最后… 指针、引用、解引用以及引用计数变得一团糟。

3.self 的循环引用

当使用代码块和异步分发的时候,要注意避免引用循环。 总是使用 weak 引用会导致引用循环。 此外,把持有 blocks 的属性设置为 nil (比如 self.completionBlock = nil) 是一个好的实践。它会打破 blocks 捕获的作用域带来的引用循环。

例子:

__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

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

dispatch_block_t completionBlock = ^{
    NSLog(@"%@", self);
}

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:completionHandler];

不是很麻烦的事情。但是, 当 block 被 self 在一个属性 retain(就像下面的例子)呢

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 的 property 属性里面 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 被一个对象复制到了另外一个(比如 myControler)作为属性赋值的结果。之前的对象在可能在被复制的 block 有机会执行被销毁。

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

你可能会想,首先,这是避免 retain cycle 警告的一个技巧。然而不是,这个到 self 的强引用在 block 的执行时间 被创建。当 block 在定义的时候, block 如果使用 self 的时候,就会 retain 了 self 对象。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 修饰只是为了避免循环引用。

在 block 内用强引用的优点是,抢占执行的时候的鲁棒性。看上面的三个例子,在 block 执行的时候

(a) 直接在 block 里面使用关键词 self

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

(b) 在 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
};

(c) 在 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;
};

在最后:

Case 1: 只能在 block 不是作为一个 property 的时候使用,否则会导致 retain cycle。

Case 2: 当 block 被声明为一个 property 的时候使用。

Case 3: 和并发执行有关。当涉及异步的服务的时候,block 可以在之后被执行,并且不会发生关于 self 是否存在的问题。

4.委托和数据源

委托是 Apple 的框架里面使用广泛的模式,同时它是一个重要的 四人帮的书“设计模式”中的模式。委托模式是单向的,消息的发送方(委托方)需要知道接收方(委托),反过来就不是了。对象之间没有多少耦合,因为发送方只要知道它的委托实现了对应的 protocol。本质上,委托模式只需要委托提供一些回调方法,就是说委托实现了一系列空返回值的方法。

一些有 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;

当委托者询问委托对象一些信息的时候,这就暗示着信息是从委托对象流向委托者,而不会反过来。 这个概念就和委托模式有些不同,它是一个另外的模式:数据源。

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

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

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

委托模式:事件发生的时候,委托者需要通知委托

数据源模式: 委托方需要从数据源对象拉取数据

这个是实际的例子:

@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 关键字来标记方法是否是必要的还是可选的。

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

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

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

5.继承

有时候你可能需要重载委托方法。考虑有两个 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 的上下文中无疑有它的实现,但是 app 会在下一行 Crash 并且报下面的错:

*** Terminating app due to uncaught exception ‘NSInvalidArgumentException‘, reason: ‘-[UIViewControllerB tableView:heightForRowAtIndexPath:]: unrecognized selector sent to instance 0x8d82820‘

这种情况下我们需要来询问特定的类实例是否可以响应对应的 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;
}

6.多重委托

多重委托是一个非常基础的概念,但是,大多数开发者对此非常不熟悉而使用 NSNotifications。就像你可能注意到的,委托和数据源是对象之间的通讯模式,但是只涉及两个对象:委托者和委托。数据源模式强制一对一的关系,发送者来像一个并且只是一个对象来请求信息。但是委托模式不一样,它可以完美得有多个委托来等待回调操作。至少两个对象需要接收来自特定委托者的回调,并且后一个需要知道所有的委托,这个方法更好的适用于分布式系统并且更加广泛用于大多数软件的复杂信息流传递。

十一、面向切面编程

Aspect Oriented Programming (AOP,面向切面编程) 在 Objective-C 社区内没有那么有名,但是 AOP 在运行时可以有巨大威力。在 Objective-C 的世界里,这意味着使用运行时的特性来为 切面 增加适合的代码。通过切面增加的行为可以是:

(1)在类的特定方法调用前运行特定的代码

(2)在类的特定方法调用后运行特定的代码

(3)增加代码来替代原来的类的方法的实现

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

比如,下面的代码会对于执行 MyClass 类的 myMethod: (实例或者类的方法) 执行块参数。

[MyClass aspect_hookSelector:@selector(myMethod:)
                 withOptions:AspectPositionAfter
                  usingBlock:^(id<AspectInfo> aspectInfo) {
            ...
        }
                       error:nil];

换一句话说:这个代码可以让在 @selector 参数对应的方法调用之后,在一个 MyClass 的对象上(或者在一个类本身,如果方法是一个类方法的话)执行 block 参数。

时间: 2024-09-30 10:14:57

《禅与Objective-C编程艺术》读书笔记(二)的相关文章

JAVA读书推荐----《深入分析Java Web技术内幕》--《java多线程编程核心技术》--《大型网站技术架构 核心原理与案例分析》-《Effective Java中文版》

(1)  首先推荐的不是一本书,而是一个博客,也是我们博客园另外一位博友java_my_life. 目前市面上讲解设计模式的书很多,虽然我前面讲了看书是最好的,但是对设计模式感兴趣的朋友们,我推荐的是这个博客.这位博友的设计模式讲得非常非常好,我认为90%的内容都是没有问题且很值得学习的,其讲解设计模式的大体路线是: 1.随便开篇点明该设计模式的定义 2.图文并茂讲解该设计模式中的结构 3.以详细的代码形式写一下该种设计模式的实现 4.补充内容 5.讲解该设计模式的优缺点 对于一个设计模式我们关

《大型网站技术架构-核心原理与案例分析》之一: 大型网站架构演化

最近刚刚读完李智慧的<大型网站技术架构-核心原理与案例分析>,对每章重点内容作了一些笔记,以便加深印象及日后查阅. 一.大型网站软件系统的特点 高并发,大流量:需要面对高并发用户,大流量访问. 高可用:系统7X24小时不间断服务. 海量数据:需要存储.管理海量数据,需要使用大量服务器. 用户分布广泛,网络情况复杂:许多大型互联网都是为全球用户提供服务的,用户分布范围广,各地网络情况千差万别. 安全环境恶劣:由于互联网的开放性,使得互联网站更容易受到攻击,大型网站几乎每天都会被黑客攻击. 需求快

大型网站技术架构--核心原理和案例分析 大型网站架构演化(一)

如果把上世纪90年代CERN正式发布web标准和第一个WEB服务的出现当作互联网的开始,那么互联网站的发展之经历了短短20多年的时间.在20多年的时间里,互联网的世界发生了变化,今天,全球有近一半的人口使用互联网,人们的生活因为互联网而产生了巨大的变化.从信息检索到即使通信,从电子购物到文化娱乐,互联网渗透到生活的每一个 角落,而且这种趋势还在蔓延.因为互联网,我们的世界正变得越来越小. 同时我们也看到,在互联网跨越式发展进程中,在电子商务火热的市场背后却是不堪重负的网站架构.某些B2C网站逢促

《大型网站技术架构核心原理与案例分析》阅读笔记-01

通过阅读该书籍我们能够更加清楚的树立大型网站的的技术发展历程,剖析大型网站技术架构模式,深入的讲述大型互联网架构核心原理,并通过一些典型的技术案例来讲述大型网站开发全景视图,该书籍深入的阐述了各种大型网站面临的各种架构问题及解决方案. 在第一章第一篇大型网站架构演化中了解到与传统企业应用系统相比,大型互联网应用系统具有高并发大流量.高可用性.海量数据.用户分布广泛,网络情况复杂.安全环境恶劣.需求快速变更,发布频繁.渐进式发展等特点:大型网站架构演化发展历程经历了初始阶段的网络架构它的应用程序.

大型网站技术架构-核心原理与案例分析

阿里系的书,也是讲大型网站系统架构的,平常我们总是挂在嘴边的高性能.高可用.易扩展.安全性,这些所谓的系统非功能性指标到底如何实现,书里面讲了这些干货,作为网站架构师或者哪怕是应用系统的架构师,都值得了解,也许不一定都能用上,但是等需要用的那天,你肯定不会迷茫. 1.大型网站架构发展常见历程:应用/数据库分离--->使用缓存--->应用服务器集群--->数据库读写分离--->CDN及反向代理--->使用分布式文件系统和分布式数据库--->NoSQL及搜索引擎--->

【大型网站技术架构 核心原理与案例分析】读书笔记

章节 笔记 1.概述 网站架构模式:分层.分割.分布式.集群.缓存.异步.冗余.自动化.安全. 核心架构要素:性能.可用性.伸缩性.扩展性.安全. 4.高性能 一般重复请求一万次计算总响应时间然后除以一万得到单词响应时间. 测试程序并不是启动多线程然后不停发送请求,而是在两次请求之间加入一个随机等待时间. 吞吐量:每天通过收费站的车辆数目:并发数:正在行驶的车辆数目:响应时间:车速.TPS:每秒事务数:HPS:每秒请求数:QPS:每秒查询数. 性能计数器:System Load(系统负载,最理想

大型网站技术架构-核心原理与案例分析-阅读笔记4

在第四章案例章节中的淘宝网的架构演化案例分析小节中作者主要分析了淘宝架构的演化,以淘宝网的实例给我们分析介绍了淘宝网的业务发展历程及淘宝网的技术架构演化两个方面,在业务发展中作者写到淘宝的技术是随着淘宝业务一起发展起来的,业务是推动这技术发展的动力,淘宝如今的规模和当初有很明显的变化,在技术架构演化中介绍了架构技术的更新升级,该章节中主要介绍淘宝网的发展的历程,在随着时间的发展不断中网站的架构不断的引用着新的技术,由最初简单的c2c更改过来的网站,放弃了lamp架构转而使用java作为开发平台并

大型网站技术架构-核心原理与案例分析-阅读笔记3

在第二章的架构章节中的 随机应变:网站的可拓展架构的篇章中作者介绍了构建网站的可扩展架构.利用分布式队列降低系统的耦合性.利用分布式可复用的业务平台.可拓展的数据结构.利用开放平台建设网站生态圈五个方面,作者在讲述前通过微信的成功发布及其中摇一摇功能的加入的开发的快捷引出来的,其中构建网站的可扩展架构中区分了扩展性和伸缩性的区别,讲到了低耦合性的系统跟容易扩展,并且更容易复用,一个低耦合性的系统也可以让系统更加容易的开发和维护,在如何降低系统的耦合性中,作者主要介绍用分布式消息队列的方法来降低系

大型网站技术架构-核心原理与案例分析-阅读笔记5

在第四章案例章节中的海量分布式存储系统Doris的高可用架构设计分析的小节中作者主要分析介绍了分布式存储的高可用架构和不同故障情况下的高可用解决两个方面,在两小节前作者给我们介绍了Doris是一个海量分布式KV存储系统,其设计的目的是支持中等规模高可用.可伸缩的Kv存储群.跟主流的NoSQL系统HBase相比,doris具有相似的性能和线性伸缩能力,并具有更好的可用性及更友好的图形用户管理界面.而在分布式存储的高可用架构的小节中作者给我们分析了Doris的整体架构,其系统整体上可分为应用程序服务

学习笔记8:《大型网站技术架构 核心原理与案例分析》之 固若金汤:网站的安全架构

一.网站攻击与防御 攻击: 1.XSS攻击:危险字符转义,HttpOnly 2.注入攻击:参数绑定 3.CSRF(跨站点请求伪造):Token,验证码,Referer Check 4.其他漏洞攻击 Error Code HTML 注释 文件上传 路径遍历 防御: 1.Web应用防火墙:ModSecurity 2.网站安全漏洞扫描 二.信息加密技术与密钥管理 1.单向散列加密 2.对称加密 3.非对称加密 4.密钥安全管理 三.信息过滤与反垃圾 1.文本匹配:正则,Trie算法,多级Hash,降噪