[iOS]小学生的读书采蜜本里有什么-Effective Objective-C 2.0阅读笔记

[iOS]小学生的读书采蜜本里有什么- Objective-C 2.0阅读笔记

文章目录

第 1 条 :了解 Objective-C 语言的起源

Objective-C 为C语言添加了面向对象特性,是其超集 。
Objective-C 语言使用 “消息结构” (messagingstructure)而非“函数调用” (function calling)。
Objective-C 语言由 Smalltalk 演化而来,后者是消息型语言的鼻祖。

消息与函数调用之间的关键区别在于:使用消息结构的语言,其运行时所应执行的代码由 运行环境 来决定;而使用函数调用的语言,则由 编译器 决定。

使用函数调用的语言,如果范例代码中调用的函数是多态的,那么在运行时就要按照“虚方法表”(virtualtable)来查出到底应该执行哪个函数实现。而采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。
实际上,编译器甚至不关心接收消息的对象是何种类型。接收消息的对象问题也要在运行时处理,其过程叫做“动态绑定 ” (dynamicbinding)

分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出 时自动清理。

Objective-C 将堆内存管理抽象出来了。不需要用 malloc 及 free 来分配或释放对象所占内存。Objective-C
运行期环境把这部分工作抽象为一套内存管理架构 ,名叫“引用计数”。 在 Objective-C 代码中,有时会遇到定义里不含 *
的变量,它们可能会使用“栈空间” (stack space)。这些变量所保存的不是 Objective-C 对象。

第 2 条 :在类的头文件中尽量减少引入其他头文件

与 C 和 C++ 一样 ,Objective-C 也使用“头文件” (headerfile)与“实现文件” (implementationfilc)来区隔代码。

引入头文件的办法可行,但是不够优雅。除非确有必要,否则不要引人头文件。在编译一个使用了 EOCPerson 类的文件时,如果想添加一个 EOCEmployer 类的属性,不需要知道 EOCEmployer 类的全部细节,只需要知道有 一个类名叫EOCEmployer 就好。有个办法能把这一情况告诉编译器

@class EOCEmployer;

这叫做 向前声明 (forward declaring)该类。然后可以在实现文件再引入 EOCEmployer 类的头文件。

将引入头文件的时机尽量延后,只在确有需要时才引入,这样就可以减少类的使用者所需引人的头文件数量。 否则要引入许多根本用不到的内容,这当然会增加编译时间。
向前声明也解决了两个类互相引用的问题。如果在各自头文件中引入对方的头文件,则会导 致“循环引用” (chicken-and-eggsituation)。当解析其中一个头文件时,编译器会发现它引入 了另 一个头文件,而那个头文件又回过头来引用第一个头文件。使用 #import 而非 #include 指令虽然不会导致死循环,但却这意味着两个类里有一个无法被正确编译

有时候必须要在头文件中引入其他头文件

如果你写的类继承自某个超类
必须引入定义那个超类的头文件。
如果要声明你写的类遵从某个协议(protocol)
该协议必须有完整定义,且不能使用向前声明。向前声明只能告诉编译器有某个协议,而此时编译器却要知道该协议中定义的方法。

有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该 类遵循某协议”
的这条声明移至“class-continuation分类” 中。如果不行的话,就把协议单独放在一个头文件中 ,然后将其引入。

第 3 条 :多用字面量语法,少用与之等价的方法

字面量语法实际上只是一种“语法糖”。也称“糖衣语法”,是指计算机语言中与另外一套语法等效但是开发者用起来却更加方便的语法。语法糖可令程序更易读,减少代码出错机率。

具体介绍一下好处

  1. 字面量语法更为精筒。也令对象变得整洁,因为声明中只包含数值,而没有多余的语法成分。
  2. 这种做法不仅简单,而且还利于操作数组。,使用字面量语法更为安全。抛出异常令应用程序终止执行,这比通过方法调用创建好数组之后才发现元素个数少了要好。向数组中插人 nil 通常说明程序有错,而通过异常可以更快地发现这个错误。

具体介绍一下局限

  1. 字面量语法有个小小的限制,就是除 了字符串以外,所创建出来的对象必须属 于 Foundation框架才行。如果自定义了这些类的子类,则无法用字面量语法创建其对象。
  2. 使用字面量语法创建出来的字符串、数组、字典对象都是不可变的 (immutable)。若想要可变版本的对象,则需用mutableCopy方法拷贝一份。

具体介绍一下方法

NSNumber 类

NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *doubleNumber = @3.14159;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';

int x = 5;
float y= 6.32f;
NSNumber *expressionNumber = @(x * y);

NSArray 类

NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
NSString *dog = animals[1];

NSDictionary 类

NSDictionary *personData = @(@"firstName" : @"Matt", 
							@"lastName" :@"Galloway"@"age" : @28};
//改为可变数组
[mutableArray replaceObjectAtIndex:1 withObject: @"dog"] ; [mutableDictionary setObject:@"Galloway"forKey: @"lastName"] ;

mutableArray [1] = @"dog";
mutableDictionary [@"lastName"] = @"Galloway";

注意
调用方法赋值时顺序是<对象 >,<键 >,<对象 >,<键 >
字面量语法的顺序则是<键 >,<对象 >,<键 >,<对象 >

第 4 条 :多用类型常量,少用 #define 预处理指令

编写代码时经常要定义常量。如果用 #define 预处理指令定义出来的常量没有类型信息,预处理过程会把碰到的所有相同字符一律替换,假设此指令声明在某个头文件中,所有引入了这个头文件的代码其相同字符都会被替换。

static const NSTimeInterval kAnimationDuration = 0.3;

上面方法定义的常量包含类型信息,其好处是清楚地描述了常量的含义

常用的命名法是:若常量局限于某“编译单元”(translationunit,也就是“ 实现文件”,implementationfile)之内,则在前面加字母k ;若常量在类之外可见,则通常以类名为前缀。第 19 条详解了命名习惯(namingconvention)

若不打算公开某个常量
应将其定义在使用该常量的实现文件里

// EOCAnimatedView.h #import ‹UIKit/UIKit.h>
@interface EOCAnimatedView : UIView 
- (void)animate;
@end

// EOCAnimatedView.m
#import "EOCAnimatedView.h"
static const NSTimeInterval kAnimationDuration = 0.3;
@implementation EOCAnimatedView 
- (void)animate {
	[UIViewanimateWithDuration:kAnimationDuration animations: ^(>{
			// Perform animations
			}];
}
@end

变量一定要同时用static 与const 来声明。如果试图修改由const 修饰符所声明的变量, 那么编译器就会报错。static 修饰符则意味着该变量仅在定义此变量的编译单元中可见。

实际上,如果 一个变量既声明为static,又声明为const,那么编译器根本不会创建符号, 而是会像#defne预处理指令一样,把所有遇到的变量都替换为常值。不过还是要记住:用这种方式定义的常量带有类型信息。

由于此类常量不在全局符号表中,所以无须为其名称加前缀。

有时候需要对外公开某个常量
NSString*constEOCStringConstant =@“VALUE”: 这个常量在头文件中“声明”,且在实现文件中“ 定义”。

// In the header file
extern NSString *const EOCStringConstant;
// In the implementation file
NSString*constEOCStringConstant = @"VALUE":

常量定义应从右至左解读,所以在本例中,EOCStringConstant就是“ 一个常量, 而这个常量是指针,指向NSString 对象”。这与需求相符:我们不希望有人改变此指针常量, 使其指向另一个NSString对象。

这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。

第 5 条 :用枚举表示状态、选项、状态码

应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。

要想每次不用敲入 enum 而只需写 EOCConnectionState,则需使用 typedef 关键字重新定义枚举类型:

enum EOCConnectionState (
	EOCConnectionStateDisconnected,
	EOCConnectionStateConnecting,
	 EOCConnectionStateConnected,
};
typedef enum EOCConnectionState EOCConnectionState;

现在可以用简写的EOCConnectionState来代替完整的enumEOCConnectionState 了:

EOCConnectionState state = EOCConnectionStateDisconnected;

如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为 2 的幂,以便通过按位或操作将其组合起来。

用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做 可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。

凡是需要以按位或操作来组合的枚举都应使用NS _OPTIONS 定义。若是枚举不需要互相组合,则应使用NS _ENUM来定义。

在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后, 编译器就会提示开发者:switch 语句并未处理所有枚举。

第 6 条 :理解“属性”这一概念

属性:编译器会自动写出一套存取方法, 用以访问给定类型中具有给定名称的变量

“属性” (property)是Objecive-C的一项特性,用于封装对象中的数据

属性的优势

  1. 如果使用了属性的话,那么编译器就会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(由编译器在编译期执行)

  2. 要访问属性,可以使用“点语法”,如同在C中访问分配在栈上的struct 结构体里面的成员,也需使用类似语法。编译器会把“点语法”转换为对存取方法的调用,使用点 语法” 的效果与直接调用存取方法等效

  3. 可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字。下述语法会将生成的实例变量命名为_myFirstNvame 与_myLastName,而不再使用默认的名字、

@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName =_myLastName; 
@end
  1. 若不想令编译器自动合成存取方法,则可以自己实现。如果你只实现了其中 一个存取方法,那么另外一个还是会由编译器来合成。但是使用 @dynamic 关键字会告诉编译器不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。

对象布局在编译期(compiletime)就已经固定了。只要碰到访问变量的代码,编译器就把其替换为“偏移量”(offset),这个偏移量是“硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。

如果在原有对象内又加了一个实例变量,把偏移量硬编码于其中的那些代码都可能会读取到错误的值。所以如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须 重新编译 ,否则就会出错。例如 ,某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象

“应用程序二进制接口” (ApplicationBinaryInterface, ABI)
Objective-C的做法是,把实例变量当做一种存储偏移量所用的“特殊变量” (specialvariable),交由 “类对象” (classobject)保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。甚至可以在运行期向类中新增实例变量

所以不一定要在接口中把全部实例变量都声明好,可以将某些变量从接口的public区段里移走,以便保护与类实现有关的内部信息。

四种属性特质

若是自己定义存取方法,那么就应该遵从与属性特质相符的属性特质

原子性

在并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与操作后的结果,那么该操作具备“原子性”(atomicity)

默认 情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备nonatomic特质,则不使用同步锁。

具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性。这也就是说,如果两个线程读写同一属性,那么不论何时, 总能看到有效的属性值。若是不加锁的话(或者说使用nonatomic语义),那么当其中一个线程正在改写某属性值时,另一个线程也许会突然闯人,把尚未修改好的属性值读取出来。 发生这种情况时,线程读到的属性值可能不对。
所有属性都声明为nonatomic。这样做的历史原因是:在i OS中使用同步锁的开销较大,这会带来性能问题。 一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全”,若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。一个线程在连续多次读取某属性值的过程中 有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读到不同的属性值

读写权限

  1. 具备readwrite(读写)特质的属性拥有“获取方法”(getter)与“设置方法”(setter)。若该属性由@synthesize实现,则编译器会 自动生成 这两个方法。
  2. 具备readonly(只读)特质的属性仅拥有获取方法,只有当该属性由@synthesize实现时 ,编译器才会为其合成获取方法。

你可以用此特质把某个属性对外公开为只读属性,然后在“class-continuation分类” 中将其重新定义读写属性。

内存管理语义
这一组特质 仅会影响“设置方法”

  1. assign “设置方法 ”只会执行针对“纯量类型” (scalar type,例如CGFloat 或 NSInteger等)的 简单赋值操作
  2. strong 此特质表明该属性定义了一种 “拥有关系” (owningrelationship)。这种属性设置新值时,设置方法会 先保留新值,并释放旧值,然后再将新值设置上去
  3. weak 此特质表明该属性定义了一种“ 非拥有关系”(nonowningrelationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质 同assign类似 ,然而在属性所指的对象遭到摧毁时,属性也会清空
  4. unsafe_unretained 此特质的语义和assign相同,但是它 适用于“对象类型” ,该特质表达一种“非拥有关系”,当标对象遭到摧毁时,属性值不会自动清空(“ 不安全”,unsafe),这一点与weak有区别。
  5. copy 此特质所表达的所属关系与strong 类似。然而设置方法并不保留新值,而是将其“拷贝”(copy)。拷贝一份“不可变” 的 ,对象中的字符值就不会无意间变动。只要实现属性所用的对象是 “可变的”(mutable),就应该在设置新属性值时拷贝一份。

方法名

getter= 指定“获取方法” 的方法名
setter= 指定“设置方法” 的方法名

通过上述特质,可以微调由编译器所合成的存取方法。
形如,

@property (nonatomic, getter=ison) BOOL on;

不过需要注意 :若是自己来实现这些存取方法,那么应该保证其具备相关属性所声明的特质,否则会误导该属性的使用者;而且, 若是不遵从这一约定,还会令程序产生bug

第 7 条 :在对象内部尽量直接访问实例变量

直接访问和通过属性的写法有一些区别

  1. 由于不经过Objective-C的“方法派发” 步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存
  2. 直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值
  3. 如果直接访问实例变量,那么不会触发KVO通知
  4. 通过属性来访问有助于排查与之相关的错误,因可以给 “获取方法 ”和/或“设置 方法” 中新增“断点”(breakpoint),监控该属性的调用者及其访问时机

所以,建议大家在读取实例变量的时候采用 直接访问 的形式,而在设置实例变量的时候 通过属性 来做

在初始化方法中总是应该直接访问实例变量,因为子类可能会“覆写” (override)设置方法

  1. 如果待初始化的实例变量声明在超类中,而我们又无法在子类中直接访问此实例变量的话,那么就需要调用 “设置方法” 了。
  2. 如果遇到“惰性初始化”(lazyinitialization)。在这种情况下,必须通过 “获取方法”来访问属性,否则,实例变量就永远不会初始化

第 8 条 :理解“对象等同性”这一概念

按照==操作符比较的是两个 指针本身 ,而不是其所指的对象
一般使用NSObject 协议中声明的“isEqual”:方法来判断两个对象的等同性

NSString 类实现了一个自己独有的等同性判断方法,名叫“ isEqual ToString:”传递给该方法的对象必须是NSString,否则结果未定义(undefined)。调用该方法比调用“isEqual:”方法快。后者还要执行额外的步骤,因为它不知道受测对象的类型。

NSObject 协议中有两个用于判断等同性的关键方法:

- (BOOL)isEqual: (id)object; 
- (NSUInteger)hash;

NSObject类对这两个方法的默认实现是:当且仅当其“指针值”(pointervalue) 完全相等 时,这两个对象才相等。若想在自定义的对象中正确覆写这些方法,就必须先理解其约定 (contract)。

如果“isEqual:” 方法判定两个对象相等,那么其hash 方法也必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:” 方法未必会认为两者相等。

isEqual方法如下
有一个EOCPerson类

@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName; 
@property (nonatomic, assign) NSUInteger age;
@end

假定如果两个EOCPerson的所有字段均相等,那么这两个对象就相等。于是“isEqual”方法可以写成


-(BOOL)isEqual: (id)object {
	//判断指针是否相等
	if (self == object) {
		return YES;
	}
	//判断所属类是否相等
	if ([self class] != [object class]) {
		return NO;
	}
	EOCPerson *otherPerson = (EOCPerson *)object;
	//逐个属性判断是否相等
	if (![_firstName isEqualToString: otherPerson.firstName]) {
		return NO;
	} 
	if (![_lastName isEqualToString: otherPerson.lastName]) {
		return NO;
	}
	if (_age != otherPerson.age) {
		return NO;
	}
	return YES;
}

hash方法如下

根据等同性约定:若两对象相等,则其哈希码 (hash) 也相等,但是两个哈希码相同的对象却未必相等

三个方法
法一:直接返回定值
法二:将NSString对象中的属性都塞入另一个字符串中,然后令hash方法返回该字符串的哈希码
法三:将各属性的哈希码相异或

//法三示例
- (NSUInteger)hash {
	NSUInteger firstNameHash = [_firstNamehash]:
	NSUInteger lastNameHash = [_lastName hash];
	NSUInteger ageHash = _age;
	return firstNameHash ^ lastNameHash ^ ageHash;
}

编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法

NSArray与NSDictionary类也具有特殊的等同性判定方法,“isEqualToArray:”与“isEqualToDictionary:” 如果和其相比较的对象不是数组或字典,那么这两个方法会各自抛出异常。由于Objective-C在编译期不做强类型检查(strong type checking),这样容易不小心传入类型错误的对象,因此开发者应该保证所传对象的类型是正确的。

如果经常需要判断等同性,那么可能会自己来创建等同性判定方法。在编写判定方法时, 也应一并覆写“ isEqual:” 方法 。后者的常见实现方式为:如果受测的参数与接收该消息的对象都属于同一个类,那么就调用自已编写的判定方法,否则就交由超类来判断 。例如,在EOCPerson类中可以实现如下两个方法

- (BOOL) isEqualToPerson: (EOCPerson*) otherPerson ( i f (self = object) return YES;
	if (![_firstName isEqualToString: otherPerson.firstName]) {
		return NO;
	}
	if (![_lastName isEqualToString: otherPerson.lastName]) {
		return NO;
	}
	if (_age =! otherPerson.age) {
		return NO;
	}
	return YES;
}
- (BOOL) isEqual: (id) object i
	if ([self class] == [object class]) {
		return [self isEqualToPerson: (EOCPerson*)object];
	} else {
		return [super isEqual: object];
	}
}

等同性判定的执行深度创建等同性判定方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断
NSArray的检测方式为先看两个数组所含对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其“ isEqual:” 方法。如果对应位置上的对象均相等,那么这两个数组就相等,这叫做 “深度等同性判定” (depequality)。不过有时候无须将所有数据逐个比较,只根据其中部分数据即可判明二者是否等同

是否需要在等同性判定方法中检测全部取决于受测对象。只有类的编写者才可以确定两个对象实例在何种情况下应判定为相等

有一种情况一定要注意,就是在容器中放入可变类对象的时候。把某个对象加入collection之后,就不应再改变其哈希码了

需要确保哈希码不是根 据对象的“可变部分” (mutableportion)计算出来的,或是保证放入collection之后就不再改变对象内容了

如果把某 对象放入set 之后又修改其内容,那么后面的行为将很难预料。并不是说绝对不能这么做,而是说如果真要这么做,那就得注意其隐患,并用相应的代码处理可能发生的问题

第 9 条 :以“类族模式”隐藏实现细节

“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。
Objective-C的系统框架中普遍使用此模式。比如想创建UIButton按钮,需要调用下面这个 “ 类方法”

+ (UIButton*) buttonWithType: (UIButtonType) type;

该方法所返回的对象,其类型取决于传入的按钮类型。然而,不管返回什么类型的对象,它们都继承自同一个基类:UIButton。这么做的意义在于:UIButton 类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。 使用者只需明白如何创建按钮,如何设置像“标题” 这样的属性,如何增加触摸动作的目标对象等问题就好。
我们可以把各种按钮的绘制逻辑都放在 一个类里,并根据按钮类型来切换。然而,若是需要依按钮类型来切换的绘制方法有许多种, 那么就会变得很麻烦

如果使用“类族模式”,该模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类后面,以保持接又简洁。用户无须自己创建子类实例,只需调用基类方法来创建即可

//首先要定义抽象基类:
typedef NS_ENUM(NSUInteger, EOCEmployeerype) {
	EOCEmployeeTypeDeveloper,
	EOCEmployeeTypeDesigner, 
	EOCEmployeeTypeFinance,
};

@interface EOCEmployee : NSObject
@property (copy) NSString *name; 
@property NSUInteger salary;
// Helper for creating Employee objects
+ (EOCEmployee*) employeeWithType: (EOCEmployeeType) type;
// Make Employees do their respective day's work - (void) doADaysWork;
@end

@implementation EOCEmployee
+ (EOCEmployee*) employeeWithType: (EOCEmployeeType) type {
	switch (type) {
		case EOCEmployeeTypeDeveloper:
			return [EOCEmployeeDeveloper new];
			break;
		case EOCEmployeeTypeDesigner:
			return [EOCEmployeeDesigner new];
			break; 
		case EOCEmployeeTypeFinance:
			return [EOCEmployeeFinance new];
			break;
	}
}
- (void) doADaysWork f
// Subclasses implement this.
@end

在本例中,基类实现了一个“类方法”,该方法根据待创建的雇员类别分配好对应雇员类实例。这种“工厂模式"(Factorypatter)是创建类族的办法之一。

如果你想创建的类中没有init初始化的方法,那么这就是在暗示你该类的实例也许不应该由用户直接创建。总而言之,以后创建对象一定不要被其的表象迷惑住了,你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。

系统框架中有许多类族。大部分collection 类都是类族 ,例如NSArray 与其可变版本 NSMutableArray。它是两个抽象基类,但是他们两个拥有相同的方法,这个方法可能就是他们共同类族中的方法,而可变数组的特殊方法就是只适用于可变数组的方法,其他的共同方法可能就是类族中的方法。

在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某个类的实例,此实例充当一个“占位数组”,也就是说,你把这个位置是先分配给其类族的,后来其类族才将这个位置分配给你创建的具体数据类型的

你若是想向NSArray已有类新增子类,那就得遵循以下规则:

  1. 子类应该继承自类族中的抽象基类。
  2. 子类应该定义自己的数据存储方式。
  3. 子类应当覆写超类文档中指明需要覆写的方法。

第 10 条 :在既有类中使用关联对象存放自定义数据

10.在既有类中使用关联对象存放自定义数据

在iOS开发里,分类是不能添加成员变量的,只允许给分类添加属性,所以出现了关联对象
可以给某对象关联许多其他对象,这些对象通过“键” 来区分。存储对象值的时候,可 以指明“存储策略” (storagepolicy),用以维护相应的“内存管理语义”
存储策略由名为 o b j c _ A s s o c i a t i o n P o l i c y 的 枚 举 所 定 义
在这里插入图片描述

void objc_setAssociatedObject (id object, void*key, id value,objc_AssociationPolicy policy)
此方法以给定的键和策略为某对象设置关联对象值。

id objc_getAssociatedObject(id object,void*key)
此方法根据给定的键从某对象中获取相应的关联对象值。

void objc_removeAssociatedObjects(id object)
此方法移除指定对象的全部关联对象。

我们可以把某对象想象成NSDictionary,把关联到该对象的值理解为字典中的条 目,于是,存取关联对象的值就相当于在NSDictionary对象上调用[object setObject:value forKey:key]与[object objectForKey:key]方法。然而两者之间有个重要差别:设置关联对象时 用的键(key)是个“不透明的指针”(opaquepointer)° 。如果在两个键上调用“isEqual:” 方法 的返回值是YES,那么NSDictionary 就认为 二者相等;然而在设置关联对象值时,若想令两 个键匹配到同 一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时, 通常使用静态全局变量做键。

关联对象很有用,但是只应该在其他办法行不通时才去考虑用它。若是滥用,则很快就会令代码失控,使其难于调试。“保留环” 产生的原因很难查明
,因为关联对 象之间的关系并没有正式的定义(formal definition),其内存管理语义是在关联的时候才定义
的,而不是在接又中预先定好的 。使用这种写法时要小心,不能仅仅因为某处可以用该写法 就一定要用它
。想创建这种UIAlertView还有个办法,那就是从中继承子类,把块保存为子类中的属性。若是需要多次用到alert
视图,那么这种做法比使用关联对象要好。

第 11 条 :理解 objc——msgSend 的作用

objc_msgSend,原 型 (prototype) 如下:

void objc_msgSend (id self, SEL cmd, ...)

这 是 个 “ 參 数 个 数 可 变 的 函 数 ” (v a r i a d i c f u n c t i o n ) , 能 接 受 两 个 或 两 个 以 上 的 参 数 。
第 一 个参数代表接收者,第二个参数代表选择子(SEL 是选择子的类型),后续参数就是消息中的 那些参数,其顺序不变。选择子指的就是方法的名字。“选择子” 与“方法” 这两个词经常 交替使用。

objc_msgSend 函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作, 该方法需要在接收者所属的类中搜寻其“方法列表 ”(listofmethods),如果能找到与选择子 名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找 到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。

调用一个方法似乎需要很多步骤。所幸objc_msgSend会将匹配结果缓存 在 “ 快 速 映 射 表 ” (t a s t m a p ) 里 面 , 每 个 类 都 有 这 样 一 块 缓 存 , 若 是 稍 后 还 向 该 类 发 送 与 选 择 子 相 同 的 消 息 , 那 么 执 行 起 来 就 很 快 了 。 当 然 啦 , 这 种 “ 快 速 执 行 路 径 ” (f a s t p a t h ) 还 是 不 如 “ 静 态 绑 定 的 函 数 调 用 操 作 ” (s t a t i c a l l y b o u n d f u n c t i o n c a l l ) 那 样 迅 速 , 不 过 只 要 把 选 择 子 缓 存 起 来 了, 也 就 不 会 慢 很 多 , 实 际 上 , 消 息 派 发 (m e s s a g e d i s p a t c h ) 并 非 应 用 程 序 的 瓶 颈 所 在 。

其他边界情况

需要交由Objective-C运行环境中的另 一些函数来处理:

• objc_msgSendLstret。如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。

• obje_msgSend_Lfpret。如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的CPU中调用函数时,需要对“浮点数寄存器"(Hloating-point register)做特殊处理, 也就是说,通常所用的objc_msgSend 在这种情况下并不合适。这个函数是为了处理 x86 等架构CPU 中某些令人稍觉惊讶的奇 状况。

• objc_msgSendSuper。如果要给超类发消息,例如[supermessage:parameter],那么就交由此函数处理。也有另外两个与objc_msgSend1 _stret 和objc_msgSend_fpret 等效的函数,用于处理发给super 的相应消息。

刚才曾提到,objc_msgSend等函数一旦找到应该调用的方法实现° 之后,就会“跳转过去”。 之所以能这样做,是因为Obj ective -C对象的每个方法都可以视为简单的C 函数,其原型如下:

<return_type> Class_selector(id self, SEL _cmd, ...)

真 正 的 函 数 名 和 上 面 写 的 可 能 不 太 一 样 , 笔 者 用 “ 类 ” (c l a s s ) 和 “ 选 择 子 " (s e l e c t o r ) 来 命名是想解释其工作原理。每个类里都有 一张表格,其中的指针都会指向这种函数,而选择 子的名称则是查表时所用的 “键” 。objc_msgSend 等函数正是通过这张表格来寻找应该执行 的方法并跳至其实现的。

如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化” 技术。 编译器会生成调转至另 一函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”(frame stack)。只有当某函数的最后 一个操作仅仅是调用其他函数而不会将其返回值另作他用时, 才能执行“尾调用优化” 。这项优化对obj c_msgSend 非常关键,如果不这么做的话,那么每 次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备 “栈帧”,大家在“栈踪 迹” (stacktrace)中可以看到这种“栈帧”。此外,若是不优化,还会过早地发生“栈溢出” (s t a c k o v e r f l o w ) 现 象 。

第 12 条 :理解消息转发机制

若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行。但是,在编译 期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以 编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息 后,就会启动 “消息转发” (message forwarding)机制,程序员可经由此过程告诉对象应该如 何处理未知消息。

-[
NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87
*** Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '-[__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87'

上面这段异常信息是由NSObject的“doesNotRecognizeSelector:” 方法所抛出的,此 异常表明:消息接收者的类型是__NSCFNumber,而该接收者无法理解名为lowercaseString 的选择子。

控制台中看到的那个_ NSFCNumber是为了实现“无缝桥接”而 使 用 的 内 部 类 (i n t e r n a l c l a s s ), 配 置 N S N u m b e r 对 象 时 也会一并创建此对象

这里消息转发过程以应用程序崩溃而告终,不过,开发者在编 写自己的类时,可于转发过程中设置挂钩,用以执行预定的逻辑,而不使应用程序崩溃

  1. 消息转发阶段
    消息转发分为两大阶段,第一阶段先征询接收者,所属的类,看其是否能动态添加方法,处理当前这个“未知的选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。

  2. 动态方法解析
    对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector;

该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与“resolveInstanceMethod:”类似,叫做“resolveClassMethod”。

使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面 就可以了。 此方案常用来实现@dynamic属性(参见第6条), 比如说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法 在编译期就能确定。

  1. 备援接收者
    当接收者还有第二次机会能处理未知的选择子,在这一步中运行期系统会问它:能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector;

方法参数代表未知的选择子,若当前接收者能找到各授对象,则将其返回,若找不到, 就 返 回 nil。 通过此方案,我们可以用“组合”(composition) 来模拟出“多重继承”( multipleinheritance )的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法 将能够处理某选择 子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。
请注意,我们无法操作经由这 一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。

4.完整的消息转发
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制 了。 首先创建NSInvocation 对象,把与尚未处理的那条消息有关的全部细节都封于其中。 此对象包含选择子、目标(target )及参数。在触发NSInvocation 对象时,“ 消息派发系统”
( message-dispatchsystem )将亲自出马,把消息指派给目标对象。
此步骤会调用 下列方法来转发消息:

 - (void)forwardInvocation:(NSInvocation*)invocation

这个方法可以 实现得很简单: 只需改变调用目标,使消息在新目标 上得以调用即可。然 而这样实现出来的方法与“备援接收者〞方案所实现的方法等效, 所以很少有人采用这么简 单的实现方式。比较有用的实现方式为: 在触发消息前, 先以某种 方式改变消息内容, 比如 追加另外一个参数,或是改换选择子,等等。
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话, 继承体系中的每个类都有机会处理此调用请求, 直至NSObject。 如果最后调用了NSObject 类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:” 以拋出异常, 此异常表明选择 子最终未能得到处理。

接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在 第
一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍
后还收到同名选择子,那么根本无须启动消息转发流程。若想在第三步里把消息转给备援的
接收者,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放 在第 二步执行会更为简单
,不然的话,还得创建并处理完整的NSInvocation。

第13条:用“方法调配技术” 调试“黑盒方法”

在运行期改变与给定的选择子名称相对应的方法,此方案经常称为 “方法调配”。
类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做 IMP,其原型如下:

id (*IMP)(id, SEL, ...)

在运行期,可以向类中新增或替换选择子所对应的方法实现。
使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

第14条:理解“类对象 ”的用意

“在运行期检视对象类型”这一操作也叫做“类型信息查询”,这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。
每个Objective-C对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要跟一个“*”字符:

NSString *pointerVariable = @"Some string"; 

对于通用的对象类型id,由于其本身已经是指针了,所以我们能够这样写:

id genericTypedString = @"Some string"; 

上面这种定义方式与用NSString*来定义相比,其语法意义相同 唯一区别在于,如果声明时指定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会探知此情况,并发出警告信息
id类型本身定义在这里:

typedef struct objc_object {  
    Class isa;  
} *id; 

由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常 称为“isa”指针
Class对象也定义在运行期程序库的头文件中:

typedef struct objc_class *Class;  
struct objc_class {  
    Class isa;  
    Class super_class;  
    const char *name;  
    long version;  
    long info;  
    long instance_size;  
    struct objc_ivar_list *ivars;  
    struct objc_method_list **methodLists;  
    struct objc_cache *cache;  
    struct objc_protocol_list *protocols;  
}; 

此结构体存放类的“元数据”,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为Objective-C对象。
结构体里还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。
“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

在类继承体系中查询类型信息
可以用类型信息查询方法来检视类继承体系。

“isMemberOfClass:”能够判断出对象是否为某个特定类的实例。
“isKindOfClass:”能够判断出对象是否为某类或其派生类的实例。

由于Objective-C使用“动态类型系统”,所以用于查询对象所属类的类型信息查询功能非常有用。从collection中获取对象时,通常会查询类型信息,这些对象不是“强类型的”,把它们从collection中取出来时,其类型通常是id。如果想知道具体类型,那就可以使用类型信息查询方法。
也可以用比较类对象是否等同的办法来做。若是如此,那就要使用==操作符。应该尽量使用类型信息查询方法。

每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。

第 15 条:用前缀避免命名空间冲突

OC语言中没有其他语言那种内置的命名空间机制,所以我们在对文件命名时要十分的注意,若是发生重名冲突,那么应用程序相应的链接过程就会出错,导致运行文件不知道究竟该调用那个文件,因为其中出现了重复的符号。 也就是理解成命名的时候需要加上前缀,比如一个项目里存在很多歌ViewController的子类,那么在命名的时候需要分清楚子类的名称,加上前缀总是好的 使用Cocoa 创建应用程序时一定要注意,Apple 宣称其保留使用所有“两字母前缀” (two-letterprefix)的权利,所以你自己选用的前缀应该是三个字母的。

选择与你的公司、应用程序或者二者皆有关联的名称作为类名的前缀,并在所有代码中均使用这一前缀。
若是自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀。

第 16 条:提供 “全能初始化方法”

所有对象均要初始化。初始化时,有些对象可能无须开发者向其提供额外信息,不过一般来说还是要提供的。以iOS的UI框架UIKit为例,其中有个类叫做UITableViewCell,初始化该类对象时,需要指明其样式及标识符,标识符能够区分不同类型的单元格。由于这种对象的创建成本较高,所以绘制表格时可依照标识符来复用,以提升程序效率。
我们把这种可为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”。(实际上就是在初始化时直接提供需要的数据)

全能初始化

如果创建类实例的方式不止一种,那么这个类就会有许多个初始化方法。

在这个过程里有一个方法使得其他方法初始化的时候必须调用它,那么这个方法称“全能初始化方法”。
只有在全能初始化方法中,才会存储内部数据,其他的方法只是在存储内部数据后再进行了一些其他操作而已

在类中提供一个全能初始化方法,并与文档里指明。其他初始化方法均应调用此方法。
若全能初始化方法与超类不同,则需覆写超类中的对应方法。 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

一个类有多个全能初始化方法要注意,我们就是要维持原来类的调用链,每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。因为其父类有两个全能初始化方法,这两种初始化方法定义出来的数据可能是不同的,若是你在子类中调用了错误的父类初始化方法,它就会可能因为数据类型的问题使程序发生错误

重写初始化方法也要注意如果子类的全能初始化方法与超类方法的名称不同,我们总应覆写超类的全能初始化方法,避免子类调用父类的全能初始化方法

第 17 条:实现description 方法

调试程序时,经常需要打印并查看对象信息。一种办法是编写代码把对象的全部属性都输出到日志中。不过最常见的做法还是像下面这样:

NSLog(@"object = %@", object);

在构建需要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代“格式字符串”里的%@。比方说,object是个数组,若用下列代码打印其信息:

NSArray *object = @[@"A string", @(123)];
NSLog(@"object = %@", object);

输出:

object = {
	"A string"
	123
}

如果是一个自定义的类,则输出:

object = <EOCPerson: 0x7fd9a1600600>

这时候就需要我们重写description方法

例如下:

- (NSString *) description {
	return [NSString stringWithFormat:@"<%@: %p, %@>",
			[self class],
			self,
			@{@"latitude":_title,
			@"latitude":@(_latitude),
			@"longitude":@(_longitude)}
		];
} 

输出:

location = <EOCLocation: 0x7f98f2e01d20, {
	latitude = "51.506"
	longitude = 0;
	title = London;
}>

debugDescription:

这个也是一种描述方法,和description差不多,就是描述的位置不一样,description是在函数调用类的时候触发方法才输出的,而debugDescription是在控制台中使用命令打印该对象时才调用的。当然加断点查看时也可以看到debugDescription的描述。

如果你在description不想将一些内容输出的话,你就可以将那些数据写在debugDescription中,让程序员自己调试时可以方便的看到这些数据,而description方法就输出你想要让用户看到的信息就行了。

要点:

实现description方法返回一个有意义的字符串,用以描述该实例。
若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。

第 18 条:尽量使用不可变对象

这里的不可变对象不是使用系统提供的不可变类,而是在设置属性的时候添加上readnoly属性

默认情况下的属性是既可读又可写的,这样的类我们在这里成为可变类,但一般的数据未必需要改变。

例如:

@property (nonatomic, copy, readonly) NSString *identifier; @property (nonatomic, copy, readonly) NSString *title; @property (nonatomic, assign, readonly) float latitude;

现在,这个属性就只能用在实现代码内部设置这些属性了,但其实,在对象外部还可以通过“键值编码”技术来设置这些属性,就像“setValue:forKey:”方法。“点语法”也可以,因为点语法就是调用set方法的。这样做虽说可以改动,但是却违背了本心,还会导致数据不同而出现问题,所以不建议更改。

[pointOfInterest setValue:@"abc" forKey:@"identifier"];

这样子可以改动属性值,因为KVC会在类里查找“setIdentifier:”方法,并借此修改此属性。即使没有于公共接口中公布此方法,它也依然包含在类中。不过,这样做等于违规地绕过了本类所提供的API,要是开发者使用这种“杂技代码”的话,那么得自己开应对可能出现的问题。
还有一种可以修改数据的方法就是直接用类型信息查询功能查出属性所对应的实例变量在内存布局中的偏移量,以此来人为设置这个实例变量的值。这样做比绕过本类的公共API还要不合规范。所以不应该因为这个原因而忽视所提的建议,大家还是要尽量编写不可变的对象。

尽量创建不可变的对象
若某属性进可于对象内部修改,则在“分类”中将其由属性扩展为readwrite属性
不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。

第 19 条:使用清晰而协调的命名方式

给方法命名时的注意事项可总结成下面几条规则:

如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还 有修饰语,例如localized String。属性的存取方法
不遵循这种命名方式,因为一般认 为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝, 我们也认为那相当
于原有的对象。这些存取方法应该按照其所对应的属性来命名。
应该把表示参数类型的名词放在参数前面。 例如:
其中width就是名词
如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数, 则应该在动词后面加 上一个或多个名词。
不要使用str 这种简称,应该用string 这样的全称。
Boolean 属性应加is 前缀。如果某方法返回非属性的Boolean 值,那么应该根据其功 能,选用has 或is 当前缀。
将get 这个前缀留给那些借由“输出参数〞来保存返回值的方法,比如说,把返回值填充到〝C语言式数组” ( C - style array ) 里的那种方法就可以使用这个词做前缀 。
类与协议的命名:

应该为类与协议的名称加上前缀,以避免命名空间冲突,而且应该像给方法起名时那样把词句组织好,使其从左至右读起来较为通顺。基本命名规则就是:命名方式应该一致,如果要从其他的类中继承子类,那么就要遵守其原本的命名惯例。 例如:UIView它的子类就应该是***View,表明其来历。

起名时应遵从标准的OC命名规范,这样创建出来的接口更容易为开发者所理解。
方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
方法名里不要使用缩略后的类型名称。
给方法名起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。

第 20 条:为私有方法名加前缀

给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。

不要单用一个下划线做私有方法的前缀(_xxx),因为这种做法是预留给苹果公司用的。

第 21 条 : 理解Objective-C错误模型

当前很多编程语言都有“异常”(exception)机制,OC也不例外。
首先要注意的是,“自动引用计数”(Automatic Reference Counting, ARC)在默认情况下不是“异常安全的”(exception safe)。具体来说,这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions。可是在释放资源之前如果抛出异常了,那么该资源就不会被释放了:

id someResource = /*...*/;
if (/*check for error*/) {
	@throw [NSException exceptionWithName:@"ExceptionName" reason:@"There was an error" userInfo:nil];
}
[someResource doSomething];
[someResource release];

在抛出异常前先释放someResource,这样做当然能解决此问题,不过要是待释放的资源有很多,而且代码的执行路径更为复杂的话,那么释放资源的代码就容易写的很乱。此外,代码中加入了新的资源之后,开发者经常会忘记在抛出异常前先把它释放掉。
OC语言现在采用的方法是:只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。 也就是说,不用再编写复杂的“异常安全”的代码了。
异常只用来处理严重错误(fatal error,致命错误);对于“不那么严重的错误”(nonfatal error,非致命错误),OC语言所采用的编程范式为:令方法返回nil/0,或是使用NSError,以表明其中有错误发生。如:

 - (id)initWithValue:(id)value {
	if (self = [super init]) {
		if (/*Value means instance can't be created*/) {
			self = nil;
		} else {
			//Initialize instance
		}
	}
	return self;
}

这种情况下,如果if语句发现无法用传入的参数值来初始化当前实例,那么就把self设置成nil,这样的话,整个方法的返回值也就是nil了。调用者发现初始化方法并没有2把实例创建好,于是便可以知道其中发生了错误。

NSError的用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给调用者。NSError对象里封装了三条消息:

Error domain(错误范围,类型为字符串)产生错误的根源,通常用一个特有的全局变量来定义。 Error
code(错误码,类型为整数)独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围可能会发生一系列相关错误,这些错误通常采用enum定义。
User info(用户信息,类型为字典)有关错误的额外信息,其中或许包含一段“本地化描述”(localized
description),或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”(chain of
errors)。

只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
在错误不那么严重的情况下,可以指派“委托方法”来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。

第 22 条:理解 NSCopying 协议

我们经常会使用copy函数,但是若是你自定义的类,他自己就不会实现这个函数,此时就需要你自己来实现了,要实现copy函数就的实现NSCopying协议,该协议只有一个方法:

- (id)copyWithZone:(NSZone *)zone;

copy方法由NSObject实现,该方法只是以“默认区”为参数来调用“copyWithZone:”。所以要实现copy函数,他才是关键。
想要重写copy函数,要声明该类遵从NSCopying协议,并实现其中的方法。

#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName; @property (nonatomic, copy, readonly) NSString *lastName;
- (id) initwithFirstName: (NSString*)firstName andLastName: (NSString*) lastName;
@end

实现协议中规定的方法:

- (id) copyWithZone: (NSZone*)zone {
	EOCPerson *copy = [[[self class] allocWithZone: zone] initwithFirstName: firstName andLastName: lastName];
	return copy; 
}

若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
如果自定义的对象分为可变版本和不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。
复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法

第 29 条:理解引用计数

Objective-C 使用引用计数来管理内存:每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数:用完了之后,就递减其计数。计数变为 0时,就可以把它销毁。
在ARC中,所有与引用计数有关的方法都无法编译(由于 ARC 会在编译时自动插入内存管理代码,因此在编译时,所有与引用计数相关的方法都会被 ARC 替换为适当的代码)。

在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在 Objective-C 中叫做 “保留计数”也可以叫 “引用计数”。NSObject 协议声明了下面三个方法用于操作计数器,以递增或递减其值:

Retain 递增保留计数。 release 递减保留计数。 autorelease 待稍后清理 “自动释放池”时,再递减保留计数。
查看保留计数的方法叫做 retainCount,不推荐使用。

对象创建出来时,其保留计数至少为 1。若想令其继续存活,则调用 retain 方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用 release 或 autorelease 方法。最终当保留计数归零时,对象就回收了,也就是说系统会将其占用的内存标记为 “可重用”。此时,所有指向该对象的引用也都变得无效。
应用程序在其生命周期中会创建很多对象,这些对象都相互联系着。例如,表示个人信息的对象会引用另一个表示人名的字符串对象,还可能会引用其他个人信息对象,比如在存放朋友的 set 中就是如此。这些相互关联的对象就构成了一张 “对象图”。对象如果持有指向其他对象的强引用,那么前者就 “拥有” 后者。对象想令其所引用的那些对象继续存活,就可将其 “保留”。等用完了之后,再释放。
按 “引用树” 回溯,那么最终会发现一个 “根对象”。在 iOS 应用程序中,它是 UIApplication 对象。是应用程序启动时创建的单例。
如下面这段代码:

NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];

//不能假设number对象一定存活
NSLog(@"number = %@", number);//***
//因为对象所占的内存在 “解除分配”之后,只是放回 “可用内存池”。如果执行 NSLog 时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。
//因过早释放对象而导致的 bug 很难调试。

[array release];

在上面这段代码中:创建了一个可变数组 array,然后创建了一个 NSNumber 对象 number,并将其添加到数组中,数组也会在 number 上调用retain 方法,以期继续保留此对象,这时number的引用计数至少为2。接着,我们试着通过 release 方法释放了 number 对象,因为数组对象还在引用着number对象,因此它仍然存活,但是不应该假设它一定存活。最后,通过调用 release 方法释放了数组 array,确保了内存的正确管理。上面这段代码在ARC中是无法编译的,因为调用了release方法。
调用者通过 alloc 方法表达了想令该对象继续存活下去的意愿,不过并不是说对象此时的保留计数必定是1。在 alloc 或 “initWithInt:” 方法的实现代码中,也许还有其他对象也保留了此对象(如初始化过程中的委托调用、通知其他对等),所以,其保留计数可能会大于 1。

为避免在不经意间使用了无效对象,一般调用完 release 之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为 “悬挂指针”。
可以这样编写代码来防止此情况发生:

NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;

刚才那个例子中的数组通过在其元素上调用 retain 方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这一般通过访问 “属性”来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为 “strong 关系”,则设置的属性值会保留。假设有个名叫 foo 的属性由名为 _foo 的实例变量所实现,那么,该属性的设置方法会是这样:

- (void)setFoo:(id)foo {
  [foo retain];
  [_foo release];
  _foo = foo;
 }

当设置属性 foo 的新值时,属性的设置方法会依次执行以下操作:

保留新值:使用 retain 方法保留新值,确保其在设置方法之外仍然可用。
释放旧值:使用 release 方法释放旧值,以减少其保留计数,并在不再需要时释放内存。
更新实例变量:将实例变量 _foo 的引用指向新值。
上面这些操作的顺序很重要。如果在保留新值之前就释放了旧值,并且旧值和新值指向同一个对象,那么在释放旧值时可能会导致对象被系统回收,而在后续保留新值时,该对象已经不存在了。这就会导致 _foo 成为一个悬挂指针,即指向已经释放的内存空间,这样的指针是无效的,并且可能导致应用程序崩溃或出现其他问题。
自动释放池
自动释放池(autorelease)是 Objective-C 内存管理的重要特性之一。
简单来说它允许开发者推迟对象的释放时间,通常在下一个事件循环中才执行释放操作。
比如说有这个代码:

- (NSString *)stringValue {
    NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
    return str;
}

在上面这段代码中,str对象在stringValue方法的作用域中被alloc出来,因此它的引用计数为1,但是因为它的作用域是stringValue方法,因此我们期望的是它在该方法结束前引用计数应为0,但是因为缺少了释放操作,因此该str对象的引用计数为1比期望值多1。
但是我们又不能直接在stringValue方法中将str对象释放,否则还没等方法返回,系统就把该对象回收了。这里应该用 autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越 “方法调用边界”(method callboundary)后一定存活。

因此我们需要改写 stringValue 方法,使用 autorelease 来释放对象:

- (NSString *)stringValue {
    NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
    return [str autorelease];
}

改写后使用了 autorelease 方法将str对象放入自动释放池中,以延迟其释放时间。这样,在方法返回时,对象的引用计数会保持预期的值,而不会多出 1。
可以像下面这样使用 stringValue 方法返回的字符串对象:

NSString *str = [self stringValue];
NSLog(@"The string is: %@", str);

在第一段代码中,NSString *str = [self stringValue]; 返回的字符串对象 str 是一个被放入自动释放池的对象。因此,尽管没有显式地调用 retain 方法,但在 NSLog(@“The string is: %@”, str); 之后,str 对象的引用计数不会被减少。这是因为该对象在自动释放池中,直到下一个事件循环才会被释放。
但是,如果你需要在稍后持有这个对象,比如将它设置给一个实例变量,那么你需要手动增加其引用计数,以防止在自动释放池释放时对象被释放,比如像这样,假设在另一个地方创建了一个 ExampleClass 的实例,并希望将stringValue返回的对象设置为该实例的 instanceVariable:

ExampleClass *exampleObject = [[ExampleClass alloc] init];
NSString *str = [exampleObject stringValue];

exampleObject.instanceVariable = str;

NSLog(@"The instance variable is: %@", exampleObject.instanceVariable);

则需要确保 str 对象不会在自动释放池被释放时被释放。因此,我们需要手动增加 str 对象的引用计数,以确保它不会被过早释放:

//手动增加引用计数
exampleObject.instanceVariable = [str retain];
//......

//并且在稍后手动释放
[exampleObject.instanceVariable release];

用引用计数机制时,经常要注意的一个问题就是 “保留环”,也就是呈环状相互引用的多个对象。这将导致内存泄漏,因为循环中的对象其保留计数不会降为 0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。图里的每个对象都引用了另外两个对象之中的一个。在这个循环里,所有对象的保留计数都是 1。
在垃圾收集环境中,通常将这种情况认定为 “孤岛”。此时,垃圾收集器会把三个对象全都回收走。而在 Objective-C 的引用计数架构中,则享受不到这一便利。通常采用 “弱引用”来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环,从而避免内存泄漏。

引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为 1。若保留计数为正,则对象继续存活。当保留计数降为 0 时,对象就被销毁了。
在对象生命期中,其余对象通过引用来保留或释放对象。保留于释放操作分别会递增及递减保留计数。

第 30 条:以ARC简化引用计数

引用计数这个概念相当容易理解。需要执行保留与释放操作的地方也很容易就能看出来。所以 Clang 编译器项目带有一个 “静态分析器”。用于指明程序里引用计数出问题的地方。
比如再拿上一条中的这段代码举例子:

  if ([self shouldLogMessage]) {
      NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
      NSLog(@“str = %@, str);
  }

这段代码中,alloc增加了str对象的引用计数,但是在这个if块的作用域中,它却缺少了释放操作,因此str对象的引用计数比预期值多1,导致了内存泄漏。因为上述这些规则很容易表述,所以计算机可以简单地将其套用在程序上,从而分析出有内存泄漏问题的对象。这正是 “静态分析器” 要做的事。

静态分析器还有更为深入的用途。既然可以查明内存管理问题,那么应该也可以根据需要,预先加入适当的保留或释放操作以避免这些问题。自动引用计数的思路就是源于此。
因此假如使用了ARC,它就会自动将代码改写为这样:

  if ([self shouldLogMessage]) {
      NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
      NSLog(@“str = %@, str);
      [message release];
  }

使用 ARC 时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在是由 ARC 自动为你添加。
由于 ARC 会自动执行 retain、release 、autorelease 等操作,所以直接在 ARC 下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:

  1. retain
  2. release
  3. autorelease
  4. dealloc

直接调用上述任何方法都会产生编译错误,因为 ARC 要分析何处应该自动调用内存管理方法,所以如果手工调用的话,就会干扰其工作。
实际上,ARC 在调用这些方法时,并不通过普通的 Objective-C 消息派发机制,而是直接调用其底层 C 语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多 CPU 周期。
使用 ARC 时必须遵循的方法命名规则
ARC 将内存管理语义在方法名中表示出来确立为硬性规定。
简单地体现在方法名上。若方法名以下列词语开头,则其调用上述四种方法的那段代码要负责释放方法所返回的对象:

  1. alloc
  2. new
  3. copy
  4. mutableCopy

若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。 在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。
(我自己简单理解就是使用 “alloc”、“new”、“copy” 或者 “mutableCopy” 开头的方法,方法内部的引用计数要自己手动管理(release或者autorelease等),而不使用这四个开头的方法的引用计数就是自动管理的)。
除了会自动调用 “保留” 与 “释放” 方法外,使用 ARC 还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化,例如,在编译器,ARC 会把能够互相抵消的retain、release、autorelease 操作约简。如果发现在同一对象上执行了多次 “保留” 与 “释放” 操作,那么 ARC 有时可以成对地移除这两个操作。
ARC 也包含运行期组件。此时所执行的优化很有意义,大家看到之后就会明白为何以后的代码都应该用 ARC 来写了。前面讲到,某些方法在返回对象前,为其执行了 autorelease 操作,而调用方法的代码可能需要将返回的对象保留,比如像下面这种情况就是如此:

_myPerson = [EOCPerson personWithName:@"Bob smith"];

调用 “personWithName:” 方法会返回新的 EOCPerson 对象,而此方法在返回对象之前,为其调用了 autorelease 方法。由于实例变量是个强引用,所以编译器在设置其值的时候还需要执行一次保留操作。因此,前面那段代码与下面这段手工管理引用计数的代码等效:

EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"];
_myPerson = [tmp retain];

此时应该能看出来, “personWithName:” 方法里面的 autorelease 与上段代码中的 retain 都是多余的。为提升性能,可将二者删去。但是,在 ARC 环境下编译代码时,必须考虑 “向后兼容性”(backward compatibility),以兼容那些不使用 ARC 的代码。
在 ARC 环境下,编译器会尽可能地优化代码,以提高性能和效率。在处理方法中返回自动释放的对象时,编译器可以通过一些特殊的函数来优化代码,从而避免不必要的 autorelease 和 retain 操作,提升代码的执行效率。
具体来说,在方法中返回自动释放的对象时,编译器会替换对 autorelease 方法的调用,改为调用 objc_autoreleaseReturnValue 函数。这个函数会检查方法返回后即将执行的代码,如果发现需要在返回的对象上执行 retain 操作,那么就会设置一个标志位,而不会立即执行 autorelease 操作。类似地,如果调用方法的代码需要保留返回的自动释放对象,那么编译器会将 retain 操作替换为 objc_retainAutoreleasedReturnValue 函数,该函数会检查之前设置的标志位,如果已经设置,则不会执行 retain 操作。
objc_autoreleaseReturnValue 函数检测方法调用者是否会立刻保留对象要根据处理器来定。

变量的内存管理语义
ARC 也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。
在编写设置方法(setter)时,使用 ARC 会简单一些。如果不用 ARC ,那么需要像下面这样来写:

- (void)setObject:(id)object {
    [_object release];
    _object = [object retain];
}

但是在这段代码中,如果新值和实例变量已有的值相同,那么在执行设置方法时会出现问题。具体来说,当新值和旧值相同时,首先会调用 [_object release] 来释放旧值,此时如果旧值只有当前对象在引用,那么旧值的引用计数会减少为0。接着,会调用 [object retain] 来保留新值,但是此时旧值的内存已经被释放掉了,再次对其执行保留操作就会导致访问已释放的内存,从而引发应用程序崩溃。
使用 ARC 之后,就不可能发生这种疏失了。在 ARC 环境下,与刚才等效的设置函数可以这么写:

- (void)setObject:(id)object {
    _object = object;
}

ARC 会用一种安全的方式来设置:先保留新值,再释放旧值,最后设置实例变量。用了 ARC 之后,根本无须考虑这种 “边界情况”。

在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:

__strong: 默认语义,保留此值。
__unsafe_unretained: 不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
__weak: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。
__autoreleasing: 把对象 “按引用传递” (pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。

比方说,想令实例变量的语义与不使用 ARC 时相同,可以运用 __weak 或 __unsafe_unretained 修饰符:

@interface EOCClass : NSObject {
    __weak id _weakObject;
    __unsafe_unretained id _unsafeUnretainedObject;
}
@end

我们经常会给局部变量加上修饰符,用以打破由“块”,所引入的“保留环”。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致 “保留环”。可以用 __weak 局部变量来打破这种 “保留环”:

NSURL *url = [NSURL URLWithString:@"http://www.example.com/"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
EOCNetworkFetcher *__weak weakFetcher  = fetcher;

[fetcher startWithCompletion:^(BOOL success) {
    NSLog(@"Finished fetching from %@", weakFetcher.url);
}];

在这段代码中,我们使用了 __weak 修饰符来声明一个局部变量 weakFetcher,它指向 fetcher 对象。通过使用 __weak 修饰符,我们避免了块对 fetcher 对象的强引用,从而打破了潜在的保留环。

ARC 如何清理实例变量
ARC 也负责对实例变量进行内存管理。要管理其内存,ARC 就必须在 “回收分配给对象的内存”(deallocate)(也称为 “释放/回收/解除分配(内存)”) 时生成必要的清理代码。凡是具备强引用的变量,都必须释放,ARC 会在 dealloc 方法中插入这些代码。当手动管理引用计数时可以这样自己来编写 dealloc 方法:

- (void)dealloc {
    [_foo release];
    [_bar release];
    [super dealloc];
}

这段代码做了以下几件事情:

释放 _foo 实例变量:调用 release 方法来减少对 _foo 对象的引用计数,如果引用计数为0,则会释放 _foo 对象所占用的内存。
释放 _bar 实例变量:同样地,调用 release 方法来减少对 _bar 对象的引用计数,如果引用计数为0,则会释放 _bar 对象所占用的内存。
调用 super 的 dealloc 方法:调用父类的 dealloc 方法来执行一些必要的清理工作,确保对象的内存被正确释放。
使用 ARC 之后,不需要再编写这种 dealloc 方法。不过,如果有非 Objective-C 的对象,比如 CoreFoundation 中的对象或是由 malloc() 分配在堆中的内存,那么仍然需要清理。然而不需要像原来那样调用超类的 dealloc 方法。前文说过,在 ARC 下不能直接调用 dealloc。ARC 会自动在 .cxx_destruct 方法中生成代码并运行此方法,而在生成的代码中会自动调用超类的 dealloc 方法。ARC 环境下,dealloc 方法可以像这样写:

- (void)dealloc {
    CFRelease(_coreFoundationObject);
    free(_heapAllocatedMemoryBlob);
}

可以使用CFRelease() 函数来释放CoreFoundation 对象。
可以使用 free() 函数来释放通过 malloc() 函数分配在堆上的内存块_heapAllocatedMemoryBlob

不使用 ARC 时,可以覆写内存管理方法。比方说,在实现单例类的时候,因为单例不可释放,所以我们经常覆写 release 方法,将其替换为 “空操作”(no-op)。但在 ARC 环境下不能这么做,因为会干扰到 ARC 分析对象生命期的工作。而且,由于开发者不可调用及覆写这些方法,所以 ARC 能够优化 retain、release、autorelease 操作,使之不经过 Objective-C 的消息派发机制。优化后的操作,直接调用隐藏在运行期程序中的 C 函数。

有 ARC 之后,程序员就无须担心内存管理问题了。使用 ARC 来编程,可省去类中的许多 “样板代码”。
ARC 管理对象生命期的办法基本上就是:在合适的地方插入 “保留” 及 “释放”操作。
在 ARC 环境下,变量的内存管理语义可以通过修饰符指明,而原来需要手工执行 “保留” 及 “释放”操作。
由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC 将此确定为开发者必须遵守的规则。
ARC 只负责管理 Objective-C 对象的内存。尤其要注意: CoreFoundation 对象不归 ARC 管理,开发者必须适时调用 CFRetain/CFRelease。

第 31 条:在 dealloc 方法中只释放引用并解除监听

对象在经历其生命期后,最终会为系统所回收,这时就要执行 dealloc 方法了。在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为 0 的时候。然而具体何时执行,则无法保证。也可以理解成: 我们能够通过人工观察保留操作与释放操作的位置。来预估此方法何时即将执行。但实际上,程序库会以开发者察觉不到的方式操作对象,从而使回收对象的真正时机和预期的不同。你决不应该自己调用 dealloc 方法,运行期系统会在适当的时候调用它。而且,一旦调用过 dealloc 之后,对象就不再有效了,后续方法调用均是无效的。
在 dealloc 方法中主要就是释放对象所拥有的引用。对象所拥有的其他非 Objective-C 对象也要释放。比如 CoreFoundation 对象就必须手工释放,因为它们是由纯C 的API 所生成的。
在 dealloc 方法中,通常还要做一件事,那就是把原来配置过低观测行为都清理掉,比如消息通知的回收。
delloc应该这样写:

- (void)dealloc {
    CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

如果手动管理引用计数而不使用 ARC 的话,那么最后还需调用 “[super dealloc]”。ARC 会自动执行此操作。
开销较大或系统内部稀缺的资源不应该于 dealloc 中释放引用。像是文件描述符、套接字、大块内存等,都属于这种资源。不能指望 dealloc 方法必定会在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。
比方说,如果某对象管理着连接服务器所用的套接字,那么也许就需要这种 “清理方法”。此对象可能要通过套接字连接到数据库。对于对象所属的类,其接口可以这样写:

#import <Foundation/Foundation.h>

@interface EOCServerConnection : NSObject

- (void)open:(NSString *)address;

- (void)close;

@end

这段代码提供了两个方法:

open: 方法:用于打开连接到服务器的套接字。它需要一个字符串类型的参数 address,表示服务器的地址。
close 方法:用于关闭当前打开的连接。
这个类的设计允许用户通过 open: 方法打开连接,然后使用完成后通过 close 方法关闭连接
在清理方法而非 dealloc 方法中清理资源还有个原因,就是系统并不保证每个创建出来的对象的 dealloc 都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到 dealloc 消息。由于应用程序终止之后,其占用的资源也会返还给操作系统,所以实际上这些对象也就等于是消亡了。不调用 dealloc 方法是为了优化程序效率。而这也说明系统未必会在每个对象上调用其 dealloc 方法。
如果对象管理着某些资源,那么在 dealloc 中也要调用 “清理方法”,以防止开发者忘了清理这些资源。
在系统回收对象之前,必须调用 close 以释放其资源,否则 close 方法就失去了意义了,因此,没有适时调用 close 方法就是编程错误,我们应该在 dealloc 中补上这次调用,以防泄漏内存。下面举例说明 close 与 dealloc 方法如何来写:

- (void)close {
    /*clean up resources*/
    _closed = YES;
}

- (void)dealloc {
    if (!_closed) {
        NSLog(@"ERROR: close was not called before dealloc!");
        [self close];
    }
}

编写 dealloc 方法时还需要注意,不要在里面随便调用其他方法。
调用dealloc 方法的那个线程会执行“最终的释放操作”,令对象的保留计数降为 0,而某些方法必须在特定的线程里(比如主线程里)调用才行。若在 dealloc 里调用了那些方法,则无法保证当前这个线程就是那些方法所需的线程。通过编写常规代码的方式,无论如何都没办法保证其会安全运行在正确的线程上,因为对象处于 “正在回收的状态”,为了指明此状况,运行期系统已经改动了对象内部的数据结构。
在 dealloc 里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并与其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于 “键值观测” (KVO) 机制的监控之下,该属性的观察者(observer) 可能会在属性值改变时 “保留” 或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调,从而导致一些莫名其妙的错误。

在 dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”(KVO)或 NSNOtificationCenter 等通知,不要做其他事情。
如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定: 用完资源后必须调用 close 方法。
执行异步任务的方法不应该在 dealloc 里调用; 只能在正常状态下执行的那些方法也不应在 dealloc 里调用,因为此时对象已处于正在回收的状态了。

第 32 条:编写 “ 异常安全代码” 时 留意内存管理问题

许多时下流行的编程语言都提供了 “异常”这一特性。在当前的运行期系统中,C++ 与 Objective-C 的异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编写的 “异常处理程序”来捕获。
Objective-C 的错误模型表明,异常只应在发生严重错误后抛出,不过有时仍然需要编写代码来捕获并处理异常。比如使用 Objective-C++ 来编码时,或是编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制时,就需要捕获及处理异常了。此外,有些系统库也会用到异常,比如,在使用 “键值观测”(KVO)功能时,若想注销一个尚未注册的“观察者”,便会抛出异常。
在 try 块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非 catch 块能处理此问题,否则对象所占内存就将泄漏。
异常处理例程将自动销毁对象,然而在手动管理引用计数时,销毁工作有些麻烦。以下面这段使用手工引用计数的 Objective-C 代码为例:

@try {
    EOCSomeClass *object = [[EOCSomeClass alloc] init];
    [object doSomethingThatMayThrow];
    [object release];
}
@catch (...) {
    NSLog(@"Whoops, there was an error. Oh well...");
}

这段代码使用了 Objective-C 中的异常处理机制,尝试执行一些可能会抛出异常的代码,并在发生异常时捕获并处理它。具体来说:
@try { … } @catch (…) { … } 是 Objective-C 中的异常处理语法。@try 块用于包含可能会抛出异常的代码,而 @catch 块则用于捕获异常并进行处理。
在 @try 块中,首先创建了一个 EOCSomeClass 类的对象 object,然后调用了 doSomethingThatMayThrow 方法。这个方法可能会抛出异常。
在 @try 块的最后,调用了 [object release] 方法来释放 object 对象。这表明代码的编写者使用了手动内存管理。
如果在 @try 块中的代码抛出了异常,那么异常处理流程会跳转到对应的 @catch 块中。在这个例子中,@catch 块中的代码会执行,它打印了一条错误日志。由于 @catch 块的参数是 …,表示捕获所有类型的异常,因此无论什么类型的异常都会被捕获并处理。
但如果 doSomethingThatMayThrow 抛出异常了呢?由于异常会令执行过程终止并跳至catch 块,因而其后的那行 release 代码不会运行。在这种情况下,如果代码抛出异常,那么对象就泄漏了。这么做不好。解决方法是使用 @finally 块,无论是否抛出异常,其中代码都保证会运行,且只运行一次。比方说,刚才那段代码可改写如下:

EOCSomeClass *object;

@try {
    object = [[EOCSomeClass alloc] init];
    [object doSomethingThatMayThrow];
} 
@catch (...) {
    NSLog(@"Whoops, there was an error. Oh well...");
} 
@finally {
    [object release];
}

在 ARC 环境下,问题会更严重。下面这段使用 ARC 的代码与修改前的那段代码等效:

@try {
    EOCSomeClass *object = [[EOCSomeClass alloc] init];
    [object doSomethingThatMayThrow];
}
@catch (...) {
    NSLog(@"Whoope, there was an error. Oh well...");
}

在 ARC 下,由于不能手动调用 release 方法来释放对象,因此无法像在手动管理内存时那样将释放操作放在 @finally 块中。而且,ARC 不会自动处理异常导致的内存释放,因为这需要添加大量的额外代码来跟踪待清理的对象,并在抛出异常时释放它们,这可能会影响程序的性能并增加应用程序的大小。

虽然在 Objective-C 代码中,抛出异常通常是在应用程序必须因异常状况而终止时才发生的,但默认情况下 ARC 并不会为异常处理添加额外的代码。这是因为在应用程序即将终止时,是否会发生内存泄漏已经无关紧要了。因此,默认情况下,ARC 不会为异常处理添加额外的代码。如果需要在 ARC 环境下处理异常并进行自动内存管理,可以通过开启 -fobjc-arc-exceptions 编译器标志来实现。
但最重要的是:在发现大量异常捕获操作时,应考虑重构代码。

捕获异常时,一定要注意将 try 块所创立的对象清理干净。
在默认情况下,ARC 不生成安全处理异常所需的清理代码。开启编译器标志后,可以生成这种代码,不过会导致应用程序变大,而且会降低运行效率。

第 33 条 : 以弱引用避免保留环

对象图里经常会出现一种情况,就是几个对象都以某种方式互相引用,从而形成“环”。这种情况通常会泄漏内存,因为最后没有别的东西会引用环中的对象。这样的话,环里的对象就无法为外界所访问了,但对象之间尚有引用,这些引用使得它们都能继续存活下去,而不会为系统所回收。最简单的保留环由两个对象构成,它们互相引用对方。
例如这里有两个类:

#import <Foundation/Foundation.h>

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject

@property (nonatomic, strong) EOCClassB *other;

@end

@interface EOCClassB : NSObject

@property (nonatomic, strong) EOCClassA *other;

@end

保留环会导致内存泄漏。如果只剩一个引用还指向保留环中的实例,而现在又把这个引用移除,那么整个保留环就泄漏了。
避免保留环的最佳方式就是弱引用。这种引用经常用来表示 “非拥有关系”。将属性声明为 unsafe_unretained 即可。修改刚才那段范例代码:

#import <Foundation/Foundation.h>

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject

@property (nonatomic, strong) EOCClassB *other;

@end

@interface EOCClassB : NSObject

@property (nonatomic, unsafe_unretained) EOCClassA *other;

@end

修改之后,EOCClassB 实例就不再通过 other 属性来拥有 EOCClassA 实例了。属性特质 (attribute) 中的 unsafe_unretained 一词表明,属性值可能不安全,而且不归此实例所拥有。如果系统已经把属性所指的那个对象回收了,那么在其上调用方法可能会使应用程序崩溃。由于本对象并不保留属性对象,因此其有可能为系统所回收。
还可以使用weak 属性特质,刚刚的代码还可以修改为:

@property (nonatomic,weak) EOCClassA *other;

unsafe_unretained 与 weak 属性,在其所指的对象回收以后表现出来的行为不同。当指向 EOCClassA 实例的引用移除后,unsafe_unretained 属性仍然指向那个已经回收的实例,而 weak 属性则指向 nil。
一般来说,如果不拥有某对象,那就不要保留它,当然 collection 例外。有时,对象中的引用会指向另外一个并不归自己拥有的对象,比如 Delegate 模式就是这样。

将某些引用设为 weak,可避免出现 “保留环”。
weak 引用可以自动清空,也可以不自动清空。自动清空(autonilling)是随着 ARC 而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。

第 34 条:以“自动释放池块 ”降低 内存峰值

由前面的内容知道:自动释放池用于存放那些需要稍后某个时刻释放的对象。
创建自动释放池所用语法如下:

@autorelease {
    /...
}

通常只有一个地方需要创建自动释放池,那就是在 main 函数里。比如说iOS程序的main函数一般这样写:

int main(int argc, char *argv[]) {
    @autoreleasepool {
        
        return UIApplicationMain(argc, argv, nil, @"EOCAppDelegate");
    }
}

从技术角度看,不是非得有个“自动释放池块”才行。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。这个池可以理解成最外围捕捉全部自动释放对象所用的池。
下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并于对应的右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到 release 消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。比方说:

@autoreleasepool {
    NSString *string = [NSString stringWithFormat:@"1= %i", 1];
    @autoreleasepool {
        NSNumber *number = [NSNumber numberWithInt:1];
    }
}

这段代码创建了两个自动释放池。第一个自动释放池从第 1 行开始,到第 10 行结束。在此范围内,字符串 string 被创建,并且在自动释放池的末尾被释放。
在第 5 行到第 8 行之间的范围内,又创建了一个新的自动释放池。在这个内层自动释放池中,数字 number 被创建,然后在内层自动释放池的末尾被释放。
注意,内层自动释放池的范围是嵌套在外层自动释放池的范围内的。因此,number 对象的释放操作会在外层自动释放池的范围结束时执行,而 string 对象的释放操作则会在整个自动释放池的范围结束时执行。

有如下代码:

for (int i = 0; i< 100000; i++) {
    [self doSomethingWithInt:i];
}

如果 “doSomethingWithInt:”方法要创建临时对象,那么这些对象很可能会放在自动释放池里。即便这些对象在调用完方法之后就不再使用了,它们也依然处于存活状态,因为目前还在自动释放池里,等待系统稍后将其释放并回收。然而,自动释放池要等线程执行下一次事件循环时才会清空。即执行 for 循环时,会持续有新的对象创建出来,并加入自动释放池中。所有这种对象都要等 for 循环执行完才会释放。这样一来,在执行 for 循环时,应用程序所占内存量就会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。
或者比如说要从数据库中读数据:

NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray new];

for (NSDictionary *record in databaseRecords) {
    EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
    [people addObject:person];
}

若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。增加一个自动释放池即可解决此问题。如果把循环内的代码包裹在“自动释放池块”中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。例如:

NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray new];

for (NSDictionary *record in databaseRecords) {
    @autoreleasepool {
        EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
        [people addObject:person];
    }
}

新增的自动释放池块可以减少内存峰值,因为系统会在块的末尾把某些对象回收掉。
是否应该用池来优化效率,完全取决于具体的应用程序。所以尽量不要建立额外的自动释放池。
有一种老式写法,是使用 NSAutoreleasePool 对象。但是这种写法并不会在每次执行 for 循环时都清空池,通常用来创建那种偶尔要清空的池,采用随着 ARC 所引入的新语法,可以创建出更为 “轻量级”的自动释放池。现在可以改用自动释放池块把 for 循环中的语句包起来,这样的话,每次执行循环时都会建立并清空自动释放池。

自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池里。
合理运用自动释放池,可降低应用程序的内存峰值。
@autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。

第35条:用“僵尸对象” 调试内存 管理问题

Cocoa 提供了 “僵尸对象”(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。给僵尸对象发送消息后,控制台会打印消息,而应用程序则会终止。
也可以在Xcode 里打开此选项,这样的话,Xcode 在运行应用程序时会自动设置环境变量。开启方法:编辑应用程序的 Scheme,在对话框左侧选择 “Run”,然后切换至 “Diagnostics” 分页,最后勾选 “Enable Zombie Objects” 选项。
僵尸对象的工作原理涉及Objective-C的运行时程序库、Foundation框架和CoreFoundation框架的实现代码。当系统即将回收对象时,如果启用了僵尸对象功能,会执行一个额外的步骤,即将对象转化为僵尸对象而不是立即回收。
僵尸类是从名为 NSZombie 的模板类里复制出来的。这些僵尸类没有多少事情可做,只是充当一个标记。
僵尸类的作用会在消息转发例程中体现出来。 NSZombie 类(以及所有从该类拷贝出来的类)并未实现任何方法。此类没有超类,因此和 NSObject 一样,也是个“根类”,该类只有一个实例变量,叫做 isa ,所有 Objective-C 的根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过 “完整的消息转发机制”。

系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量 NSZombieEnable 可开启此功能。
系统会修改对象的 isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够相应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

第 36 条:不要使用retainCount

每个对象的引用计数都有一个计数器,其值表明还有多少个其他对象想令此对象继续存活。
NSObject 协议中定义了下列方法,用于查询对象当前的保留计数:

(NSUInteger)retainCount;

然而 ARC 已经将此方法废弃了。如果在 ARC 中调用,编译器就会报错。但问题在于,保留计数的绝对值一般都与开发者所应留意的事情完全无关。即便只在调试时才能调用此方法,通常也还是无所助益的。
因为它返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的保留计数了。
开发者在期望系统于某处回收对象时,应该确保没有尚未抵消的保留操作,也就是不要令保留计数大于期望值。在这种情况下,如果发现某对象的内存泄漏了,那么应该检查还有谁仍然保留这个对象,并查明其为何没有释放此对象。

即便只为调试,此方法也不是很有用。由于对象可能处在自动释放池中,所以其保留计数未必如想象般精确。那到底何时才应该用 retainCount 呢?最佳答案是:绝对不要用,尤其考虑到苹果公司在引入 ARC 之后已正式将其废弃,就更不应该用了。

对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”(absolute retain count)都无法反映对象生命期的全貌。
引入 ARC 之后,retainCount 方法就正式废止了,在 ARC 下调用该方法会导致编译器报错。

第37条:理解“块” 这一概念

块可以实现闭包,它与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。块用 “^” 符号来表示,后面跟着一对花括号,括号里面是块的实现代码。
块的强大之处是:在声明它的范围里。所有变量都可以为其所捕获。这也就是说,那个范围里的全部变量,在块里依然可用。比如,下面这段代码所定义的块,就使用了块以外的变量:

int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b){
    return a + b + addItional;
};
int add = addBlock(2, 5);

默认情况下,为块所捕获的变量,是不可以在块里修改的。声明变量的时候可以加上 __block 修饰符,这样就可以在块内修改了。例:

NSArray *array = @[@0, @1, @2, @3, @4, @5];
__block NSInteger count = 0;
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BooL *stop) {
    if([number compare:@2] == NSOrderedAscending) {
    count++;
    }
}];

这段范例代码也演示了 “内联块”的用法。传给 “numerateObjectsUsingBlock:” 方法的块并未先赋给局部变量,而是直接内联在函数调用里了。这样可以把所有业务逻辑都放在一处。
如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。块本身可视为对象。块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。
如果将块定义在 OC 类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用 self 变量。块总能修改实例变量,所以在声明时无须加 _ _block 。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把 self 变量一并捕获了,因为实例变量是与 self 所指代的实例关联在一起的。就是说:在OC类的实例方法中使用块时可以轻松地访问和操作当前实例的变量和方法而不必过多关注__block的使用。这使得在块内部处理实例变量变得更加方便。
例:

@interface EOCClass
- (void)anInstanceMethod {
    void (^someBlock)() = ^{
      _anInstanceVariable = @"Something";
      NSlog(@"_anInstanceVariable = %@", _anInstanceVariable);
    };
} 
@end

如果某个 EOCClass 实例正在执行 anInstanceMethod 方法,那么 self 变量就指向此实例。由于块里没有明确使用 self 变量,所以很容易就会忘记 self 变量其实也为块所捕获了。直接访问实例变量和通过 self 来访问是等效的。
self 也是个对象,因而块在捕获它时也会将其保留。如果 self 所指代的那个对象同时保留了块,那么这种情况通常就会导致 “保留环”。

块的内部结构
每个 OC 对象都占据着某个内存区域。每个对象所占的内存区域也有大有小。块本身也是对象,在存放块对象的内存区域中,首个变量是指向 Class 对象的指针isa。其余内存里含有块对象正常运转所需要的各种信息。
在内存布局中,最重要的就是 invoke 变量,这是个函数指针,指向块的实现代码。descriptor 变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了 copy 与 dispose 这两个辅助函数所对应的函数指针。
块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在 descriptor 变量后面,捕获了多少个变量,就要占据多少内存空间。

全局块、栈块及堆块
定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。这里有段代码:

void (^block)();
if () {
    block = ^{
        NSLog(@"Block A");
    };
} else {
    block = ^{
        NSLog(@"Block B");
    };
}
block();

这个代码问题在于,块的生命周期与其定义的范围有关。在这里,两个块都分配在栈内存中,而栈内存的生命周期通常与包含它的作用域相关。一旦超出 if 或 else 的作用域,栈上的块可能会被释放,而且内存区域可能被覆写。
这样的代码在编译时通常能够通过,但在运行时可能会出现问题,因为 block() 调用时,块的定义范围可能已经结束,栈上的内存可能已经被其他变量或数据覆写。
为解决此问题,可以通过调用copy方法。这样的话,就可以把块从栈复制到堆了。拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。而“分配在栈上的块”则无须明确释放,因为栈内存本来就会自动回收。
应该改正为:

void (^block)();
if () {
    block = [^{
        NSLog(@"Block A");
    } copy];
} else {
    block = [^{
        NSLog(@"Block B");
    } copy];
}
block();

还有一类块叫做 “全局块”。这种块不会捕捉任何状态,运行时也无须有状态来参与。块所使用的整个内存区域在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要在每次用到的时候于栈中创建。
全局块的拷贝操作是个空操作,因为全局块决不可能为系统所回收。这种块实际上相当于单例。下面是个全局块:

void (^block)() = ^{
  NSLog(@"This is a block");
}

这完全是种优化技术

块是C、C++、Objective-C 中的词法闭包。
块可接受参数,也可返回值。
块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的 Objective-C 对象一样,具备引用计数了。

第 38 条:为常用的块类型创建 typedef

每个块都具备其“固有类型”,因而可将其赋给适当类型的变量。这个类型由块所接受的参数及其返回值组成。
与其他类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不要放在右侧。这种语法非常难记,也非常难读。鉴于此,我们应该为常用的块类型起个别名。
为了隐藏复杂的块类型,需要用到C 语言中名为 “类型定义”的特性。typedef 关键字用于给类型起个易读的别名。
举个例子,不使用typedef的话是这样声明一个块:

void (^sumBlock)(int, int) = ^(int a, int b) {
    int sum = a + b;
    NSLog(@"Sum: %d", sum);
};

使用typedef:

// 使用 typedef 创建块类型别名
typedef void (^SumBlock)(int, int);
// 使用块类型别名声明块变量
SumBlock sumBlock = ^(int a, int b) {
    int sum = a + b;
    NSLog(@"Sum: %d", sum);
};

这次代码读起来就顺畅多了:与定义其他变量时一样,变量类型在左边,变量名在右边。可见使用 typedef 可以将复杂的块类型声明简化为易读的别名,使代码更加清晰、易懂。通过项特性,可以把使用块的 API 做得更为易用些。

定义方法参数所用的块类型语法,又和定义变量时不同。若能把方法签名中的参数类型写成一个词,那读起来就顺口多了。于是,可以给参数类型起个别名,然后使用词名称来定义:

//创建一个名为EOCCompletionHandler的块类型别名

typedef void (^EOCCompletionHandler) (NSData *data, NSError *error);

//在方法签名中,使用了先前创建的块类型别名EOCCompletionHandler作为参数类型
//startWithCompletionHandler方法接受一个块作为参数,而这个块的类型就是

EOCCompletionHandler
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;

现在看上去就简单多了,而且易于理解。
使用类型定义还有个好处,就是重构块的类型签名时会很方便。
比如给块再加一个参数,那么只需要修改类型定义语句即可:

typedef void (^EOCCompletionHandler) (NSData *data, NSTimeInterval duration, NSError *error);

修改之后,凡是使用了这个类型定义的地方都会无法编译而且报的是同一种错误,于是开发者可据此逐个修复。

最好在使用块类型的类中定义这些 typedef,而且还应该把这个类的名字加在由 typedef 所定义的新类型名前面,这样可以阐明块的用途。还可以用 typedef 给同一个块签名类型创建数个别名。
比如Accounts 框架:

typedef void (^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);

如果有好几个类都要执行相似但各有区别的异步任务,而这几个类又不能放入同一个继承体系,那么,每个类就应该有自己的 completion handler 类型。这几个 completion handler 的签名也许完全相同,但最好还是在每个类里各自定义一个别名,而不要同一个名称。反之,若这些类能纳入同一个继承中,则应该将类型定义语句放在超类中,以供各子类使用。

以 typedef 重新定义块类型,可令块变量用起来更加简单。
定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需要修改相应 typedef 中的块签名即可,无须改动其他 typedef。

第39 条:用 handler 块降低代码分散程度

为用户界面编码时,一种常用的范式就是 “异步执行任务”。这种范式的好处在于:处理用户界面的显示及触摸操作所用的线程,不会因为要执行 I/O 或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。假设把执行异步任务的方法做成同步的,那么在执行任务时,用户界面就变得无法响应用户输入了。某些情况下,如果应用程序在一定时间内无响应,那么就会自动终止。iOS 系统上的应用程序就是如此,“系统监控器”在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。

I/O 操作(输入/输出操作):

输入操作(Input): 从外部设备或文件中读取数据到计算机系统中。例如,从键盘读取用户输入、从磁盘读取文件等。 输出操作(Output):
将计算机系统中的数据发送到外部设备或存储到文件中。例如,将数据写入显示器显示、将结果写入磁盘文件等。

I/O 操作可能涉及到慢速的设备(如硬盘、网络),因此在进行这些操作时,系统可能需要等待一段时间。

网络通信: 指在不同计算机或设备之间传递数据的过程。
通过网络连接,计算机系统可以通过一系列协议进行数据的发送和接收,包括传输层协议(如TCP或UDP)和应用层协议(如HTTP、FTP等)。
网络通信包括从客户端到服务器的请求和响应,文件传输,远程过程调用(RPC)等。 在移动应用或网络应用中,常见的 I/O 操作和网络通信包括:
读写本地文件: 通过文件系统进行读写,例如保存应用程序数据、读取配置文件等。 数据库操作:
与本地或远程数据库进行数据交互,例如存储和检索用户信息。 HTTP 请求和响应:
通过网络协议进行数据传输,例如从服务器获取数据、上传文件等。 Socket 编程:
直接在网络上建立连接,进行实时通信,例如聊天应用、在线游戏等。

异步方法在执行完任务之后,需要以某种手段通知相关代码。实现此功能有很多方法。常用的技巧是设计一个委托协议,令关注此事件的对象遵从该协议。对象成为 delegate 之后,就可以在相关事件发生时(例如某个异步任务执行完毕时)得到通知了。委托模式有个缺点:如果类要分别使用多个获取器下载不同数据,那么就得在 delegate 回调方法里根据传入的获取器参数来切换。这么写代码,不仅会令 delegate 回调方法变得很长,而且还要把网络数据获取器对象保存为实例变量,以便在判断语句中使用。
然而如果改用块来写的话,代码会更清晰。块可以令这种 API 变得更紧致,同时令开发者调用起来更加方便。改用块来写的好处是:无须保存获取器,也无须在回调方法里切换。每个 completion handler 的业务逻辑,都是和相关的获取器对象一起来定义的。
比如像这样:

#import <Foundation/Foundation.h>
// 定义块类型
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
// 使用块作为参数的方法
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end
@implementation EOCNetworkFetcher
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://www.example.com"]];
        dispatch_async(dispatch_get_main_queue(), ^{
            if (handler) {
                handler(data);
            }
        });
    });
}
@end

用块写出来的代码显然更为整洁。而且,由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。

这种写法还有其他用途,比如,现在很多基于块的 API 都使用块来处理错误。这又分为两种办法。可以分别用两个处理程序来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码,与处理正常情况所用的代码,都封装到同一个 completion handler 块里。如果想采用两个独立的处理程序,那么可以这样设计 API:

#import <Foundation/Foundation.h>
// 声明块类型
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void(^EOCNetworkFetcherErrorHandler)(NSError *error);

@interface EOCNetworkFetcher : NSObject
// 初始化方法声明
- (instancetype)initWithURL:(NSURL *)url;
// 方法声明,接受两个块作为参数
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion failureHandler:(EOCNetworkFetcherErrorHandler)failure;
@end

调用方式如下:

EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:^(NSData *data) {
  // Handle success
} failureHandler:^(NSError *error) {
  // Handle failure
}];

另一种风格则像下面这样,把处理成功情况和失败情况所用的代码全放在一个块里:

#import <Foundation/Foundation.h>
// 声明块类型,接受两个参数:NSData 和 NSError
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data, NSError *error);

@interface EOCNetworkFetcher : NSObject
// 初始化方法声明
- (instancetype)initWithURL:(NSURL *)url;
// 方法声明,接受一个块作为参数
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end

调用方式如下:

// 创建网络获取器实例
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];

[fetcher startWithCompletionHandler:^(NSData *data, NSError *error) {
    if (error) {
        // 处理失败情况
        NSLog(@"Error occurred: %@", error);
    } else {
        // 处理成功情况
        NSLog(@"Data received: %@", data);
    }
}];

这种写法的缺点是:由于全部逻辑都写在一起,所以会令块变得比较长且比较复杂。然而也有好处,那就是更为灵活。比方说,在传入错误信息时,可以把数据也传进来。而且还有一个优点是调用 API 的代码可能会在处理成功响应的过程中发现错误。

总体来说,笔者建议使用同一个块来处理成功与失败情况,苹果公司似乎也是这样设计其 API 的。

有时需要在相关时间点执行回调操作,这种情况也可以使用 Handler 块。
基于 handler 来设计 API 还有个原因,就是某些代码必须运行在特定的线程上。例如NSNotificationCenter 就属于这种 API,它提供了一个方法,调用者可以经由此方法来注册想要接收的通知,等到相关事件发生时,通知中心就会执行注册好的那个块。调用者可以指定某个块应该安排在哪个执行队列里。

- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotification *))block;

此处传入的 NSOperationQueue 参数就表示出触发通知时用来执行块代码的那个队列。这是个“操作队列”,而非“底层 GCD 队列”,不过两者语义相同。

在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用 handler 块来实现,则可直接将块与相关对象放在一起。
设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

第40 条:用块引用其所属对象时 不要出现保留环

使用块时,若不仔细思量,则很容易导致“保留环”。比如说下面这个例子:

// EOCNetworkFetcher.h

#import <Foundation/Foundation.h>

typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject

@property (nonatomic, strong, readonly) NSURL *url;

- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;- 
@end

// EOCNetworkFetcher.m

#import "EOCNetworkFetcher.h"

@interface EOCNetworkFetcher ()

@property (nonatomic, strong, readwrite) NSURL *url;
@property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property (nonatomic, strong) NSData *downloadedData;

@end

@implementation EOCNetworkFetcher

- (instancetype)initWithURL:(NSURL *)url {
    if ((self = [super init])) {
        _url = url;
    }
    return self;
}

- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {
    self.completionHandler = completion;
    // Start the request
    // Request sets downloadedData property
    // When request is finished, p_requestCompleted is called
}

- (void)p_requestCompleted {
    if (_completionHandler) {
        _completionHandler(_downloadedData);
    }
}

@end

某个类可能会创建这个网络请求的实力,并用其从url中下载数据:

// EOCClass.m

@implementation EOCClass {
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetchedData;
}

- (void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
    _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"Request URL %@ finished", _networkFetcher.url);
        _fetchedData = data;
    }];
}

@end

在上面这段代码中:当 EOCClass 类的实例调用 downloadData 方法时,它会创建一个 EOCNetworkFetcher 的实例对象 _networkFetcher。
在 _networkFetcher 的 startWithCompletionHandler: 方法中,传入了一个块作为参数,该块会捕获 self(即 EOCClass 实例),因为它需要设置 EOCClass 实例中的 _fetchedData 实例变量。
这样,块持有了 EOCClass 实例,EOCClass 实例持有了 _networkFetcher 实例,而 _networkFetcher 实例又持有了传入的块,形成了保留环。

要打破保留环也很容易:要么令 _networkFetcher 实例变量不再引用获取器,要么令获取器的 completionHandler 属性不在持有 handler 块。在这个例子中,应该等 completion handler 块执行完毕后,再去打破保留环,以便使获取器对象在 handler 块执行期间保持存活状态。比方说,completion handler 块的代码可以这么修改:

[_networkFetcher startWithCompletionHandler:^(NSData *data) {
    NSLog(@"Request for URL %@ finished", _networkFetcher.url);
    _fetchedData = data;
    _networkFetcher = nil;    
];

一般来说,只要适时清理掉环中的某个引用,即可解决此问题,然而,未必总有这种机会。若是 completion handler 一直不运行,那么保留环就无法打破,于是内存就会泄漏。
如果 completion handler 块所引用的对象最终又引用了这个块本身,那么就会出现另一种形式的保留环。
举个例子:

- (void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
    EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"Request URL %@ finished", networkFetcher.url);
        _fetchedData = data;
    }];
}

在上面这个例子中,downloadData 方法创建了一个 EOCNetworkFetcher 的实例 networkFetcher,并设置了其 startWithCompletionHandler 方法的 completion handler 块。
在 completion handler 块中,使用了 networkFetcher 实例的 url 属性。由于块内部引用了 networkFetcher 实例,因此块会保留这个实例。
反过来,networkFetcher 实例也持有了 completion handler 块,因为 networkFetcher 的属性 completionHandler 是一个 copy 类型的块属性。
因此,形成了一个保留环:networkFetcher 持有 completion handler 块,而 completion handler 块又持有 networkFetcher 实例。这会导致 networkFetcher 实例和其相关的对象无法被释放,从而造成内存泄漏。
这个问题可以这样解决:只需要将 p_requestCompleted 方法按如下方式修改即可:

- (void)p_requestCompleted {
    if (_completionHandler) {
      _completionHandler(_downloadedData);
   }
   self.completionHandler = nil;
}

这样一来,只要下载请求执行完毕,保留环就解除了,而获取器对象也将会在必要时为系统所回收。

注意,要在 start 方法中把 completion handler 作为参数传进去。假如把 completion handler 暴露为获取器对象的公共属性,那么就不便在执行完下载请求之后直接将其清理掉了,因为既然已经把 handler 作为属性公布了,那就意味着调用者可以自由使用它,若是此时又在内部将其清理掉的话,则会破坏“封装语义”。在这种情况下要想打破保留环,只有一个办法可用,那就是强迫调用者在 handler 代码里自己把 compleionHandler 属性清理干净。

如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
一定要找个适当的时机解除保留环,而不能把责任推给API 的调用者。

第41条:多用派发队列,少用同步锁

派发队列
在 Objective-C 中,派发队列是一种用来管理任务执行的队列系统。它基于GCD框架,用于异步执行任务,可以在串行或并发的队列上执行任务。派发队列可以是串行队列或并发队列。

串行队列:串行队列中的任务一个接一个按顺序执行,每个任务执行完毕后才会执行下一个任务。
并发队列:并发队列中的任务可以同时执行,不需要等待前一个任务完成。 同步锁

同步锁是一种用于控制并发访问共享资源的机制。在多线程环境下,当多个线程同时访问某个共享资源时,可能会出现数据竞争的情况,导致程序出现不确定的行为或错误。同步锁可以确保在某个线程修改共享资源时,其他线程不会同时进行修改,从而保证数据的一致性和正确性。
常见的同步锁机制包括:

@synchronized 块:使用 @synchronized 关键字创建临界区,确保同一时间只有一个线程能够访问临界区中的代码。
NSLock 类:NSLock 是 Foundation 框架中提供的一个锁对象,可以使用 lock 和 unlock
方法来实现对临界区的加锁和解锁操作。 dispatch_semaphore 信号量:通过信号量来实现对临界区的控制,可以使用
dispatch_semaphore_wait 和 dispatch_semaphore_signal 函数来实现加锁和解锁操作。
NSRecursiveLock 类:NSRecursiveLock 是 NSLock
的子类,允许同一个线程多次对锁进行加锁操作,可以解决递归调用时可能出现的死锁问题。

在 GCD 出现之前,如果有多个线程要执行同一份代码,通常要使用锁来实现某种同步机制,有两种办法,第一种是采用内置的 “同步块”:

- (void)synchronizedMethod {
    @synchronized(self) {
        //。。。
    }
}

这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁就释放了。但是若是在 self 对象上频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码,这样做其实并没有必要。
另一个办法是直接使用 NSLock 对象:

_lock = [[NSLock alloc] init];

- (void)synchronizedMethod {
    [_lock lock];
    //...
    [_lock unlock];
}

它可以创建一个 NSLock 对象 _lock,然后在 synchronizedMethod 方法中,使用 lock 方法来获取锁,然后执行安全的代码。执行完安全代码后,通过调用 unlock 方法释放锁。这种方式提供了更多的控制能力,可以更灵活地管理锁的获取和释放
也可以使用 NSRecursiveLock 这种 “递归锁”(重入锁),线程能够多次持有该锁,而不会出现死锁现象。

为什么要多用派发队列,少用同步锁
使用同步锁的这几种方法有其缺陷。比方说,在极端情况下,同步块会导致死锁,另外,其效率也不见得很高,而如果直接使用锁对象的话,一旦遇到死锁,就会非常麻烦。
因此我们可以使用 GCD ,它能以更简单、更高效的形式为代码加锁。

在之前的学习中,我们学习过atomic 特质,它是用于修饰属性的原子性,并且该特性是与锁这个机制紧密相连的,而GCD与锁的机制不同,它是通过队列和调度机制来管理任务的执行,而不需要显式地使用锁来保护共享数据,因此使用GCD不需要依赖属性的原子性。
而再回顾一下atomic特质,它可以指定属性的存取方法。而开发者如果想自己来编写访问方法的话,那么通常会这样写:

- (NSString *)someString {
    @synchronized(self) {
        return _someString;
    }
}

- (void)setSomeString:(NSString *)someString {
    @synchronized(self) {
        _someString = someString;
    }
}

刚才说过,滥用 @synchronized(self) 会很危险,因为所有同步块都会彼此抢夺同一个锁。这么做虽然能提供某种程度的 “线程安全”(thread safety),但却无法保证访问该对象时绝对是线程安全的。
所以要用一种简单而高效的办法代替同步块或锁对象,那就是使用 “串行同步队列”。将读取操作及写入操作都安排在同一个队列里,即可保证数据同步:

_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);

-(NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;          
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_sync(_suncQueue, ^{
        _someString = someString;       
    });
}

在上面这段代码中,创建了一个串行同步队列 _syncQueue,用于处理对 someString 属性的读取和写入操作。
在someString 方法中,通过调用 dispatch_sync 将读取操作安排在 _syncQueue 中执行。在串行队列中使用 dispatch_sync 可以确保读取操作按顺序执行,并且在读取操作完成之前阻塞当前线程。读取操作执行完毕后,将结果赋值给 localSomeString 变量,然后返回。
setSomeString: 方法中,同样使用 dispatch_sync 将写入操作安排在 _syncQueue 中执行。写入操作也会按照顺序执行。把设置操作与获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作就都同步了。

设置代码也可以这样写:

- (void)setSomeString:(NSString *)someString {
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    });
}

上面这段代码用异步派发替代了同步派发,意味着设置方法 setSomeString: 中的实例变量 _someString 的操作会在一个后台线程上执行,而不会阻塞当前线程。这可以提高设置方法的执行速度,并使得调用者不必等待设置操作完成。
但这么改有个坏处:这种写法可能比原来慢,因为执行异步派发时,需要拷贝块。若拷贝块所用的时间明显超过执行块所花的时间,则这种写法将比原来更慢。

多个获取方法可以并发执行,而获取方法与设置方法之间不能并发执行
利用这个特点,还能写出更快一些的代码来,这次不用串行队列,而改用并发队列:
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

- (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    });
}

在上面这段代码中:_syncQueue 是一个并发队列,通过 dispatch_get_global_queue 函数创建,它允许多个任务同时在不同的线程上执行。
someString 方法用 dispatch_sync 函数将读取操作添加到 _syncQueue 中,并且等待这个操作完成后再返回结果。这确保了在多个线程同时调用 someString 方法时,能够安全地读取 _someString 的值。
setSomeString: 方法使用 dispatch_async 函数将写入操作添加到 _syncQueue 中,这样就可以确保多个设置方法之间不会并发执行,保证了数据的一致性和安全性。

像现在这样写代码,还无法正确实现同步。所有读取操作与写入操作都会在同一个队列上执行,不过由于是并发队列,所以读取与写入操作可以随时执行。而我们恰恰不想让这些操作随意执行。此问题用一个简单的 GCD 功能即可解决,它就是栅栏。

在并发队列中,栅栏块的作用是确保在其前面的任务执行完毕后,才会执行栅栏块,而在其后的任务则会等待栅栏块执行完毕后才能继续执行。
下列函数可以向队列中派发块,将其作为栅栏使用:

dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);

上面这段代码向并发队列中添加栅栏块,保证了栅栏块之前的任务并发执行,而栅栏块本身及其后的任务则是顺序执行的。这样,可以确保写入操作在读取操作之后进行,从而避免了并发读取与写入操作导致的数据同步问题。
使用栅栏具体的实现代码:

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

- (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    //通过 dispatch_barrier_async 函数将一个栅栏块提交到 _syncQueue 中执行
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}

在这个并发队列中,读取操作是用普通的块来实现的,而写入操作则是用栅栏块来实现的。读取操作可以并行,但写入操作必须单独执行,因为它是栅栏块。
这种做法肯定比使用串行队列要快。注意,设置函数也可以改用同步的栅栏块来实现。

派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用 @synchronized 块或 NSLock 对象更简单。
将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
使用同步队列及栅栏块,可以令同步行为更加高效。

第42条:多用GCD,少用 performSelector 系列方法

什么是performSelector
performSelector 是 Objective-C 中的一个方法,用于在对象上调用指定的方法,并且可以延迟执行或在指定的线程上执行。

- (nullable id)performSelector:(SEL)aSelector;

它会在当前线程中调用指定的方法 aSelector,如果方法有返回值,则返回该返回值;如果方法没有返回值,则返回 nil。它相当于直接调用选择子:[object selectorName];
还有一个带有 withObject: 参数的版本,可以传递一个参数给指定的方法:

- (nullable id)performSelector:(SEL)aSelector withObject:(nullable id)anObject;

这种编程方式极为灵活,经常可用来简化复杂的代码。

为何要多用GCD
但是使用performSelector的特性的代价是,如果在 ARC 下编译代码,那么编译器会发出如下警示信息:

warning: performSelector may cause a leak because its selector
is unknown [-Warc-performSelector-leaks]

原因在于,编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用 ARC 的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC 采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。
有如下代码:

SEL selector;

if (/*some condition*/) {
    selector = @selector(newObject);
} else if (/*some other condition*/) {
    selector = @selector(copy);
} else {
    selector = @selector(someProperty);
}

id ret = [object performSelector:selector];

if (selector == @selector(newObject) || selector == @selector(copy)) {
    [ret release]; // 手动释放返回的对象
}

此代码中如果调用的是两个选择子之一,那么 ret 对象应由这段代码来释放,而如果是第三个选择子,则无须释放。不仅在 ARC 环境下应该如此,而且在非 ARC 环境下也应该这么做,这样才算严格遵循了方法的命名规范。如果不使用 ARC,那么在前两种情况下需要手动释放 ret 对象,而在后一种情况下则不需要释放。这个问题很容易忽视,而且就算用静态分析器,也很难侦测到内存泄漏。performSelector 系列的方法之所以要谨慎使用,这就是其中一个原因。
少使用performSelector系列方法的另一个原因在于:该系列方法返回值只能是 void 或对象类型。尽管所要执行的选择子也可以返回 void,但是 performSelector 方法的返回值类型毕竟是 id。如果想返回整数或浮点数等类型的值,那么就需要执行一些复杂的转换操作了,而这种转换很容易出错。
performSelector 还有如下几个版本,可以在发消息时顺便传递参数:

- (id)performSelector:(SEL)selector withObject:(id)object;
- (id)performSelector:(SEL)selector withObject:(id)objectA withObject:(id)objectB;

比方说,可以用下面这个版本来设置对象中名为 value 的属性值:

id object = /*an object with a property called value */;
id newValue = /*new value for the property */;
[object performSelector:@selector(setValue:) withObject:newValue];

由于参数类型是 id,所以传入的参数必须是对象才行。如果选择子所接受的参数是整数或浮点数,那就不要采用这些方法了。

performSelector 系列方法还有个功能,就是可以延后执行选择子,或将其放在另一个线程上执行。下面列出了此方法中一些更为常用的版本:

- (void)performSelector:(SEL)selector withObject:(id)argument afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)selector onThread:(NSThread *)thread withObject:(id)argument waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)selector withObject:(id)argument waitUntilDone:(BOOL)wait;

但是这些方法都无法处理带有两个参数的选择子。能够指定执行线程的那些方法也不是特别通用。如果要用这些方法,就得把许多参数都打包到字典中,然后在受调用的方法里将其提取出来,这样会增加开销。而且还可能出 bug。
所以就需要改用其他替代方案,使它不受这些限制。
最主要的替代方案就是使用块,可以通过在 GCD 中使用 block 来实现。延后执行可以用 dispatch_after 来实现,在另一个线程上执行任务则可通过 dispatch_sync 及 dispatch_async 来实现。
比如要要延后执行某项任务,我们应该:

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
    [self doSomething];
});

要把任务放在主线程上执行应该:

dispatch_async(dispatch_get_main_queue(), ^{
    [self doSomething];
});

performSelector 系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC 编译器也就无法插入适当的内存管理方法。
performSelector 系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。
如果想把任务放在另一个线程上执行,那么最好不要用 performSelector 系列方法,而是应该把任务封装到块里,然后调用GCD 的相关方法来实现。

第43条:掌握GCD及操作队列的使用时机

在执行后台任务时,GCD 并不一定是最佳方式。还有一种技术叫做 NSOperationQueue,它虽然与 GCD 不同,但是却与之相关,开发者可以把操作以 NSOperation 子类的形式放在队列中,而这些操作也能够并发执行。

GCD是纯C的API,而NSOperationQueue是Objective-C的对象。这意味着使用GCD时,任务通过块(block)来表示,而块是一种轻量级的数据结构;而使用NSOperationQueue时,任务通过NSOperation的子类来表示,这是一种更为重量级的Objective-C对象。
虽然GCD提供了一种更轻量级的方式来处理任务,但并不总是最佳选择。有时候,使用NSOperationQueue所带来的开销微乎其微,而使用完整的对象所带来的好处可能会超过其缺点。NSOperationQueue提供了更多的灵活性和控制,例如可以对操作进行取消、暂停和恢复等操作。

NSOperationQueue相比于纯GCD的优势:

取消操作: 使用NSOperationQueue可以轻松取消操作。可以在NSOperation对象上调用cancel方法来设置取消标志,这使得取消操作变得更加简单。相比之下,如果使用纯GCD,任务一旦被提交到队列中就无法取消。
指定操作间的依赖关系: NSOperation允许指定操作之间的依赖关系,这使得某些操作必须在其他操作执行完毕后才能执行。这种依赖关系对于需要按特定顺序执行任务的情况非常有用。
监控操作属性: NSOperation对象的属性可以通过键值观察(KVO)机制进行监控,这使得可以轻松地检测操作的状态变化,例如判断操作是否被取消或完成。
指定操作的优先级: NSOperation允许指定操作的优先级,这使得可以控制操作执行的顺序。与GCD不同,NSOperation提供了更为灵活的优先级管理机制。
重用NSOperation对象: NSOperation对象是Objective-C的对象,可以存储任何信息,并且可以多次使用。这使得NSOperation相对于简单的GCD块更为强大,因为它们可以包含更多的逻辑和状态信息。
有一个 API 选用了操作队列而非派发队列,这就是 NSNotificationCenter ,开发者可通过其中的方法来注册监听器,以便在发生相关事件时得到通知,而这个方法接受的参数是块,不是选择子:

- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotification *))block;

在解决多线程与任务管理问题时,派发队列并非唯一方案。
操作队列提供了一套高层的 Objective-C API,能实现纯 GCD 所具备的绝大部份功能,而且还能完成一些更为复杂的操作,那些操作若改用 GCD 来实现,则需另外编写代码。

第 4 4 条:通过Dispatch Group机制,根据系统资源状况来执行任务

dispatch group(意为“派发分组”或“调度组”) 是 GCD 的一项特性,能够把任务分组。
其中最重要的用法,就是把将要并发执行的多个任务合为一组,于是调用者就可以知道这些任务何时才能全部执行完毕。
把压缩一系列文件的任务表示成 dispatch group,下面这个函数可以创建 dispatch group:

dispatch_group_t dispatch_group_create();

想把任务编组,有两种办法。第一种是用下面这个函数:

void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

它是普通 dispatch_async 函数的变体,比原来多一个参数,用于表示待执行的块所归属的组。还有种办法能够指定任务所属的 dispatch group,那就是使用下面这一对函数:

void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);

前者能够使分组里正要执行的任务数递增,而后者则使之递减。调用了 dispatch_group_enter 以后,必须有与之对应的 dispatch_group_leave 才行。这与引用计数相似。
下面这个函数可用于等待 dispatch group 执行完毕:

long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

此函数接受两个参数,一个是要等待的 group,另一个是代表等待时间的 timeout 值。timeout 参数表示函数在等待 dispatch group 执行完毕时,应该阻塞多久。
除了可以用上面那个函数等待 dispatch group 执行完毕之外,也可以换个办法,使用下列函数:

void dispatch_group_notiy(dispath_group_t group, dispatch_queue_t queue, dispath_block_t block);

不同的是:开发者可以向此函数传入块,等 dispatch group 执行完毕之后,块会在特定的线程上执行。
比方说,在 Mac OS X 与 iOS 系统中,都不应阻塞主线程,因为所有 UI 绘制及事件处理都要在主线程上执行。如果想令数组中的每个对象都执行某项任务,并且想等待所有任务执行完毕,那么就可以使用这个 GCD 特性来实现。

若当前线程不应阻塞,则可用 notify 函数来取代 wait:

dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup, notifyQueue, ^{
    //...
});

也可以把某些任务放在优先级高的线程上执行,同时仍然把所有任务都归入同一个 dispatch group。并在执行完毕时获得通知。
开发者未必总需要使用 dispatch group。有时候采用单个队列搭配标准的异步派发,也可以实现相同效果。
为了执行队列中的块,GCD 会在适当的时机自动创建新线程或复用旧线程。如果使用并发队列,那么其中有可能会有多个线程,这也意味着多个块可以并发执行。在并发队列中,执行任务所用的并发线程数量,取决于各种因素,而GCD 只要是根据系统资源状况来判定这些因素的。假如 CPU 有多个核心,并且队列中有大量任务等待执行,那么GCD 就可能会给该队列配备多个线程。通过 dispatch group 所提供的这种简便方式,既可以并发执行一系列给定的任务,又能在全部任务结束时得到通知。

一系列任务可归入一个 dispatch group 之中。开发者可以在这组任务执行完毕时获得通知。
通过 dispatch group ,可以在并发式派发队列里同时执行多项任务。此时 GCD 会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码。

第 45 条 :使 用 dispatch_once 来执行只需运行一次的线程安全代码

单例模式常见的实现方式为:在类中编写名为 sharedInstance 的方法,该方法只会返回全类共用的单例实例,而不会在每次调用时都创建新的实例。比如说:

@implementation EOCClass

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

@end

不过,GCD 引入了一项特性,能使单例实现起来更为容易。所用的函数是:

void dispatch_once(dispatch_once_t *token, dispatch_block_t block);

此函数接受类型为 dispatch_once_t 的特殊参数,笔者称其为 “标记”(token),此外还接受块参数。对于给定的标记来说,该函数保证相关的块必定会执行,且仅执行一次。此操作完全是线程安全的。
刚才实现单例模式所用的 sharedInstance 方法,可以用此函数来改写:

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

使用 dispatch_once 可以简化代码并且彻底保证线程安全,开发者根本无须担心加锁或同步。所有问题都由 GCD 在底层处理。由于每次调用时都必须使用完全相同的标记,所以标记要声明成 static。此外,dispatch_once 更高效。

经常需要编写 “只需执行一次的线程安全代码”(thread-safe single-code execution)。通过 GCD 所提供的 dispatch_once 函数,很容易就能实现此功能。
标记应该声明在 statci 或 global 作用域中,这样的话,在把只需执行一次的块传给dispatch_once 函数时,传进去的标记也是相同的。

第 46 条 :不要使用dispatch_get_current_queue

使用 GCD 时,经常需要判断当前代码正在哪个队列上执行,向多个队列派发任务时,更是如此。

dispatch_get_current_queue本来是用于解决由不可重入的代码所引发的死锁,但是因为它已经被废弃,所以可以选择通过 GCD 所提供的功能来设定“队列特有数据”(queue-specific data),此功能可以把任意数据以键值对的形式关联到队列里。最重要之处在于,假如根据指定的键获取不到关联数据,那么系统就会沿着层级体系向上查找,直至找到数据或到达根队列为止。比如说这个例子:

dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);

dispatch_set_target_queue(queueB, queueA);

static int kQueueSpecific;

CFStringRef queueSpecificValue = CFSTR("queueA");

dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);

dispatch_sync(queueB, ^{
    dispatch_block_t block = ^{ NSLog(@"No deadlock!"); };
    CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
    if (retrievedValue) {
        block();
    } else {
        dispatch_sync(queueA, block);
    }
});

在上面这段代码中:有两个串行队列 queueA 和 queueB。将 queueB 的目标队列设置为 queueA,表示 queueB 依赖于 queueA。定义一个静态整型变量 kQueueSpecific 作为键值,用于关联队列特有数据。使用 dispatch_queue_set_specific 函数将特定值(在这里是 queueA 的标识符)与 queueA 关联起来。在 queueB 上执行同步任务,内部判断是否可以访问 queueA 的特定值。如果能够访问到 queueA 的特定值,则直接执行任务,否则在 queueA 上同步执行任务。
这样,即使在 queueB 上同步执行任务,也不会产生死锁,因为在获取 queueA 的特定值时,会自动向上查找到其父级队列 queueA 的特定值,从而避免了循环等待。

dispatch_get_current_queue 函数对行为常常与开发者所预期的不同。此函数在iOS开发中已经废弃、只应做调试之用。
由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用函数解决的问题,通常也能改用“队列特定数据”来解决。

第47条:熟悉系统框架

编写 Objective-C 应用程序时几乎都会用到系统框架,如果直接使用这些框架中的类,那么应用程序就可以得益于新版系统库所带来的改进,而开发者也就无须手动更新其代码了。
将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。

各种常用框架:

总结:

Cocoa 和 Cocoa Touch 框架:Cocoa 框架用于 macOS 应用程序开发,而 Cocoa Touch 用于 iOS
应用程序开发。它们集成了一系列常用的框架和工具,用于创建图形界面应用程序。 Foundation 框架:Foundation 框架是
Cocoa 和 Cocoa Touch 中的一个核心框架,包含了诸如 NSObject、NSArray、NSDictionary
等基础类。它提供了许多基础核心功能,例如集合类、字符串处理以及字符串解析等。 CoreFoundation
框架:CoreFoundation 框架不是 Objective-C 框架,而是用 C 语言编写的。然而,它与 Foundation
框架密切相关,提供了一套与 Foundation 中相对应的 C 语言 API。CoreFoundation 中的数据结构可以与
Foundation 中的 Objective-C 对象进行平滑转换,这种技术称为“无缝桥接”。 CFNetwork
框架:此框架提供了C语言级别的网络通信能力,它将“BSD套接字”(BSD socket)抽象成易于使用的网络接口。而 Foundation
则将该框架里的部分内容封装为 Objective-C 语言的接口,以便进行网络通信,例如可以用 NSURLConnection 从 URL
中下载数据。 CoreAudio 框架:该框架所提供的 C语言API
可用来操作设备上的音频硬件。这个框架属于比较难用的那种,因为音频处理本身就很复杂。所幸由这套API 可以抽象出另外一套
Objective-C 式API,用后者来处理音频问题会更简单些。 AVFoundation框架:此框架所提供的Objective-C
对像可用来回放并录制音频及视频,比如能够在 UI 视图类里播放视频。 CoreData 框架:此框架所提供的 Objective-C
接口可将对象放入数据库,以便持久保存。CoreData 会处理数据的获取及存储事宜,而且可以跨越 Mac OS X 及 iOS 平台。
CoreText 框架:此框架提供的C 语言接口可以高效执行文字排版及渲染操作。 AppKit 和 UIKit 框架:AppKit 用于
macOS 应用程序开发,而 UIKit 用于 iOS 应用程序开发。它们是构建在 Foundation 和 CoreFoundation
之上的核心 UI 框架,提供了 UI 元素和粘合机制,用于组装应用程序的所有内容。 CoreAnimation
框架:CoreAnimation 是一个用 Objective-C 编写的框架,它提供了渲染图形和播放动画所需的工具。虽然
CoreAnimation 本身不是一个框架,但它是 QuartzCore 框架的一部分,被广泛用于 UI 框架中。
CoreGraphics 框架:CoreGraphics 是用 C 语言编写的框架,提供了绘制 2D 图形所需的数据结构和函数。UIKit
框架中的 UIView 类在确定视图控件之间的相对位置时会用到 CoreGraphics 中定义的数据结构。 其他框架:除了上述核心 UI
框架之外,还有许多其他框架构建在 UI 框架之上,例如 MapKit 框架用于为 iOS 应用程序提供地图功能,Social 框架用于为
macOS 和 iOS 应用程序提供社交网络功能。这些框架通常与操作系统平台对应的核心 UI 框架结合使用,以丰富应用程序的功能。

可以看出Objective-C的一项重要特点:经常需要使用底层的 C 语言级 API。用 C 语言来实现 API 的好处是,可以绕过 Objective-C 的运行期系统,从而提升执行速度。

许多系统框架都可以直接使用。其中最重要的是 Foundation 与 CoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能。
很多常见任务都能用框架来做,例如音频与视频处理、网络通信、数据管理等。
请记住:用纯C 写成的框架与用 Objective-C 写成的一样重要,若想成为优秀的 Objective-C 开发者,应该掌握C 语言的核心概念。

第48 条:多用块枚举,少用for 循环

语言中引入“块”这一特性后,又多出来几种新的遍历方式,采用这几种新方式遍历 collection 时,通常会大幅度简化编码过程,笔者下面将会详细说明。

for循环
for循环是大家都很熟悉的写法。但是用它遍历字典或者set就会很麻烦。因为字典与 set 都是“无序的”,所以无法根据特定的整数下标来直接访问其中的值。
for循环也可以执行反向遍历,执行反向遍历时,使用 for 循环会比其他方式简单许多。

使用 NSEnumerator 来遍历
NSEnumerator 是个抽象基类,其中只定义了两个方法,供其具体子类(concrete subclass)来实现:

- (NSArray *)allObjects;
- (id)nextObject;

其中关键的方法是 nextObject,它返回枚举里的下个对象。每次调用该方法时,其内部数据结构都会更新,使得下次调用方法时能返回下个对象。等到枚举中的全部对象都已返回之后,再调用就将返回 nil,这表示达到枚举末端了。
例如遍历字典和set:

NSDictionary *aDictionary = /*...*/;
  NSEnumerator *enumerator = [aDictionary keyEnumerator];
  id key;
  while ((key = [enumerator nextObject]) != nil) {
    id value = aDictionary[key];
    // Do something with 'key' and 'value'
  }

  // set 
  NSSet *aSet = /*...*/;
  NSEnumerator *enumerator = [aSet objectEnumerator];
  id object;
  while ((object = [enumerator nextObject]) != nil) {
    // Do something with 'object'
  }

在第一段代码中,使用了 NSDictionary 类的 keyEnumerator 方法来获取字典中所有键的枚举器,然后通过枚举器逐个获取键,并使用键来访问字典中的值。在注释部分的代码块中,可以对每个键值对执行相应的操作。
在第二段代码中,使用了 NSSet 类的 objectEnumerator 方法来获取集合中的所有对象的枚举器,然后通过枚举器逐个获取集合中的对象。在注释部分的代码块中,可以对每个对象执行相应的操作。

快速遍历
快速遍历与使用NSEnumerator 来遍历差不多,然而语法更简洁,它就是for…in…
这样写简单多了。如果某个类的对象支持快速遍历,那么就可以宣称自己遵从名为 NSFastEnumeration 的协议,从而令开发者可以采用此语法来迭代该对象。

由于 NSEnumerator 对象也实现了 NSFastEnumeration 协议,所以能用来执行反向遍历。若要反向遍历数组,可采用下面这种写法:

NSArray anArray = /…*/;
  for (id object in [anArray reverseObjectEnumerator]) {

// Do something with ‘object’
  }
基于块的遍历方式
最新引入的一种做法就是基于块来遍历。NSArray 中定义了下面这个方法,它可以实现最基本的遍历功能:

  • (void)enumerateObjectsUsingBlock:(void(^)(id object, NSUInteger idx, BOOL *stop))block;

这个方法的方法名是enumerateObjectsUsingBlock:,它的参数:一个块作为参数,块包含三个参数:object:数组中的元素;idx:元素在数组中的索引;stop:一个指向布尔值的指针,用于控制遍历过程。如果将 *stop 设置为 YES,则遍历会停止。
该方法遍历时既能获取对象,也能知道其下标。此方法还提供了一种优雅的机制,用于终止遍历操作。
用它遍历字典与 set 也同样简单:

// Dictonary
  NSDictionary aDictionary = /…*/;
  [aDictionary enumerateKeyAndObjectsUsingBlock:
    ^(id key, id object, BOOL *stop){
      // Do something with ‘key’ and ‘object’
      if (shouldStop) {
        *stop = YES;
      }
  }];

// Set
  NSSet aSet = /…*/;
  [aSet enumerateObjectsUsingBlock:
    ^(id object, BOOL *stop) {
      // Do something with ‘object’
      if (shouldStop) {
        *stop = YES;
      }
  }];

此方式大大胜过其他方式的地方在于:遍历时可以直接从块里获取更多信息。在遍历数组时,可以知道当前所针对的下标。遍历有序set (NSOrderedSet)时也一样。而在遍历字典时,无须额外编码,即可同时获取键与值,因而省去了根据给定键来获取对应值这一步。
另外一个好处是,能够修改块的方法签名,以免进行类型转换操作。
若已知字典中的对象必为字符串,则用基于块的方式来遍历可以这样编码:

NSDictionary aDictionary = /…*/;
  [aDictionary enumerateKeysAndObjectsUsingBlock:
    ^(NSString *key, NSString *obj, BOOL *stop) {
      // Do something with ‘key’ and ‘obj’
  }];
之所以能如此,是因为 id 类型相当特殊,它可以像本例这样,为其他类型所覆写。
用此方式也可以执行反向遍历。数组、字典、set 都实现了前述方法的另一个版本,使开发者可向其传入 “选项掩码”(option mask):

- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id obj, NSUInteger idx, BOOL *stop))block
  - (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id key, id obj, BOOL *stop))block
NSEnumerationOption 类型是个 enum,其各种取值可用“按位或”(bitwise OR)连接,用以表明遍历方式。

遍历collection 有四种方法。最基本的办法是 for 循环,其次是 NSEnumerator 遍历法及快速遍历法,最新、最先进的方式则是“块枚举法”。
“块枚举法”本身就能通过GCD 来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点。
若提前知道待遍历的 collection 含有何种对象,则应修改块签名,指出对象的具体类型。

第49 条:对自定义其内存管理语义 的collection使用无缝桥接

CoreFoundation 框架也定义了一套C 语言API,用于操作表示这些collection 及其他各种collection 的数据结构。例如,NSArray 是Foundation 框架中表示数组的 Objective-C 类,而CFArray 则是 CoreFoundation 框架中的等价物。这两种创建数组的方式也许有区别,然而有项强大的功能可在这两个类型之间平滑转换,它就是 “无缝桥接”(toll-free bridging)。
使用“无缝桥接”技术,可以在定义于 Foundation 框架中的 Objective-C 类和定义于 CoreFoundation 框架中的C数据结构之间相互转换。
下列代码演示了简单的无缝桥接:

NSArray *anNSArray = @[@1, @2, @3, @4, @5];
  CFArrayRef aCFArray = (_bridge CFArrayRef)anNSArray;
  NSLog(@“Size of array = %li”, CFArrayGetCount(aCFArray));
  // Output: size of array = 5
这段代码演示了如何将NSArray对象转换为CFArrayRef类型,使用__bridge进行类型转换,将anNSArray转换为CFArrayRef类型的对象aCFArray。
__bridge 本身的意思是:ARC 仍然具备这个 Objective-C 对象的所用权。而 __bridge_retained 则与之相反,意味着ARC 将交出对象的所有权。与之相似,反向转换可通过 __bridge_transfer 来实现。这三种转换方式称为 “桥式转换”。
在使用 Foundation 框架中的字典对象时会遇到一个大问题,那就是其键的内存管理语义为 “拷贝”,而值的语义是却是“保留”。除非使用强大的无缝桥接技术,否则无法改变其语义。
CoreFoundation 框架中的字典类型叫做 CFDictionary。其可变版本称为 CFMutableDictionary 。创建 CFMutableDictionary 时,可以通过下列方法来指定键和值的内存管理语义:

CFMutableDictionaryRef CFDictionaryCreateMutable (
    CFAllocatorRef allocator,
    CFIndex capacity,
    const CFDictionaryKeyCallBacks *keyCallBacks,
    const CFDictionaryValueCallBacks *valueCallBacks
  }
首个参数表示将要使用的内存分配器。CoreFoundation 对象里的数据结构需要占用内存,而分配器负责分配及回收这些内存。开发者通常为这个参数传入 NULL,表示采用默认的分配器。第二个参数定义了字典的初始大小。它并不会限制字典的最大容量,只是向分配器提示了一开始应该分配多少内存。最后两个参数定义了许多回调函数,用于指示字典中的键和值在遇到各种事件时应该执行何种操作。

通过无缝桥接技术,可以在 Foundation 框架中的 Objective-C 对象与 CoreFoundation 框架中的 C 语言数据结构之间来回转换。
在 CoreFoundation 层面创建collection 时,可以指定许多回调函数,这些函数表示此collection 应如何处理其元素。然后,可运用无缝桥接技术,将其转换成具备特殊内存管理语义的 Objective-C collection。

第50 条:构建缓存时选用NSCache 而非NSDictionary

实现缓存时使用NSCache 类更好,它是 Foundation 框架专为处理这种任务而设计的。。
NSCache 胜过 NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。
NSCache 并不会“拷贝”键,而是会 “保留”它。NSCache 对象不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。
另外,NSCache 是线程安全的。而 NSDictionary 则绝对不具备此优势,意思就是:在开发者自己不编写加锁代码的前提下,多个线程便可以同时访问 NSCache。
开发者可以操控缓存删减其内容的时机。有两个与系统资源相关的尺度可供调整,其一是缓存中的对象总数,其二是所有对象的“总开销”。开发者在将对象加入缓存时,可为其指定“开销值”。当对象总数或总开销超过上限时,缓存就可能会删减其中的对象了,在可用的系统资源趋于紧张时,也会这么做。
向缓存中添加对象时,需要计算对象的“开销值”。这个“开销值”是一个附加因素,通常用于帮助决定何时从缓存中移除对象。

下面这段代码演示了缓存的用法:

#import <Foundation/Foundation.h>

typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
  @interface EOCNetworkFetcher : NSObject
  - (id)initWithURL:(NSURL *)url;
  - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
  @end

@interface EOCClass : NSObject

@end
  @implementation EOCClass {
    NSCache *_cache;
  }

- (id)init {
    if ((self = [super init])) {
      _cache = [NSCache new];
      _cache.countLimit = 100;
      /**
      * The Size in bytes of data is used as the cost,
      * so this sets a cost limit of 5MB.
      */
      _cache.totalCostLimit = 5 * 1024 * 1024;
    }
    return self;
  }

- (void)downloadDataForURL:(NSURL *)url {
    NSData *cachedData = [_cache objectForKey:url];
    if (cachedData) {
      // Cache hit
      [self useData:cacheData];
     } else {
      // Cache miss
      EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
      [fetcher startWithCompletionHandler:^(NSData *data) {
        [_cache setObject:data forKey:url cost:data.length];
        [self useData:data];
      }];
     }
    }
  @end

这段代码实现了一个网络数据下载器 EOCNetworkFetcher 和一个使用了缓存的类 EOCClass。
EOCNetworkFetcher 类:负责从指定的 URL 下载数据。它包含了一个初始化方法 initWithURL: 和一个启动下载的方法 startWithCompletionHandler:。
EOCClass 类:包含了一个 NSCache 对象 _cache,用于缓存下载的数据。它还有一个方法 downloadDataForURL:,用于下载指定 URL 的数据,首先检查缓存中是否有数据,如果有则直接使用缓存的数据,如果没有则启动网络下载器进行下载,并将下载的数据存入缓存中。
在本例中,下载数据所用的 URL,就是缓存的键。若缓存中没有访问者所需的数据,则下载数据并将其放入缓存。

还有个类叫做 NSPurgeableData,此类是NSMutableData 的子类,而且实现了 NSDiscardableContent 协议。如果某个对象所占的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口。
如果将 NSPurgeableData 对象加入NSCache,那么当该对象为系统所丢弃时,也会自动从缓存中移除。通过 NSCache 的 evictsObjectsWithDiscardedContent 属性,可以开启或关闭此功能。
使用 NSPurgeableData 改写的话:

- (void)downloadDataForURL:(NSURL *)url {
    NSPurgeableData *cachedData = [_cache objectForKey:url];
    if (cachedData) {
      [cacheData beginContentAccess];
      [self useData:cachedData];
      [cacheData endContentAccess];
    } else {
      EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
      [fetcher startWithCompletionHandler:^(NSData *data) {
        NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
        [_cache setObject:purgeableData forKey:url cost: purgeableData.length];
        [self useData:data];
        [purgeableData endContentAccess];
       }];
    }    
  }

这段代码通过 _cache 对象使用给定的 url 从缓存中获取数据。这里使用了 NSPurgeableData 类型的 cachedData 对象来存储获取到的数据。
如果缓存中存在数据(即 cachedData 不为 nil),则进入缓存命中的分支。在这个分支中,首先通过 beginContentAccess 方法将数据标记为开始访问状态,然后使用该数据,最后通过 endContentAccess 方法将访问结束。
如果缓存中不存在数据,则进入缓存未命中的分支。在这个分支中,首先创建一个 EOCNetworkFetcher 对象,用于从网络下载数据。在下载完成后,将下载的数据封装为 NSPurgeableData 对象,并存入缓存中。然后使用该数据,最后通过 endContentAccess 方法将访问结束。

实现缓存时应选用 NSCache 而非 NSDictionary 对象。因为 NSCache 可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键。
可以给 NSCache 对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的“硬限制”(hard limit),它们仅对NSCache 起指导作用。
将 NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能,也就是说,当 NSPurgeableData 对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种 “重新计算起来很费事的”数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

第 51 条 : 精简 initialize 与 load 的实现代码

有时候类必须先执行某些初始化操作,然后才能正常使用。-(void) load 方法,它是一个类方法,用于在类或分类被加载到运行时系统时执行。每个类和分类都会在程序启动时调用一次 load 方法,且仅调用一次。
load 方法的执行顺序是先执行类的 load 方法,然后执行分类的 load 方法。这意味着,如果一个类有多个分类,并且它们都实现了 load 方法,那么先执行类的 load 方法,然后按照分类的引入顺序依次执行各个分类的 load 方法。
然而,load 方法存在一个问题,即在执行该方法时,运行时系统处于“脆弱状态”。这意味着在 load 方法中使用其他类可能是不安全的,因为在执行子类的 load 方法之前,必定会先执行所有超类的 load 方法。而在加载依赖的其他库时,无法确定其中各个类的加载顺序,因此在 load 方法中使用其他类是不可靠的。
比如说:

#import “EOCClassA.h”
@implementation EOCClassB

  • (void)load {
        NSLog(@“Loading EOCClassB”);
        EOCClassA *object = [EOCClassA new];
      }

在 EOCClassB 的load 方法里使用 EOCClassA 却不太安全,因为无法确定在执行 EOCClassB 的 load 方法之前,EOCClassA 是不是已经加载好了。
load 方法不像普通的方法一样遵循继承规则。如果一个类自身没有实现 load 方法,那么无论其超类是否实现了 load 方法,系统都不会自动调用。这意味着子类的 load 方法不会自动调用其父类的 load 方法,除非子类自己实现了 load 方法并在其中显式调用了父类的 load 方法。

而且 load 方法务必实现得精简一些,也就是要尽量减少其所执行的操作,因为整个应用程序在执行load 方法时都会阻塞。

想执行与类相关的初始化操作,还有个办法,就是覆写下列方法:

  • (void)initialize;

对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。 它是由运行期系统来调用的,绝不应该通过代码直接调用。只有当程序用到了相关的类时,它才会调用。
此方法与 load 还有个区别,就是运行期系统在执行该方法时,是处于正常状态的,因此,从运行期系统完整度上来讲,此时可以安全使用并调用任意类中的任意方法。
最后一个区别是: initialize 方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码。即它是可以继承的。
当初始化基类 EOCBaseClass 时,EOCBaseClass 中定义的 initialize 方法要运行一遍,而当初始化 EOCSubClass 时,由于该类并未覆写此方法,因而还要把父类的实现代码再运行一遍。鉴于此,通常都会这么来实现 initialize 方法:

  • (void)initialize {
      if (self == [EOCBaseClass class]) {
        NSLog(@“%@ initialized”, self);
      }
    }
    load 与 initialize 方法的实现代码要尽量精简。在里面设置一些状态,使本类能够正常运作就可以了,不要执行那种耗时太久或需要加锁的任务。对于 load 方法来说,其原因已在前面解释过了,而 initialize 方法要保持精简的原因,也与之相似。
    其二,开发者无法控制类的初始化时机。类在首次使用之前,肯定要初始化,但编写程序时不能令代码依赖特定的时间点,否则会很危险。
    最后一个原因,如果某个类的实现代码很复杂,那么其中可能会直接或间接用到其他类。若那些类尚未初始化,则系统会迫使其初始化。

initialize 方法只应该用来设置内部数据。不应该在其中调用其他方法,即便是本类自己的方法,也最好别调用。

在加载阶段,如果类实现了 load 方法,那么系统就会调用它。分类里也可以定义此方法,类的 load 方法要比分类中的先调用。与其他方法不同,load 方法不参与覆写机制。
首次使用某个类之前,系统会向其发送 initialize 消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类。
load 与 initialize 方法都应该实现的精简一些,这有助于保持应用程序的响应能力,也能减少引入 “保留环”(interdependency cycle)的几率。
无法在编译期设定的全局常量,可以放在 initialize 方法里初始化。

第52条:别忘了NSTimer 会保留其 目标对象

计时器要和 “运行循环”(run loop)相关联,运行循环到时候会触发任务。创建 NSTimer 时,可以将其“预先安排”在当前的运行循环中,也可以先创建好,然后由开发者自己来调度。无论采用哪种方式,只有把计时器放在运行循环里,它才能正常触发任务。
由于计时器会保留其目标对象,所以反复执行任务通常会导致应用程序出问题。也就是说,设置成重复执行模式的那种计时器,很容易引入 “保留环”。
比如说下列代码:

#import <Foundation/Foundation.h>
  @interface EOCClass : NSObject
  - (void)startPolling;
  - (void)stopPolling;
  @end

@implementation EOCClass {
    NSTimer *_pollTimer;
  }
  - (id)init {
    return [super init];
  }
  - (void)dealloc {
    [_pollTimer invalidate];
  }
  - (void)stopPolling {
    [_pollTimer invalidate];
    _pollTimer = nil;
  }
  - (void)startPolling {
    _pollTimer = [NSTimer scheduledTimerWithTimeInterval: 5.0 target: self selector:@selector(p_doPoll) userInfo: nil repeats: YES];
  }
  - (void)p_doPoll {
  }
  @end

这段代码有个问题:当创建 EOCClass 实例并调用其 startPolling 方法时,会创建一个 NSTimer 对象并将其赋值给 _pollTimer 实例变量。由于 NSTimer 对象的目标对象是 EOCClass 实例本身,因此会对 EOCClass 实例进行强引用,导致 EOCClass 实例无法被释放。而 _pollTimer 实例变量也会对 NSTimer 对象进行强引用,使得 NSTimer 对象也无法被释放。这样就形成了一个保留环,即相互持有对方的强引用,导致内存泄漏。
如果想在系统回收本类实例的过程中令计时器无效,从而打破保留环,那又会陷入死结。因为在计时器对象尚且有效时,EOCClass 实例的保留计数绝不会降为 0 ,因此系统也绝不会将其回收。而现在又没人来调用 invalidate 方法,所以计时器将一直处于有效状态。
当指向 EOCClass 实例的最后一个外部引用被移除后,该实例仍然存活,因为计时器持有对它的强引用。同时,计时器对象也无法被系统释放,因为它被 EOCClass 实例强引用。这导致了实例和计时器对象互相持有对方的强引用,形成了保留环,导致内存泄漏。

更糟糕的是,除了计时器之外,已经没有其他引用指向 EOCClass 实例了,因此该实例会永远被保留,无法被释放。

这个问题可通过“块”来解决。虽然计时器当前并不直接支持块,但是可以用下面这段代码为其添加此功能:

#import <Foundation/Foundation.h>

@interface NSTimer (EOCBlockSupport)

  • (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;

@end

@implementation NSTimer (EOCBlocksSupport)

  • (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats {
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(eoc_blockInvoke:) userInfo:[block copy] repeats:repeats];
    }

  • (void)eoc_blockInvoke:(NSTimer *)timer {
    void (^block)() = timer.userInfo;
    if (block) {
    block();
    }
    }

@end

这段代码为 NSTimer 添加了支持块(blocks)的功能。通过类别(category),扩展了 NSTimer 类,添加了一个类方法 eoc_scheduledTimerWithTimeInterval:block:repeats:,用于创建带有块回调的定时器。
在 eoc_scheduledTimerWithTimeInterval:block:repeats: 方法中,调用了 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 方法创建了一个定时器,并传入了一个 selector,但是这个 selector 实际上是 eoc_blockInvoke: 方法。同时,将传入的块对象通过 copy 方法复制,并作为 userInfo 参数传递给定时器。
eoc_blockInvoke: 方法是一个类方法,用于实际执行定时器触发时的回调操作。它从定时器的 userInfo 中获取了保存的块对象,并执行该块对象。
这样,通过调用 eoc_scheduledTimerWithTimeInterval:block:repeats: 方法创建的定时器,当触发定时器时,就会执行传入的块代码块。

我们修改刚才那段有问题的范例代码,使用新分类中的 eoc_scheduledTimerWithTimeInterval 方法来创建计时器并改用 weak 引用,即可打破保留环:

- (void)startPolling {
    __weak EOCClass *weakSelf = self;
    _pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval: 5.0 block:^{
          EOCClass *strongSelf = weakSelf;
          [strongSelf p_doPoll];
          } repeats:YES];
  }

这段代码采用了一种很有效的写法,它先定义了一个弱引用,令其指向 self,然后使块捕获这个引用,而不直接去捕获普通的 self 变量,也就是说,self 不会为计时器所保留。当块开始执行时,立刻生成 strong 引用,以保证实例在执行期间持续存活。
采用这种写法之后,如果外界指向 EOCClass 实例的最后一个引用将其释放,则该实例就可为系统所回收了。

NSTimer 对象会保留其目标,直到计时器本身失效为止,调用 invalidate 方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。
反复执行任务的计时器(repeating timer),很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。这种环状保留关系,可能是直接发生的,也可能是通过对象图里的其他对象间接发生的。
可以扩充 NSTimer 的功能,用 “块”来打破保留环。不过,除非 NSTimer 将来在公共接口里提供此功能,否则必须创建分类,将相关实现代码加入其中。

  • 28
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值