最近阅读了《禅与 Objective-C 编程艺术》这篇文章,奈何英文不算太好,而且看着好累,还好找到了中文版。读完了这篇文章,不管是对名词的解释、代码规范、还是编码技巧,都有所感悟,总结一下这篇文章需要记录的知识点。
推荐使用常量来代替字符串字面值和数字,这样能够方便复用,而且可以快速修改而不需要查找和替换。常量应该用 static 声明为静态常量,而不要用 #define,除非它明确的作为一个宏来使用。
推荐:
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
不推荐:
#define CompanyName @"Apple Inc."
#define magicNumber 42
常量应该在头文件中以这样的形式暴露给外部:
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
并在实现文件中为它赋值。
我们常常写 [[NSObject alloc] init] 这样的代码,Objective-C 的这个特性叫做 两步创建。这意味着申请分配内存和初始化被分离成两步,alloc 和 init。
alloc 负责创建对象,这个过程包括分配足够的内存来保存对象,写入 isa 指针,初始化引用计数,以及重置所有实例变量。
init 负责初始化对象,这意味着使对象处于可用状态。这通常意味着为对象的实例变量赋予合理有用的值。
alloc 方法将返回一个有效的未初始化的对象实例。每一个对这个实例发送的消息会被转换成一次 objc_msgSend() 函数的调用,形参 self 的实参是 alloc 返回的指针;这样 self 在所有方法的作用域内都能够被访问。 按照惯例,为了完成两步创建,新创建的实例第一个被调用的方法将是 init 方法。注意,NSObject 在实现 init 时,只是简单的返回了 self。
- (id)array;
- (instancetype)array;
+ (id)array {
return [NSArray array];
}
+ (instancetype)array {
return [NSArray array];
}
以上代码初始化数组,返回值分别是 id 和 instancetype
返回值为 id 得到的返回类型是 id
返回值为 instancetype 得到的返回类型是 NSArray *
总结一下 instancetype 优点:使那些非关联返回类型的方法返回所在类的类型。优点是能够确定对象的类型,帮助编译器更好的为我们定位代码书写问题。
创建两个类:
Person 继承 NSObject
Student 继承 Person
BOOL bool1 = [teacher isKindOfClass:[Teacher class]];
BOOL bool2 = [teacher isKindOfClass:[Person class]];
BOOL bool3 = [teacher isKindOfClass:[NSObject class]];
上面三个 BOOL 都为 YES!
BOOL bool1 = [teacher isMemberOfClass:[Teacher class]];
BOOL bool1 = [teacher isMemberOfClass:[Person class]];
BOOL bool1 = [teacher isMemberOfClass:[NSObject class]];
上面第一个 BOOL 为 YES , 其余两个为 NO
所以 isKindOfClass 来确定一个对象是否是一个类的成员,或者是派生自该类的成员;isMemberOfClass 只能确定一个对象是否是当前类的成员
永远不要在 init 方法(以及其他初始化方法)里面用 getter 和 setter 方法,你应当直接访问实例变量。这样做是为了防止有子类时,出现这样的情况:它的子类最终重载了其 setter 或者 getter 方法,因此导致该子类去调用其他的方法、访问那些处于不稳定状态,或者称为没有初始化完成的属性或者 ivar 。记住一个对象仅仅在 init 返回的时候,才会被认为是达到了初始化完成的状态。
一些关键点:
block 是在栈上创建的
block 可以复制到堆上
block 会捕获栈上的变量(或指针),将其复制为自己私有的const(变量)。
如果 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的副本)都会引用该变量在堆上的副本。
最重要的事情是 __block 声明的变量和指针在 block 里面是作为显示操作真实值/对象的结构来对待的。
block 在 Objective-C 的 runtime(运行时) 里面被当作一等公民对待:他们有一个 isa 指针,一个类也是用 isa 指针在Objective-C 运行时来访问方法和存储数据的。在非 ARC 环境肯定会把它搞得很糟糕,并且悬挂指针会导致 crash。__block 仅仅对 block 内的变量起作用,它只是简单地告诉 block:
嗨,这个指针或者原始的类型依赖它们在的栈。请用一个栈上的新变量来引用它。我是说,请对它进行双重解引用,不要 retain 它。 谢谢,哥们。
如果在定义之后但是 block 没有被调用前,对象被释放了,那么 block 的执行会导致 crash。 __block 变量不会在 block 中被持有,最后… 指针、引用、解引用以及引用计数变得一团糟。
本质上,委托代理模式仅需要代理方提供一些回调方法,即代理方需要实现一系列空返回值的方法。
不幸的是 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;
当委托者询问代理者一些信息的时候,这就暗示着信息是从代理者流向委托者而非相反的过程。这是概念性的不同,须用另一个新的名字来描述这种模式:数据源模式。
可能有人会说 Apple 有一个 UITableViewDataSouce protocol 来做这个(虽然使用委托模式的名字),但是实际上它的方法是用来提供真实的数据应该如何被展示的信息的。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
此外,以上两个方法 Apple 混合了展示层和数据层,这显的非常糟糕,但是很少的开发者感到糟糕。而且我们在这里把空返回值和非空返回值的方法都天真地叫做委托方法。
为了分离概念,我们应该这样做:
委托模式(delegate pattern):事件发生的时候,委托者需要通知代理者。
数据源模式(datasource pattern):委托者需要从数据源对象拉取数据。