

第一章:熟悉 Objective-C
了解 Objective-C 语言的起源
OC 的动态性都是由 “运行期组件” ,也就是 Runtime 库(ObjC4)来实现的,使用 OC 的面向对象特性所需的全部数据结构以及函数都在 ObjC4 里面。
运行期组件本质上是一种与开发者所编写的代码相链接的动态库
(dynamic library),其代码能把开发者所编写的所有程序粘合起来,所以只要更新运行期组件,就可以提升应用程序性能。
在类的头文件中尽量少引用其他头文件
有时,类A需要将类B的实例变量作为它公共API的属性。这个时候,我们不应该引入类B的头文件,而应该使用向前声明(forward declaring)使用class关键字,并且在A的实现文件引用B的头文件。
1 | // EOCPerson.h |
优点:
不在A的头文件中引入B的头文件,就不会一并引入B的全部内容,这样就减少了编译时间。
可以避免循环引用:因为如果两个类在自己的头文件中都引入了对方的头文件,那么就会导致其中一个类无法被正确编译。
个别的时候,必须在头文件中引入其他类的头文件:
- 该类继承于某个类,则应该引入父类的头文件。
- 该类遵从某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中。
多用类型常量,少用 #define 预处理指令
不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
在实现文件中使用 static const 来定义 “只在编译单元内可见的常量”(translation-unit-specific constant)。由于此类常量不在
全局符号表
中,所以无须为其名称加前缀。在头文件中使用
extern
来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表
中,所以其名称应加以区隔,通常用与之相关的类名做前缀
。
类型常量
1 | static const NSTimeInterval kAnimationDuration = 0.3; |
static 修饰符意味着该常量只在定义它的 .m 中可见(设置了其使用范围)
const 修饰符意味着该常量不可修改(不可修改)
类型常量命名法
- 如果常量局限于某 “编译单元”(也就是 .m 中),则命名前面加字母 k,比如
kAnimationDuration
。 - 如果常量在类之外可见,定义成了全局常量,则通常以 类名 作为前缀,比如
EOCViewClassAnimationDuration
。
局部类型常量
1 | static const NSTimeInterval kAnimationDuration = 0.3; |
- 一定要同时使用 static 和 const 来声明。这样编译器就不会创建符号,而是像预处理指令一样,进行值替换。
- 如果试图修改由 const 修饰的变量,编译器就会报错。
- 如果不加 static,则编译器就会它创建一个 “外部符号 symbol”。此时如果另一个编译单元中也声明了同名变量,那么编译器就会抛出 “重复定义符号” 的错误:
1 | duplicate symbol _kAnimationDuration in: |
- 局部类型常量不放在 “全局符号表” 中,所以无须用类名作为前缀。
全局类型常量
1 | // In the header file |
- 此类常量会被放在
“全局符号表”
中,这样才可以在定义该常量的编译单元之外使用。 - const 位置不同则常量类型不同,以上为,定义一个指针常量
EOCStringConstant
,指向 NSString 对象。也就是说,EOCStringConstant
不会再指向另一个 NSString 对象。 - extern 是告诉编译器,在 “全局符号表” 中将会有一个名叫
EOCStringConstant
的符号。这样编译器就允许代码使用该常量。因为它知道,当链接成二进制文件后,肯定能找到这个常量。 - 必须要定义,而且只能定义一次,通常定义在声明该常量的 .h 的对应的 .m 中。
- 在实现文件生成目标文件时(编译器每收到一个 “编译单元” .m,就会输出一份 “目标文件” ),编译器会在 “数据段” 为字符串分配存储空间。链接器会把此目标文件与其他目标文件相链接,以生成最终的二进制文件。凡是用到
EOCStringConstant
这个全局符号的地方,链接器都能将其解析。 - 因为符号要放在全局符号表里,所以常量命名需谨慎,为避免名称冲突,一般以类名作为前缀。
第二章:对象、消息、运行期
理解 “属性” 这一概念
@synthesize
和 @dynamic
- 可以通过
@synthesize
来指定实例变量名字,如果你不喜欢默认的以下划线开头来命名实例变量的话。但最好还是用默认的,否则别人可能看不懂。
如果不想令编译器合成存取方法,则可以自己实现。如果你只实现了其中一个存取方法 setter
or getter
,那么另一个还是会由编译器来合成。但是需要注意的是,如果你实现了属性所需的全部方法(如果属性是 readwrite
则需实现 setter
and getter
,如果是 readonly
则只需实现 getter
方法),那么编译器就不会自动进行 @synthesize
,这时候就不会生成该属性的实例变量,需要根据实际情况自己手动 @synthesize
一下。
@synthesize name = _myName;
@dynamic
会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法,即告诉编译器你要自己做这些事。当使用了@dynamic
,即使你没有为其实现存取方法,编译器也不会报错,因为你已经告诉它你要自己来做。
- 注意:遵循属性定义
如果属性定义为copy
,那么在非设置方法里设定属性的时候,也要遵循copy
的语义
1 | - (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName |
理解 “对象等同性” 这一概念
==
操作符比较的是两个指针本身,而不是其所指的对象,应该使用 NSObject
协议中声明的 isEqual
: 方法来判断两个对象的等同性。某些对象提供了特殊的“等同性判定方法”,比如 NSString
的 isEqualToString
:、NSArray
的 isEqualToArray
:、NSDictionary
的 isEqualToDictionary
:。
理解Objective-C错误类型
用NSError描述错误。 使用NSError可以封装三种信息:
Error domain
:错误范围,类型是字符串Error code
:错误码,类型是整数User info
:用户信息,类型是字典
接口与API设计
理解 NSCopying 协议
- 对 mutable 对象与 immutable 对象 进行 copy 与 mutableCopy 的结果:
注:这里的 mutable 对象与 immutable 对象指的是系统类 NSArray、NSDictionary、NSSet、NSString、NSData 与它们的可变版本如 NSMutableArray 等。
- 以上对 collection 容器对象进行的深浅拷贝是指对容器对象本身的,对 collection 中的对象执行的
默认都是浅拷贝
。也就是说只拷贝容器对象本身,而不复制其中的数据。主要原因是,容器内的对象未必都能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中的每个对象。

- 如果想要实现对自定义对象的拷贝,需要遵守 NSCopying 协议,并实现
copyWithZone
: 方法
如果要浅拷贝,copyWithZone: 方法就返回同一个对象:return self;
如果要深拷贝,copyWithZone: 方法中就创建新对象,并给希望拷贝的属性赋值。
- 如果自定义对象支持可变拷贝和不可变拷贝,那么还需要遵守
NSMutableCopying
协议,并实现mutableCopyWithZone
: 方法,返回可变副本。而copyWithZone
: 方法返回不可变副本。使用方可根据需要调用该对象的copy
或mutableCopy
方法来进行不可变拷贝或可变拷贝。
尽量使用不可变对象
把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体做法是:
在头文件中,设置对象属性为readonly
,在实现文件中设置为readwrite
。这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。
1 | @interface EOCPerson : NSObject |
在这里,我们将friends属性设置为不可变的set。然后,提供了来增加和删除这个set里的元素的公共接口。
1 | @interface EOCPerson () |
- 在公共接口设置不可变
set
和 将增删的代码放在公共接口中是否矛盾的?
因为如果将friends
属性设置为可变的,那么外部就可以随便更改set
集合里的数据,这里的更改,仅仅是底层数据的更改,并不伴随其他任何操作。
然而有时,我们需要在更改set数据的同时要执行隐秘在实现文件里的其他工作,那么如果在外部随意更改这个属性的话,显然是达不到这种需求的。
因此,我们需要提供给外界我们定制的增删的方法,并不让外部”自行“增删。
协议与分类
通过委托与数据源协议进行对象间通信
可以通过委托 (也就是我们平常所说的 “代理 delegate” )与数据源(data source)协议进行对象间通信。
协议中可以定义什么?方法和属性。
委托模式(代理模式)
代理模式的主旨:
定义一个委托协议,若对象想接受另一个对象(委托方)的委托,则需遵守该协议,以成为 “代理方
”。而委托方则可以通过协议方法给代理方回传一些信息,也可以在发生相关事件时通知代理方。这样委托方就可以把应对某个行为的责任委托给代理方去处理了。代理的工作流程:
“委托方” 要求 “代理方” 需要实现的接口,全都定义在 “委托协议” 当中;
“代理方” 遵守 “协议” 并实现 “协议” 方法;
“代理方” 所实现的 “协议” 方法可能会有返回值,将返回值返回给 “委托方” ;
“委托方” 调用 “代理方” 遵从的 “协议” 方法。

数据源模式
旨在向类提供数据,所以也称 “数据源模式”。
数据源模式是用协议定义一套接口,令某类经由该接口获取其所需的数据。数据源模式与常规委托模式的区别在于:
数据源模式中,信息从数据源(Data Source)流向类(委托方);
常规委托模式中,信息从类(委托方)流向受委托者(代理方)。通过 UITableView 就可以很好的理解 Data Source 和 Delegate 这两种模式:
通过 UITableViewDataSource 协议获取要在列表中显示的数据;
通过 UITableViewDelegate 协议来处理用户与列表的交互操作。
1 | @protocol UITableViewDataSource<NSObject> |
性能优化
在实现委托模式和数据源模式时,如果协议方法时可选的,那么在调用协议方法时就需要判断其是否能响应。
1 | if (_delegate && [_delegate respondsToSelector:@selector(networkFetcher:didReceiveData)]) { |
如果我们需要频繁调用该协议方法,那么仅需要第一次判断是否能响应即可。以上代码可做性能优化,将代理方是否能响应某个协议方法这一信息缓存起来:
- 在委托方中嵌入一个含有位域(bitfield,又称 “位段”、“位字段”)的结构体作为其实例变量,而结构体中的每个位域则表示 delegate 对象是否实现了协议中的相关方法。该结构体就是用来缓存代理方是否能响应特定的协议方法的。
1 | @interface EOCNetworkFetcher () { |
- 重写 delegate 属性的 setter 方法,对 _delegateFlags 结构体里的标志进行赋值,实现缓存功能。
1 | - (void)setDelegate:(id<EOCNetworkFetcher>)delegate { |
- 这样每次调用 delegate 的相关方法之前,就不用通过 respondsToSelector: 方法来检测代理方是否能响应特定协议方法了,而是直接查询结构体中的标志,提升了执行速度。
1 | if (_delegate && _delegateFlags.didReceiveData) { |
内存管理
用 “僵尸对象” 调试内存管理问题
- 向已回收的对象发送消息是不安全的,对象所占内存在 “解除分配(deallocated)” 之后,只是放回可用内存池。如果对象所占内存还没有分配给别人,这时候访问没有问题,如果已经分配给了别人,再次访问就会崩溃。
这在调试的时候可能不太方便,我们可以通过 “僵尸对象(Zombie Object)
” 来更好地调试内存管理问题。
僵尸对象的启用:
通过环境变量NSZombieEnabled
启用 “僵尸对象” 功能。僵尸对象的工作原理:
它的实现代码深植与 Objective-C 的运行期库、Foundation 框架及 CoreFoundation 框架中。系统在即将回收对象时,如果发现NSZombieEnabled == YES
,那么就把对象转化为僵尸对象,而不是将其真的回收。接下来给该对象(此时已是僵尸对象)发送消息,就会在控制台打印一条包含消息内容及其接收者的信息(如下),然后终止应用程序。这样我们就能知道在何时何处向业已回收的对象发送消息了。
1 | [EOCClass message] : message sent to deallocated instance 0x7fc821c02a00 |
- 僵尸对象的实现原理:
- 在启用僵尸对象后,运行期系统就会 swizzle 交换 dealloc 方法实现,当每个对象即将被系统回收时,系统都会为其创建一个
_NSZombie_OriginalClass
类。(OriginalClass
为对象所属类类名,这些类直接由_NSZombie_
类拷贝而来而不是使用效率更低的继承,然后赋予类新的名字_NSZombie_OriginalClass
来记住旧类名。记住旧类名是为了在给僵尸对象发送消息时,系统可由此知道该对象原来所属的类。)。
然后将对象的 isa 指针指向僵尸类,从而待回收的对象变为僵尸对象。(由于是交换了 dealloc 方法,所有 free() 函数就不会执行,对象所占内存也就不会释放。虽然这样内存泄漏了,但也只是调试手段而已,所以泄漏问题无关紧要)。 - 由于
_NSZombie_
类(以及所有从该类拷贝出来的类_NSZombie_OriginalClass
)没有实现任何方法,所以给僵尸对象发送任何消息,都会进入 “完整的消息转发阶段”。而在 “完整的消息转发阶段” 中,__forwarding__
函数是核心。它首先要做的事情包括检查接收消息的对象所属的类名。若前缀为_NSZombie_
则表明消息接收者是僵尸对象,需要特殊处理:在控制台打印一条信息(信息中指明僵尸对象所接收到的消息、原来所属的类、内存地址等,[OriginalClass message] : message sent to deallocated instance 0x7fc821c02a00
),然后终止应用程序。
块与大中枢派发
理解 “块” 这一概念
如果块中没有显式地使用 self 来访问实例变量,那么块就会隐式捕获 self,这很容易在我们不经意间造成循环引用。如下代码,编译器会给出警告,建议用 self->_anInstanceVariable
或 self.anInstanceVariable
来访问。
1 | self.block = ^{ |
块的内存布局
- isa 指针指向 Class 对象
- invoke 变量是个函数指针,指向块的实现代码
- descriptor 变量是指向结构体的指针,其中声明了块对象的总体大小,还声明了保留和释放捕获的对象的 copy 和 dispose 这两个函数所对应的函数指针
- 块还会把它所捕获的所有变量都拷贝一份,放在 descriptor 变量的后面

多用派发队列,少用同步锁
- 用锁来实现同步,会有死锁的风险,而且效率也不是很高。而用 GCD 能以更简单、更高效的形式为代码加锁。
用锁实现同步
@synchronized
1 | - (void)synchronizedMethod { |
原理:@synchronized 会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕,然后释放锁。
缺点:滥用 @synchronized(self) 会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行,也就是说所有的 @synchronized(self) 块中的代码之间都同步了。若是在 self 对象上频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码,这样做其实并没有必要。
NSLock等
1 | _lock = [[NSLock alloc] init]; |
用GCD实现同步
GCD 串行同步队列
将读取操作和写入操作都安排在同一个队列里,即可保证数据同步。
1 | _syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL); |
也可以将 setter 方法的代码异步执行。由于 getter 方法需要返回值,所以需要同步执行以阻塞线程来防止提前 return,而 setter 方法不需要返回值所以可以异步执行。异步执行时需要拷贝 block
,所以这里异步执行是否能提高执行速度取决于 block 任务的繁重程度。如果拷贝 block 的时间超过执行 block 的时间,那么异步执行反而降低效率,而如果 block 任务繁重,那么是可以提高执行速度的。
GCD 栅栏函数
以上虽保证了读写安全,但并不是最优方案,因为读取方法之间同步执行了。
保证读写安全,只需满足三个条件即可:
- 同一时间,只能有一个线程进行写操作;
- 同一时间,允许有多个线程进行读操作;
- 同一时间,不允许既有读操作,又有写操作。
我们可以针对第二点进行优化,让读取方法可以并发执行。使用 GCD 栅栏函数:
1 | _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |

多用GCD,少用performSelector系列方法
NSObject 定义了几个 performSelector
系列方法,可以让开发者随意调用任何方法,可以推迟执行方法调用,也可以指定执行方法的线程等等。
1 | - (id)performSelector:(SEL)aSelector; |
performSelector:方法有什么用处?
- 如果你只是用来调用一个方法的话,那么它确实有点多余
- 用法一:selector 是在运行期决定的
1 | SEL selector; |
- 用法二:把 selector 保存起来等某个事件发生后再调用
performSelector:方法的缺点:
- 存在内存泄漏的隐患:
由于 selector 在运行期才确定,所以编译器不知道所要执行的 selector 是什么。如果在 ARC 下,编译器会给出警告,提示可能会导致内存泄漏。
由于编译器不知道所要执行的 selector 是什么,也就不知道其方法名、方法签名及返回值等,所以就没办法运用 ARC 的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC 采用了比较谨慎的做法,就是不添加释放操作,然而这样可能会导致内存泄漏,因为方法在返回对象时可能已经将其保留了。
如果是调用以
alloc/new/copy/mutableCopy
开头的方法,创建时就会持有对象,ARC 环境下编译器就会插入 release 方法来释放对象,而使用 performSelector 的话编译器就不添加释放操作,这就导致了内存泄漏。而其他名称开头的方法,返回的对象会被添加到自动释放池中,所以无须插入 release 方法,使用 performSelector 也就不会有问题。
返回值只能是 void 或对象类型
如果想返回基本数据类型,就需要执行一些复杂的转换操作,且容易出错;如果返回值类型是 C struct,则不可使用 performSelector 方法。参数类型和个数也有局限性
类型:参数类型必须是 id 类型,不能是基本数据类型;
个数:所执行的 selector 的参数最多只能有两个。而如果使用 performSelector 延后执行或是指定线程执行的方法,那么 selector 的参数最多只能有一个。
使用 GCD 替代 performSelector
- 如果要延后执行,可以使用 dispatch_after
- 如果要指定线程执行,那么 GCD 也完全可以做到
掌握 GCD 及操作队列的使用时机
根据实际情况使用GCD 或者 NSOperation,以下是它们的区别:

使用 NSOperation 和 NSOperationQueue 的优势:
- 取消某个操作
可以在执行操作之前调用NSOperation
的cancel
方法来取消,不过正在执行的操作无法取消。iOS8 以后 GCD 可以用dispatch_block_cancel
函数取消尚未执行的任务,正在执行的任务同样无法取消。 - 指定操作间的依赖关系
使特定的操作必须在另外一个操作顺利执行完以后才能执行。 - 通过 KVO 监控 NSOperation 对象的属性
在某个操作任务变更其状态时得到通知,比如 isCancelled、isFinished。而 GCD 不行。 - 指定操作的优先级
指定一个操作与队列中其他操作之间的优先级关系,优先级高的操作先执行,优先级低的则后执行。GCD 没有直接实现此功能的办法。 - 重用 NSOperation 对象
可以使用系统提供的NSOperation
子类(比如NSBlockOperation
),也可以自定义子类。
GCD 任务用块来表示,块是轻量级数据结构,而 NSOperation 则是更为重量级的 Objective-C 对象。虽说如此,但 GCD 并不是最佳方案。有时候采用对象所带来的开销微乎其微,反而它所到来的好处大大反超其缺点。另外,“应该尽可能选用高层 API,只在确有必要时才求助于底层” 这个说法并不绝对。某些功能确实可以用高层的 API 来做,但这并不等于说它就一定比底层实现方案好。要想确定哪种方案更佳,最好还是测试一下性能。
系统框架
对自定义其内存管理语义的 collection 使用无缝桥接
有 __bridge
、 __bridge_retained
、 __bridge_transfer
三种桥接方案,它们的区别为:
__bridge
:不改变对象的内存管理权所有者。__bridge_retained
:用在 Foundation 对象转换成 Core Foundation 对象时,进行 ARC 内存管理权的剥夺。__bridge_transfer
:用在 Core Foundation 对象转换成 Foundation 对象时,进行内存管理权的移交。
在使用 Foundation 框架中的字典对象时会遇到一个大问题,其键的内存管理语义为 “拷贝
”,而值的语义是 “保留
”。也就是说,在向 NSMutableDictionary 中加入键和值时,字典会自动 “拷贝” 键并 “保留” 值。如果用做键的对象不支持拷贝操作(如果要支持,就必须遵守 NSCopying
协议,并实现 copyWithZone: 方法),那么编译器会给出警告并在运行期 Crash:
1 | NSMutableDictionary *mDict = [NSMutableDictionary dictionary]; |
我们是无法直接修改 NSMutableDictionary
的键和值的内存管理语义的。这时候我们可以通过创建 CoreFoundation 框架的 CFMutableDictionary
C 数据结构,修改内存管理语义,对键执行 “保留” 而非 “拷贝” 操作,然后再通过无缝桥接技术,将其转换 NSMutableDictionary 对象
也可以使用 NSMapTable,指定 key 和 value 的内存管理语义。
构建缓存时选用NSCache而非NSDictionary
NSCache 的优势在于:
- 当系统资源将要耗尽时,它可以优雅的自动删减缓存,且会先行删减最久未使用的缓存。使用 NSDictionary 虽也可以自己实现但很复杂。
- NSCache 不会拷贝键,而是保留它。使用 NSDictionary 虽也可以实现但比较复杂
- NSCache 是线程安全的。不编写加锁代码的前提下,多个线程可以同时访问 NSCache。而 NSDictionary 不是线程安全的。
可以操控 NSCache 删减缓存的时机
totalCostLimit
限制缓存中所有对象的总开销countLimit
限制缓存中对象的总个数- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
将对象添加进缓存时,可指定其开销值
可能会删减缓存对象的时机:
- 当对象总数或者总开销超过上限时
- 在可用的系统资源趋于紧张时
需要注意的是:
- 可能会删减某个对象,并不意味着一定会删减
- 删减对象的顺序,由具体实现决定的
- 想通过调整开销值来迫使缓存优先删减某对象是不建议的,绝对不要把这些尺度当成可靠的 “硬限制”,它们仅对 NSCache 起指导作用。
使用 - (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
可以在将对象添加进缓存时指定其开销值。但这种情况只适用于开销值能很快计算出来的情况,因为缓存的本意就是为了增加应用程序响应用户操作的速度。
比方说,计算开销值时必须访问磁盘或者数据库才能确定文件大小,那么就不适用这种方法。
如果要加入缓存的是 NSData
对象,其数据大小已知,直接访问属性即可 data.length
。
NSPurgeableData
- NSPurgeableData 继承自 NSMutableData,它与 NSCache 搭配使用,可实现自动清除数据的功能。它实现了 NSDiscardableContent 协议(如果某个对象所占内存能够根据数据需要随时丢弃,就可以实现该协议定义的接口),将其加入 NSCache 后当该对象被系统所丢弃时,也会自动从缓存中清除。可以通过 NSCache 的 evictsObjectWithDiscardedContent 属性来开启或关闭此功能。
- 使用 NSPurgeableData 的方式和 “引用计数” 很像,当需要访问某个 NSPurgeableData 对象时,可以调用
beginContentAccess
进行 “持有”,并在用完时调用endContentAccess
进行 “释放”。NSPurgeableData 在创建的时候其 “引用计数” 就为 1,所以无须调用 beginContentAccess,只需要在使用完毕后调用 endContentAccess 就行。
beginContentAccess:告诉它现在还不应该丢弃自己所占据的内存
endContentAccess:告诉它必要时可以丢弃自己所占据的内存
NSPurgeableData 与 NSCache 一起实现缓存的代码示例:
1 | // Network fetcher class |
精简 initialize 与 load 的实现代码
使用 load 方法的问题和注意事项:
- 在
load
方法中使用其他类是不安全的。比方说,类 A 和 B 没有继承关系,它们之间load
方法的执行顺序是不确定的,而你在类 A 的 load 方法中去实例化 B,而类 B 可能会在其 load 方法中去完成实例化 B 前的一些重要操作,此时类 B 的 load 方法可能还未执行,所以不安全。 load
方法务必实现得精简一些,尽量减少其所执行的操作,不要执行耗时太久或需要加锁的任务,因为整个应用程序在执行load
方法时都会阻塞。- 如果任务没必要在类加载进内存时就执行,而是可以在类初始化时执行,那么改用
initialize
替代load
方法。
initialize
initialize
除了在调用时刻、调用方式、调用顺序方面与load
有区别以外。initialize
方法还是安全的。
运行期系统在执行initialize
时,是处于正常状态的,因为这时候可以安全使用并调用任意类中的任意方法了。而且运行期系统也能确保initialize
方法一定会在 “线程安全的环境” 中执行,只有执行 initialize 的那个线程可以操作类或类实例,其他线程都要先阻塞等着initialize
执行完。- 如果子类没有实现
initialize
方法,那么就会调用父类的,所以通常会在initialize
实现中对消息接收者做一下判断:
1 | + (void)initialize { |
initialize
的实现也要保持精简,其原因在于:
- 如果在主线程初始化一个类,那么初始化期间就会一直阻塞。
- 无法控制类的初始化时机。编写代码时不能令代码依赖特定的时间点执行,否则如果以后运行期系统更新改变了类的初始化方式,那么就会很危险。
- 如果在
initialize
中给其他类发送消息,那么会迫使这些类都进行初始化。如果其他类在执行initialize
时又依赖该类的某些数据,而该类的这些数据又在initialize
中完成,就会发生问题,产生 “环状依赖”。
所以,initialize
方法只应该用来设置内部数据,例如无法在编译期设定的全局常量,可以放在initialize
方法里初始化。不应该调用其他方法,即便是本类自己的方法,也最好别调用。
实现 load
和 initialize
方法时,一定要注意以上问题,精简代码。除了初始化全局状态之外,如果还有其他事情要做,那么可以专门创建一个方法来执行这些操作,并要求该类的使用者必须在使用本类之前调用此方法。比如说,如果 “单例类” 在首次使用之前必须执行一些操作,那就可以采用这个办法。
参考
《Effective Objective-C 2.0》52 个知识点总结
《Effective Objective-C》干货三部曲