[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)
先睹为快
41.多用派发队列,少用同步锁
42.多用gcd,少用performselector系列方法
43.掌握gcd及操作队列的使用时机
44.通过dispatch group机制,根据资源状况来执行任务
45.使用dispatch_once来执行只需要运行一次的线程安全代码
46.不要使用dispatch_get_current_queue
第41条:多用派发队列,少用同步锁
在objective-c中,如果有多个线程要执行同一份代码,那么有时可能会出问题。这种情况下通常要使用锁来实现某种同步机制。在gcd出现之前,有两种方法:
// 使用内置同步块@synchronized - (void)synchronizedmethod{ @synchronized(self){ // safe } } // 使用nslock对象 _lock = [[nslock alloc] init]; - (void)synchronizedmethod{ [_lock lock]; // safe [_lock unlock]; }
这两种方法都很好,但都有缺陷。同步块会降低代码效率,如在本例中,在self对象上加锁,程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码。而直接用锁对象的话,一旦遇到死锁就会非常麻烦。
替代方案就是使用gcd,下面以开发者自己实现的原子属性的存取方法为例:
// 用同步块实现 - (nsstring*)somestring{ @synchronized(self){ return _somestring; } } - (void)setsomestring:(nsstring*)somestring{ @synchronized(self){ _somestring = somestring; } } // 用gcd实现 // 创建一个串行队列 _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; }); }
假如有很多属性都使用了@synchronized(self),那么每个属性的同步块都要等其他所有同步块执行完毕后才能执行,且这样做未必能保证线程安全,如在同一个线程上多次调用getter方法,每次获取到的结果未必相同,在两次访问操作之间,其他线程可能会写入新的属性值。
本例gcd代码采用的是串行同步队列,将读取操作及写入操作都安排在同一个队列中,可保证数据同步。
可以根据实际需求进一步优化代码,例如,让属性的读取操作都可以并发执行,但是写入操作必须单独执行的情景:
// 创建一个并发队列 _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{ // 将写入操作放入异步栅栏块中执行 // 注:barrier表示栅栏,并发队列如果发现接下来需要处理的块为栅栏块,那么就会等待当前并发块都执行完毕后再单独执行栅栏块,执行完栅栏块后再以正常方式继续向下处理。 dispatch_barrier_async(_syncqueue, ^{ _somestring = somestring; }); }
第42条:多用gcd,少用performselector系列方法
objective-c本质上是一门非常动态的语言,nsobject定义了几个方法,令开发者可以随意调用任何方法,这些方法可以推迟执行方法调用,也可以指定运行方法所有的线程。其中最简单的就是performselector:
- (id)performselector:(sel)selector // performselector方法与直接调用选择子等效,所以以下两行代码执行效果相同 [object performselector:@selector(selectorname)]; [object selectorname];
如果选择器是在运行期决定的,那么就能体现出performselector的强大,等于在动态绑定之上再次实现了动态绑定。
sel selector; if(/* some contidion */){ selector = @selector(foo); }else{ selector = @selector(bar); } [object performselector:selector];
但是这样做的话,在arc下编译代码,编译器会发出警告,提示performselector可能会导致内存泄漏。原因在于编译器并不知道将要调用的选择器是什么。所以没办法运用arc的内存管理规则来判定返回的值是不是应该释放。鉴于此,arc采取比较谨慎的操作,即不添加释放操作,在方法返回对象时已将其保留的情况下会发生内存泄漏。
performselector系列方法的另一个局限性在于最多只能接受两个参数,且接受参数类型必须为对象。下面是performselector系列的常用方法:
// 延迟执行 - (id)performselector:(sel)selector withobject:(id)argument afterdelay:(nstimeinterval)delay // 由某线程执行 - (id)performselector:(sel)selector onthread:(nsthread*)thread withobject:(id)argument waituntildone:(bool)wait // 由主线程执行 - (id)performselectoronmainthread:(sel)selector withobject:(id)argument waituntildone:(bool)wait
这些方法都可以用gcd来替代其功能:
// 延迟执行方法 // 使用performselector [self performselector:@selector(dosomething) withobject:nil afterdelay:5.0]; // 使用gcd dispatch_time_t time = dispatch_time(dispatch_time_now, (int64_t)(5.0 * nsec_per_sec)); dispatch_after(time, dispatch_get_main_queue(), ^(void){ [self dosomething]; }); // 在主线程中执行方法 // 使用performselector [self performselectoronmainthread:@selector(dosomething) withobject:nil waituntildone:no]; // 使用gcd 如果waituntildone为yes,则用dispatch_sync dispatch_async(dispatch_get_main_queue(),^{ [self dosomething]; });
第43条:掌握gcd及操作队列的使用时机
gcd技术确实很棒,不过有时候采用标准系统库的,效果会更好。gcd技术的同步机制非常优秀,且对于那些只需执行一次的代码来说,使用gcd最方便。但在执行后台任务时,还可以使用操作队列(nsoperationqueue)。
两者的差别很多,最大的区别在于,gcd是纯c的api,而操作队列是objective-c的对象。gcd中,任务用块来表示,是一个轻量级的数据结构,而操作(nsoperation)则是个更为重量级的objective-c对象。需要更具对象带来的开销和使用完整对象的好处来权衡使用哪种技术。
操作队列的优势:
1. 直接在nsoperation对象上调用cancel方法即可取消操作,而gcd一旦分派任务就无法取消。
2. 可以指定操作间的依赖关系,使特定操作必须在另一个操作执行完毕后方可执行。
3. 可以通过kvo(键值观察)来监控nsoperation对象的属性变化(iscancelled,isfinished等)
4. 可以指定操作的优先级
5. 可以通过重用nsoperation对象来实现更丰富的功能
想要确定哪种方案最佳,最好还是测试一下性能。
第44条:通过dispatch group机制,根据系统资源状况来执行任务
dispatch group是gcd的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。
如果想令某容器中的每个对象都执行某项任务,并且等待所有任务执行完毕,那么就可以使用这个gcd特性来实现:
dispatch_queue_t queue = dispatch_get_global_queue(dispatch_queue_priority_default, 0); // 创建dispatch group dispatch_group_t group = dispatch_group_create(); for (id object in colletion){ // 派发任务 dispatch_group_async(group, queue, ^{ [object performtask]; }); } // 等待组内任务执行完毕 dispatch_group_wait(group, dispatch_time_forever);
假如当前线程不应阻塞,而开发者又想在这组任务全部完成后收到消息,可用notify函数来替代wait
// notify回调时所选用的队列可以根据情况来定,这里用的是主队列 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 完成任务后继续接下来的操作 });
也可以把某些任务放在优先级高的线程上执行,同时所有任务仍然属于一个group
// 创建两个优先级不同的队列 dispatch_queue_t lowpriorityqueue = dispatch_get_global_queue(dispatch_queue_priority_low, 0); dispatch_queue_t highpriorityqueue = dispatch_get_global_queue(dispatch_queue_priority_high, 0); // 创建dispatch group dispatch_group_t group = dispatch_group_create(); for (id object in lowprioritycolletion){ dispatch_group_async(group, lowpriorityqueue, ^{ [object performtask]; }); } for (id object in highprioritycolletion){ dispatch_group_async(group, highpriorityqueue, ^{ [object performtask]; }); } dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 完成任务后继续接下来的操作 });
而dispatch group也不是必须使用,编译某个容器,并在其每个元素上执行任务,可以用apply函数,下面以数组为例:
dispatch_queue_t queue = dispatch_get_global_queue(dispatch_queue_priority_default, 0); dispatch_apply(array.count, queue, ^(size_t i) { id object = array[i]; [object performtask]; });
但是使用apply函数会持续阻塞,直到所有任务都执行完毕为止。由此可见,假如把块派给了当前队列(或者体系中高于当前队列的串行队列),就将导致死锁。若想在后台执行任务,则应使用dispatch group。
第45条:使用dispatch_once来执行只需要运行一次的线程安全代码
单例模式的常见实现方式为,在类中编写名为sharedinstance的方法,该方法只会返回全类共用的单例实例,而不会每次调用时都创建新的实例。下面是用同步块来实现单例模式:
@implementation eocclass + (id)sharedinstance{ static eocclass *sharedinstance = nil; @synchronized(self){ if(!sharedinstance){ sharedinstance = [[self alloc] init]; } } return sharedinstance; } @end
gcd引入了一项特性,使单例实现起来更为容易
@implementation eocclass + (id)sharedinstance{ static eocclass *sharedinstance = nil; // 每次调用都必须使用完全相同的标志,需要将标志声明为static static dispatch_once_t oncetoken; // 只会进行一次 dispatch_once(&oncetoken, ^{ sharedinstance = [[self alloc] init]; }); return sharedinstance; } @end
dispatch_once可以简化代码并彻底保证线程安全,所有问题都由gcd在底层处理。而且dispatch_once更高效,它没有使用重量级的同步机制。
第46条:不要使用dispatch_get_current_queue
使用gcd时,经常需要判断当前代码正在哪个队列上执行。dispatch_get_current_queue函数返回的就是当前正在执行代码的队列。不过ios与mac os x都已经将它废除了,要避免使用它。
同步队列操作有可能会发生死锁:
_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; }); }
假如调用getter方法的队列恰好是同步操作所针对的队列(_syncqueue),那么dispatch_sync就会一直不返回,直到块执行完毕。可是应该执行块的那个目标队列却是当前队列,它又一直阻塞,等待目标队列将块执行完毕。这样一来就出现死锁。
这时候或许可以用dispatch_get_current_queue来解决(在这种情况下,更好的做法是确保同步操作所用的队列绝不会访问属性)
- (nsstring*)somestring{ __block nsstring *localsomestring; dispatch_block_t block = ^{ localsomestring = _somestring; }; // 执行块的队列如果是_syncqueue,则不派发直接执行块 if (dispatch_get_current_queue() == _syncqueue) { block(); } else{ dispatch_sync(_syncqueue, block); } return localsomestring; }
但是这种做法只能处理一些简单的情况,如果遇到下面情况,仍有死锁风险:
dispatch_queue_t queuea = dispatch_queue_create("com.effectiveobjectivec.queuea", null); dispatch_queue_t queueb = dispatch_queue_create("com.effectiveobjectivec.queueb", null); dispatch_sync(queuea, ^{ dispatch_sync(queueb, ^{ dispatch_sync(queuea, ^{ // 死锁 }); }); });
因为这个操作是针对queuea的,所以必须等最外层的dispatch_sync执行完毕才行,而最外层的dispatch_sync又不可能执行完毕,因为它要等到最内层的dispatch_sync执行完毕,于是就死锁了。
如果尝试用dispatch_get_current_queue来解决:
dispatch_sync(queuea, ^{ dispatch_sync(queueb, ^{ dispatch_block_t block = ^{ /* ... */ }; if (dispatch_get_current_queue() == queuea) { block(); } else{ dispatch_sync(queuea, block); } }); });
这样做仍然会死锁,因为dispatch_get_current_queue()返回的是当前队列,即queueb,这样的话仍然会执行针对queuea的同步派发操作,于是同样会死锁。
由于派发队列是按层级来组织的,这意味着排在某条队列中的块会在其上级队列里执行。队列间的层级关系会导致检查当前队列是否为执行同步派发所用的队列这种方法并不总是奏效。
要解决这个问题,最好的办法是通过gcd提供的功能来设定队列特有数据。此功能可以把任意数据以键值对的形式关联到队列里。最重要的是,假如获取不到关联数据,那么系统会沿着层级体系向上查找,直至找到数据或到达根队列为止。
dispatch_queue_t queuea = dispatch_queue_create("com.effectiveobjectivec.queuea", null); dispatch_queue_t queueb = dispatch_queue_create("com.effectiveobjectivec.queueb", null); static int kqueuespecific; cfstringref queuespecificvalue = cfstr("queuea"); // 在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); } });
dispatch_queue_set_specific函数首个参数为待设置队列,其后两个参数是键和值(函数按指针值来比较键,而不是其内容)。最后一个参数是析构函数。dispatch_function_t类型定义如下:
typedef void (*dispatch_function_t)(void*)
本例传入cfrelease做参数,用以清理旧值。
队列特定数据所提供的这套简单易用的机制,避免了使用dispatch_get_current_queue时经常遇到的陷阱。但是,可以在调试程序时使用dispatch_get_current_queue这个已经废弃的方法,只是别把它编译到发行版程序里就行。