第 37 条 理解“块”这一概念
Clang 是开发Mac OS X 及 iOS程序所用的编译器
块的基础知识
块的语法:
return_Type (^blockName)(parameter)
块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。这也就是说,那个氛围里的全部变量在块里依然可用。比如,下面这段代码所定义的块,就是用了块以外的变量:
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
retrun a + b + additional;
}
int add = addBlock(2,5);
默认情况下,为块所捕获的变量,是不可以在块里修改的。在本例中,假如块内的代码改动了additional变量的值,那么编译器就会报错。不过声明变量的时候可以加上__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 ++
}
}]
这段范例代码也演示了“内联块”的用法。传给“enumerateObjectsUsingBlock:”方法的块并未先赋给局部变量,而是直接内联在函数调用里了。
块是对象
- NSObject对象所能相应的选择子中,有很多是块也可以相应的
- 块的结构
- 于块本身也和其他对象一样,有引用计数
如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。这就引出了一个与块有关的重要问题。块本身可视为对象。实际上,在其他Objective-C对象所能相应的选择子中,有很多是块也可以响应的。二最重要之处则在于块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量。以便平衡捕获时所执行的保留操作。
`- (void)anInstanceMethod {
void (^someBlock)() = ^ {
_anInstanceVariable = @"someThing";
}
}
如果某个实例在执行anInstanceMethod放法,那么self变量就会指向此实例。由于块里没有明确使用self变量,所以很容易就会忘记self变量其实也为块所捕获了。直接访问实例遍历和通过self来访问时等效的:
self->_anInstanceVariable = @"someThing";
typedef void(^SomeBlock) (void);
@property (nonatomic, copy) BlockName someBlock;
`- (void)anInstanceMethod {
self.someBlock = ^ {
_anInstanceVariable = @"someThing";
}
}
self也是个对象,因而块在捕获它时也会将其保留。如果self所指代的那个对象同时也保留了块,那么这种情况就会导致“保留环”
修改为以下代码即可:
typedef void(^SomeBlock) (void);
@property (nonatomic, copy) BlockName someBlock;
__weak typeof(self)weakSelf = self;
`- (void)anInstanceMethod {
self.someBlock = ^ {
weakSelf.anInstanceVariable = @"someThing";
}
}
块的内部结构
块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class对象的指针,该指针叫做isa.其余内存里含有块对象正常运转所需的各种信息。
![](https://img.haomeiwen.com/i1429750/684450840bee679b.png)
- ivoke :在内存布局中,最重要的就是ivoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void*型的参数,此参数代表块。块其实就是一种代替函数指针的语法结构,原来使用函数指针时,需要用“不透明的void指针”来传递状态。而改用块之后,则可以把原来用标准C语言特性所编写的代码封装成简明且易用的接口。
- descriptor:descriptor变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块的大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比方说,前者要保留捕获的对象,而后者将之释放。
- 块还会把它捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面,捕获了多少变量,就要占据多少内存空间。请注意,拷贝的并不是对象本身,而是只想这些对象的指针变量。invoke函数为何需要把块对象作为参数传进来呢?原因在于,执行块时,要从内存中把这些捕获到的变量读出来。
全局块、栈块及堆
一下代码的写法存在的问题
栈块
void(^block)();
if(/* some condition */) {
# 类似于块的初始化
block = ^{
NSLog(@"Block A");
};
}else {
block = ^{
NSLog(@"Block B");
};
}
block();
定义在if及else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能吧分配给块的内存覆写掉。于是,这两个块只能保证在对应的if或else语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确事儿错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。(我用 button点击多次,并没有发生崩溃,说明问题不是必现)
为解决此问题,可给块对象发送copy消息以拷贝之。这样的话就可以把块从栈复制到堆了。拷贝后的块,可以在定义他的范围外使用,而且,一旦复制到堆上,块就成了带有引用计数的对象了。后续的复制操作都不会真的执行复制,知识递增块对象的引用计数。如果不再使用这个块,那就应将其释放,在ARC环境下会自动释放,而手动管理引用计数时则需要自己来调用release方法。当引用计数降为0后,“分配在堆上的块”会像其他对象一样,为系统回收。而“分配在栈上的块”则无须明确释放,因为栈内存本来就会自动回收,刚才那段代码之所以危险,原因也在于此。
修改后的代码
堆块
void(^block)();
if(/* some condition */) {
# 类似于块的初始化
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
块具有其“固有类型”
每个块都具备其“固有类型”(inherent type),因而可将其赋给适当类型的变量。这个类型由块所接受的参数及返回值组成。例如有下面这个块
^(BOOL flag, int value) {
if(flag) {
return value * 5;
}else {
return value * 10;
}
}
变量类型及其相关赋值语句如下:
int (^variableName)(BOOL flag, int value) = ^(BOOL flag, int value) {
if(flag) {
return value * 5;
}else {
return value * 10;
}
}
映射出块类型的语法结构:
return_type (^blockName)(parameters)
为了方便可以为块类型起一个别名
typedef return_type(^blockName)(parameters)
blackName tempName = ^(paramters) {
// Implementation
}
当块作为一个接受参数时:
原API
- (void)startWithCompletionHandler:(void(^)(NSData *data, NSError *error))completion;
别名之后的API
type void (^EOCCompletionHandler)(NSData data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;
这样写的好处:
- 修改之后,凡是使用了这个类型定义的地方,比如方法签名等处,都会无法编译,而且报的是同一种错误,于是开发者可以逐个修复
- 别名可以见名知意
- 用typedef给同一个块签名类型定义创建数个别名,可以按需修改不同别名。
要点
- 以typedef重新定义块类型,可令块变量用起来更加简单
- 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突
- 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改姓应的typedef中的块签名即可,无须改动其他的typedef
第 39 条 用handler块降低代码分散程度
为用户界面编码是,一种常用的范式就是“异步执行任务”(perform task asynchronously)。这种范式的好处在于:处理用户界面的显示及触摸操作多用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。
如果应用程序在一定时间内无响应,那么就会自动终止。iOS系统上的应用程序就是如此,“系统监控器”在发现某个应用程序的主线程阻塞了一段时间之后,就会令其终止。
关于notification:
调用着可以指定某个块应该安排在那个执行队列里,然而这不是必需的。若没有指定队列,则按默认执行方式执行,也就是说,将由投递通知的那个线程来执行。
`- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotification *))block;
此处传入的NSOperationQueue参数就表示触发通知时用来执行块代码的那个队列。
要点
- 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明
- 设计api时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
第 40 条 用块引用所属对象时,不要出现保留环
要点:
- 如果块所捕获的对象直接或间接的保留了块本身,那么就得当心保留环问题
- 一定要找个适当的时机解除保留环,而不能把责任推给API的调用者
第 41 条 多用派发队列,少用同步锁
同步块
`- (void)synchronizedMethod {
@synchronized(self) {
// Safe
}
}
这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁就释放了。
同步块的缺点:
@synchronized(self) 有些时候会降低代码效率。
NSLock
另一种方式时NSLock
_lock = [[NSLock alloc] init];
`- (void)synchronizedMethod {
[_lock lock];
// safe
[_lock unlock];
}
递归锁
线程能够多次持有该锁,而不会出现死锁
这些锁的缺点
这些锁方法都很好,不过也有缺陷。比方说:在极端情况下同步块会导致死锁,另外效率也不见得很高。
替代方案就是GCD
- (NSString *)someString{
@synchronized(self) {
return _someString;
}
}
- (void)setSomeString:(NSString *)someString {
@synchironized(self){
_someString = someString;
}
}
可以修改为:
// 串行队列
_syncQueue = dispatch_queue_create("com.baidu",NULL);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue,^{
_localSomeString = someString;
});
return _localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_asyc(_syncQueue, ^{
_someString = someString;
});
}
set方法使用一步队列可以提示方法的执行速度,而读取操作和写入操作依然会按顺序执行。
但是测试性能会发现,set的异步方法比同步方法要慢,因为执行异步派发时,需要拷贝块。若拷贝块所用的时间明显超过执行块所用的时间,则这种做法将会变慢。然而若是派发给队列的块执行的任务更为繁重那么这种方案时可以加快运行速度的。
这个主线:多个获取方法可以并发,而获取和设置方法不能并发。还有以下的代码可再次提高速度:
// 并发队列
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue,^{
localSomeString = _someString;
})
return localSomeString;
}
- (void)setSomeStirng:(NSString *)someString{
dispatch_async(_syncQueue,^{
_someSting = someStirng;
})
}
最优方式:
使用栅栏。在队列中栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按照顺序逐个来执行。并发队列如果发现接下来要处理的块时栅栏块(barrier block)那么就一直要等当前所有并发块都执行完才会单独执行栅栏块。
_syncQueue = dispatch_get_globa_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
- (NSString *)someString {
__block NSString *localSomeString;
dispathc_sync(_syncQueue,^{
localString = _someStirng;
})
return _someString;
}
- (void)setSomeStirng:(NSString *)someStirng {
dispatch_barrier_async(_syncQueue,^{
_someStirng = someString
})
}
两个点:
- 无论是串行队列还是并行队列,读取一定是同步的 dispatch_sync
- 栅栏操作用在set方法里
![](https://img.haomeiwen.com/i1429750/8489bcceb5b69c8f.png)
读取操作使用普通块实现的,而写入操作则是用栅栏块实现的。读取操作可以并行,但是写入操作必须单独执行,因为他是栅栏块。
要点
- 派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized块或NSLock对象更简单。
- 将同步与异步派发结合起来,可以实现与普通枷锁机制一样的同步方法,而这么做却不会阻塞执行异步派发的线程。
- 使用同步队列及栅栏块,可以令同步行为更加高效
第 42 条 多用GCD 少用performSelector
NSObject 定义了几个方法,令开发者可以随意调用任何方法。这几个方法可以推迟执行方法调用,也可以指定运行方法所用的线程。这些功能本来都很有用,但是在出现了大中枢派发及块这样的新技术之后,就显得不那么必要了。虽说有些代码还是会经常用到他们,但还是避开为妙。
下面就是这些代码
- (id)performSelector:(SEL)selector;
该方法与选择子等效。所以下面两行代码的执行效果相同。
[object performSelector:@selector(selectorName)];
[object selectName];
因为选择子是在运行期决定的,这就能体现出此方式的强大之处了
SEL selector;
if (/* some condition */) {
selector = @selector(foo);
}else if (/* some other condition */) {
selector = @selector(bar);
}else {
selector = @selector(baz);
}
[object performSelector:selector];
不过这种方式会存在警告:
warning: performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leask]
因为编译器并不知道要调用的选择子是什么,因此也就不了解其方法签名和返回值,甚至都不知道是否有返回值。而且,由于编译器不知道方法名,所以就没法运用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]
如果选用的是两个选择子之一,那么ret对象应由这段代码来释放,而如果是第三个选择子,则无须释放。不仅在arc环境下应该如此,而且在非arc环境下也应该这么做,这样才算严格遵守了方法的命名规范。如果不使用arc(此时编译器也就不发警告信息了)。那么在前两种情况下需要手动释放ret对象,而在后一种情况下不需要释放。这个问题很容易忽视,而且就算用静态分析器,也很难侦测到随后的内存泄漏。performSelector系列的方法之所以要谨慎使用,这就是其中一个原因。
要点
- performSelector系列方法在内存管理方面容易有疏忽。他无法确定将要执行的选择子具体是什么,因而arc编译器也就无法插入适当的内存管理方法。
- performSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受限制
- 如果想把任务放在另一个线程中执行,那么最好不要用performSelector系列方法,而应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。
第 43 条 掌握 GCD及操作队列的使用时机
在执行后台任务时,GCD并不一定是最佳方式。还有一种技术叫做NSOperationQueue,他虽然与GCD不同,但是却与之相关,开发者可以把操作以NSOperation子类的形式放在队列中。而这些操作也能够并发执行。其与GCD派发队列有相似之处,这并非巧合。“操作队列”(operation queue)在GCD之前就有了,其中某些设计原理因操作队列而流行,GCD就是基于这些原理构建的。实际上从iOS4与MacOS X10.6开始,操作队列在底层使用GCD来实现的。
在两者的诸多差别中,首先要注意:GCD时纯C的API,而操作队列则是Objective-C的对象。在GCD中,任务用块来表示。而块时轻量级数据结构。与之相反,“操作”(operation)则是个更为重量级的Objective-C对象。虽说如此,但GCD并不总是最佳方案。有时候采用对象所带来的开销微乎其微,使用完整对象的好处反而大大超过起缺点。
使用NSOperation及NSOperationQueue的好处如下:
- 取消某个操作。可以在NSOperation对象上调用cancel方法,不过已经启动的任务无法取消。GCD队列是无法取消的。GCD时“安排好任务之后就不管了”
- 指定操作间的依赖关系。
- 通过键值观察机制监控NSOperation对象的属性。比如可以通过isFinish 或者isCannelled来判断状态
- 指定操作的优先级。操作的优先级表示此操作与队列中其他操作之间的优先关系。优先级高的操作先执行,优先级低的后执行。操作队列的调度算法(scheduling algorithm)虽“不透明”(opaque),但必然是经过一番深思熟虑才写成的。反之,GCD则没有直接实现此功能的办法。GCD的队列确实有优先级,不过那是针对整个队列来说的,而不是针对每个块来说的。而令开发者在GCD之上自己来编写调度算法又不太合适。因此,在优先级这一点上,操作队列所提供的功能要比GCD更为遍便利。NSOperation也有“线程优先级”(thread priority)。这决定了运行此操作的线程处在何种优先级上。用GCD也可以实现此操作,然而使用操作队列,只需设置一个属性。
- 重用NSOperation对象,系统内置了一些NSOperation的子类,(比如 NSBlockOperation)共开发者使用,要不是这些固有子类的话,那就得自己创建了。这些类就是普通的Objective-C对象,能够存放任何信息。
应该尽可能使用高层的API,只在确有必要时才求助于底层
要点
- 在解决多线程与任务管理问题时,派发队列并非唯一方案
- 操作队列提供了一套高层的Objective-CAPI,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作(操作的优先级),那些操作若改用GCD来实现,则需要另编代码。
第 44 条 通过Dispatch Group机制,根据系统资源状况来执行任务
dispatch_group_t的好处:
- dispatch group 是GCD的一项特权,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这素任务完成后,调用者会得到通知。这个功能有许多作用,其中最重要最值得注意的用法就是把将要并发执行的多个任务合为一个,于是调用者就可以知道这些任务何时才能执行完毕。
- dispatch_group_enter(dispatch_group_t group)、dispatch_group_leave(dispatch_group_t group)前者能够使分组里正要执行的任务递增,而后者则使之递减。成对存在,与引用计数类似
- dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout) 设置等待时间
- dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block) 与wait函数平级。与wait函数不同的是,开发者向此函数传入块,等dispatch_group执行完毕之后,块会在待定的线程上执行。假如当前线程不应阻塞,而开发者又想在那些任务全部完成时得到通知,那么此做法就很有必要了。如果想令数组中的每个对象都执行某项任务,并且想等待所有任务执行完毕,那么就可以使用这个GCD特性来实现,代码如下:
在本例中所有的任务都派发到同一个队列之中,但实际上未必一定要这样做。也可以把某些任务放在优先级高的线程上执行,同时仍然把所有任务都归入同一个dispatch group 并在执行完毕时获得通知,代码如下:dispatch_group_t queue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0); dispatch_group_t dispatchGroup = dispatch_group_create(); for (id object in collection){ dispatch_group_async(dispatchGroup,queue,^{ [object performTask]; }); } dispatch_group_wait(dispatchGroup,DISPATCH_TIME_FPREVER); 若当前线程不应阻塞,则可用notifu函数来取代wait,如下: dispatch_queue_t notifyQueue = dispatch_get_main_queue(); dispatch_group_notify(dispatchGroup,notifyQueue,^{ // 回到主线程继续执行任务。notify回调时所选用的队列根据具体情况来定。笔者在范例中使用了主队列,这是中常见写法。也可以用自定义的串行都列或全局并发队列。 });
dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_PRIORITY_LOW, 0);
dispatch_queue_global_queue heighP
dispatch_queue_t heighPriorityQueue = dispatch_get_global_queue(DISPATCH_PRIORITY_HEIGH, 0);
dispatch_group_t dispatchGroup = dispatch_group_create();
for (id object in lowPriorityObjects) {
dispatch_async(dispatchGroup,lowPriorityQueue,^{
[object performTask];
});
}
for (id object in heighPriorityObjects) {
dispatch_async(dispatchGroup,heighPriorityQueue,^{
[object performTask];
});
}
dispatch_queue_t notifyQueue = dispatch_get_main();
dispatch_gropu_notify(dispatchGroup, notifyQueue,^{
// dispatccGruop任务执行完毕,通知主线程开始处理
})
除了像上面这样把任务提交到并发队列之外,也可以把任务提交至各个串行队列中,并用dispatch group跟踪执行状况。然而,如果所有任务都排在同一个队列里面,那么dispatch grope就用处不大了。因此此时执行任务总要逐个执行,所以只需要在提交完全部任务之后再提交一个块即可,这样做与通过notify函数等待dispatch group执行完毕然后再回调块是等效的。代码如下:
dispatch_queue_t queue = dispatch_queue_create("com.baidu",0);
for (id object in Objects) {
dispatch_async(queue, ^{
[object performTask];
})
}
dispatch_async(queue, ^{
// continue processing after completing tasks
})
上面这点代码表明,开发者未必总需要使用dispatch group。有时候采用带个队列搭配标准的异步派发,也可实现同样效果。
根据希同资源状况来执行任务(并发的理解)
为了执行队列中的块,GCD在适当的时机自动创建新线程或复用旧线程。如果使用并发队列,那么其中有可能会有多个线程,这也就意味着多个块可以并发执行。在并发队列中,执行任务所用的并发线程数量,取决于各种因素,而GCD主要根据系统资源状况来判定这些因素的。假如CPU有多个核心,并且队列中有大量任务等待执行,那么GCD就可能会给该队列配备多个线程。通过dispatch group所提供的这种简便方式,既可以并发执行一系列给定的任务,又能在全部任务结束时得到通知。由于GCD有并发队列机制,所以能够根据可用的系统资源状况来并发执行任务。而开发者则可以专注于业务逻辑代码,无须再为了处理并发任务而编写复杂的调度器。
理解:并发可以使用多个线程
dispatch 可以替换for循环的API
在前面的范例代码中,我们遍历某个collection,并在其每个元素上执行任务,而这也可以用另一个GCD函数来实现.
dispatch_apply(size_t interations, dispatch_queue_t queue, void(^block)(size_t));
dispatch_apply可以使用并发队列也可以使用串行队列
此函数会将块反复执行一定的次数,每次传给块的参数值都会递增,从0开始,直至“interations-1”。其用法如下:
dispatch_queue_t queue = dispatch_queue_create("com.baidu",0);
dispatch_apply(queue,^{
// perform task
});
采用for循环,从0到9递增:
for(i = 0; i < 10; i++) {
// perform task
}
这个执行方式
dispatch_queue_t queue = dispatch_get_global_queue(DOISPATCH_QUQUQ_PRIORITY_DEFAULT,0);
dispatch_apply(array.count, queue, ^(size_t){
id object = array[size_t];
[object performTask];
});
这个例子再次表明,未必总要使用dispatch group 。然而,dispatch_apply会持续阻塞,直到所有任务都执行完毕为止。由此可见:假如把块派给了当前队列(或者体系中高于当前队列的某个串行队列),就会导致死锁。若想在后台执行任务,则应使用dispatch group
要点
- 一系列任务可归入一个dispatch group中。开发者可以在这组任务执行完毕时获得通知。
- 通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码。
第 45 条 使用dispatch_once 来执行只需运行一次的线程安全代码
单例模式
dispatch_once(dispatch_once_t *token, dispatch_block_t block);
此函数接收类型为dispatch_once_t的特殊参数,笔者称其为“标记”,此外还接受块参数。对于给定的标记来说,该函数保证相关的块必定会执行,且仅执行一次。首次执行函数时,必定会执行块中的代码,最重要的一点在于,此操作完全是线程安全的。请注意,对于只需执行一次的块来说,每次调用函数时传入的标记都必须完全相同。因此,开发者通常将标记变量声明在static或global作用域里。
刚才实现单例模式所用的shareInstance方法,可以用此函数来改写:
@property (strong, nonamatic) NSString *string;
+ (id)shareInstance {
static EOCClass *shareInstance = nil;
static dispatch_once oncetoken;
dispatch_once(&onceToken, ^{
shareInstance = [[EOCClass alloc] init];
// 如果有属性的话
shareInstance.string = [[NSString alloc] init]
});
return shareInstance;
}
由于每次调用时都必须使用完全相同的标记,所以标记要声明称static。把该变量定义在static作用域中,可以保证编译器在每次执行shareInstance方法是都会复用这个变量,而不会创建新变量。
dispatch_once 跟高效。他没有使用重量级的同步机制。此函数采用“原子访问”来查询标记,以判断其所对应的代码原来是否已经执行过。
要点
- 经常编写“只需执行一次的线程安全代码”(thread-safe single-code execution)。通过GCD所提供的dispatch_once函数,很容易就能实现此功能。
- 标记应该声明在static或global作用域中,这样的话,在把只需执行一次的块传给dispatch_once函数时,穿进去的标记也是相同的。
第 46 条 不要使用dispatch_get_current_queue
该函数已被弃用
该函数有这种典型的错误用法(antipattern,"反模式"),就是用它检测当前队列是不是某个特定的队列,试图以此来避免执行同步派发时可能遭遇的死锁问题。考虑下面两种存取方法,其代码用队列来保证对实例变量的访问操作是同步的:
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{};
if (dispatch_get_current_queue() == queueA) {
block()
}else {
// 依然死锁
dispatch_sync(queueA,^{
})
}
})
})
![](https://img.haomeiwen.com/i1429750/9cce71f46520c434.png)
由于队列间有层级关系,所以“检查当前队列是否为执行同步派发所用的队列”这种办法,并不总是奏效。
要解决这个问题,最好的办法就是通过GCD所提供的功能来设定“队列特有数据”(queue-specific data),此功能可以把任意数据以键值对的形式关联到队列里。最重要之处在于,假如根据指定的键获取不到关联数据,那么系统就会沿着层级体系向上查找,直至找到数据或到达跟队列为止。下面的例子:
dispatch_queue_t queueA = dispatch_queue_create("com.baidu",0);
dispatch_queue_t queueB = dispatch_queue_create("com.baidu",0);
// 将queueA设置为queueB的目标队列
dispatch_set_target_queue(queueB, queueA);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA,&queueSpecificValue,(void *)queueSpecificValue, (dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{NSlog("No deadlock!")};
CFString retrievedValue = dispatch_get_specific(&kQueueSpecific);
if (retrievedValue) {
block();
}else {
dispatch_sync(queueA, block);
}
})
要点
- dispatch_get_current_queue 函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试只用。
- 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
- dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。
网友评论