编写高质量iOS与OS X代码的52个有效方法
第6章 块与大中枢派发
37.理解“块”这一概念
- 块是C、C++、Objective-C 中的词法闭包。
- 块可接受参数,也可返回值。
- 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的 Objective-C 对象一样,具备引用计数了。
38.为常用的块类型创建 typedef
- 以typedef重新定义块类型,可令块变量用起来更加简单。
- 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
- 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应的 typedef 中的块签名即可,无须改动其他typedef。
39.用 handler 块降低代码分散程度
- 在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
- 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用 handler 块来实现,则可直接将块与相关对象放在一起。
- 设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
40.用块引用其所属对象时不要出现保留环
1 | - (void)p_requestCompleted { |
只要下载请求执行完毕,保留环就解除了,而获取器对象也将会在必要时为系统所回收。
- 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
- 一定要找个适当的时机解除保留环,而不能把责任推给 API 的调用者。
41.多用派发队列,少用同步锁
锁
同步块(synchronization block)
1
2
3
4
5- (void)synchronizedMethod {
@synchronized(self) {
//Safe
}
}NSLock锁对象 NSRecursiveLock递归锁
1
2
3
4
5
6_lock = [[NSLock alloc] init];
- (void)synchronizedMethod {
[_lock lock];
//Safe
[_lock unlock];
}
缺陷:
- 在极端情况下,同步块会导致死锁。
- 效率不见得很高。
- 直接使用锁对象,一旦遇到死锁,就会非常麻烦。
- 用atomic来修饰属性,只能提供某种程度的线程安全,无法保证访问该对象时绝对是线程安全的。使用属性时,必然能从中获取到有效值,然而在同一线程上多次调用获取方法(getter),每次获取到的结果未必相同。在两次访问操作之间,其他线程可能会写入新的属性值。
- GCD
串行同步队列(serial synchronization queue)1
2
3
4
5
6
7
8
9
10
11
12
13_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(_syncQueue, ^{
_someString = someString;
});
}
思路是:把设置操作与获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作就都同步了。全部加锁任务都在GCD中处理。
并发队列(concurrent queue)
栅栏(barrier)
在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个来执行的。并发队列如果发现接下来要处理的块是个栅栏块,那么久一直要等栅栏块执行过后,再按正常方式继续向下处理。
要点
- 派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized 块或 NSLock 对象更简单。
- 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
- 使用同步队列及栅栏块,可以令同步行为更加高效。
42.多用GCD,少用 performSelector 系列方法
- performSelector 可能内存泄漏
performSelector 调用了一个方法。编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用ARC的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。 - 有局限性。
- 返回值只能是void或对象类型。
- 参数类型是id,所以只能传入对象。此外,最多只能接受两个参数。
dispatch_sync
dispatch_async
dispatch_after
要点
- performSelector 系列方法在内存管理方面容易有疏忽。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。
- performSelector 系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都收到限制。
- 如果想把任务放在另一个线程上执行,那么最好不要用 performSelector 系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。
43.掌握GCD及操作队列的使用时机
GCD与NSOperationQueue
- 操作队列在底层是用GCD来实现的。
- GCD是纯C的API,而操作队列则是Objective-C的对象。
- 在GCD中,任务用块来表示,而块是一个轻量级的数据结构。预支相反,“操作”(operation)则是个更为重量级的Objective-C对象。
使用NSOperation及NSOperationQueue的好处:
- 取消某个操作。运行任务之前,可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表明此任务不需执行,不过,已经启动的任务无法取消。若是不使用操作队列,而是把块安排在GCD队列,那就无法取消了。开发者可以在应用程序层自己来实现取消功能,不过这样做需要编写很多代码,而那些代码其实已经由操作队列实现好了。
- 指定操作间的依赖关系。
- 通过键值观察机制监控NSOperation对象的属性。如isCancelled、isFinished。如果想在某个任务变更其状态时得到通知,或是想用比GCD更为精细的方式来控制所要执行的任务,那么键值观察机制会很有用。
- 指定操作的优先级。GCD只有队列的优先级,没有任务的优先级。NSOpetation对象也有线程优先级,这决定了运行此操作的线程处在何种优先级上。GCD可以实现此功能,然而操作队列更简单,只需设置一个属性。
- 重用NSOperation对象。
系统的NSNotificationCenter API选用了操作队列而非派发队列,开发者可通过其中的方法来注册监听器,以便在发生相关事件时得到通知,而这个方法接受的参数是块,不是选择子。
要点
- 在解决多线程与任务管理问题时,派发队列并非唯一方案。
- 操作队列提供了一套高层的Objective-C API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码。
44.通过 Dispatch Group 机制,根据系统资源状况来执行任务
要点
- 一系列任务可归入一个 dispatch group 之中。开发者可以在这组任务执行完毕时获得通知。
- 通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码。
45. 使用dispatch_once来执行只需运行一次的线程安全代码
实现单例
1 | + (id)sharedInstance { |
要点
- 经常需要编写“只需执行一次的线程安全代码”(thread-safe single-code execution)。通过GCD所提供的 dispatch_once 函数,很容易就能实现此功能。
- 标记应该声明在 static 或 global 作用域中,这样的话,在把只需执行一次的块传给 dispatch_once 函数时,传进去的标记也是相同的。
46.不要使用 dispatch_get_current_queue
要点
- dispatch_get_current_queue 函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。
- 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
- dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。