多用块枚举,与快速遍历少用for循环

我们在编程时,经常会需要遍历某个collection,例如NSArrary,NSDictionary,NSSet等。

我们经常使用for循坏来遍历,这样对于数组来说还好,但是根据定义,字典与set都是“无序的"(imoniered)所以无法根据特定的整数下标来直接访问其中的值。于是,就需要先获取字典里的所有键或是set里的所有对象,这两种情况下,都可以在获取到的有序数组上遍历,以便借此访问原字典及原set中的值。

但是创建这个附加数组会有额外开销,而且还会多创建一个数组对象,它会保留collection中的所有元素对象。 当然了,释放数组时这些附加对象也要释放,可是要调用本来不需执行的方法。其他各种遍 历方式都无须创建这种中介数组。

例如:

// Dictionary
NSDictionary *aDictionary = /* .... */;
NSArray *keys = [aDictionary allKeys]; 
for (int i = 0; i < keys.count; i++) { 
    id key = keys[i]; 
    id value = aDictionary[key];
    //Do something with 'key' and 'value'
}

//Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allObjects]; 
for (int i = 0; i < objects.count; i++)
{ 
    id object = objects [i];
    //Do something with 'object'
} 

上述代码实现遍历比较麻烦,所以我们推荐使用快速遍历与块循环。

快速遍历

快速遍历是OC 2.0所引入的一个新功能。它语法更简洁,它为for循环开设了in关键字。这个关键字大幅简化了遍历collection所需的语法,比方说要遍历数组,就可以这么写:

NSArray *anArray = /* ••• */; 
for (id object in anArray) {
    //Do something with 'object'
}

这样写简单多了。如果某个类的对象支持快速遍历,那么就可以宣称自己遵从名为 NSFastEmimeraticm的协议,从而令开发者可以采用此语法来迭代该对象。

遍历字典与set为:

// Dictionary
NSDictionary *aDictionary = /* ... */; 
for (id key in aDictionary) {
    id value = aDictionary[key];
    //Do something with 'key' and 'value'
}

//Set
NSSet *aSet = /* ... */; 
for (id object in aSet) {
    //Do something with 'object'
}

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

NSArray *anArray = /* ... */;
for (id object in [anArray reverseObjectEnumerator]){
    //Do something with 'object'
}

在目前所介绍的遍历方式中,这种办法是语法最简单且效率最髙的,然而如果在遍历字典时需要同时获取键与值,那么会多出来一步。而且,与传统的for循环不同,这种遍历方式无法轻松获取当前遍历操作所针对的下标。遍历时通常会用到这个下标,比如很多算法都需要它。

基于块的遍历

在当前的Objective-C语言中,最新引人的一种做法就是基于块来遍历。NSArray中定义了下面这个方法,它可以实现最基本的遍历功能:

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

此之外,还有一系列类似的遍历方法,它们可以接受各种选项,以控制遍历操作,稍后将会讨论那些方法。

在遍历数组及set时,每次迭代都要执行由block参数所传人的块,在遍历数组及set时,每次迭代都要执行由block参数所传人的块,这个块有三个参数, 分别是当前迭代所针对的对象、所针对的下标,以及指向布尔值的指针。前两个参数的含义不言而喻。而通过第三个参数所提供的机制,开发者可以终止遍历操作。

例如,下面这段代码用此方法来遍历数组:

NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:
        ^(id object, NSUInteger id, BOOL *stop){
    //Do something with 'object' 
    if (shouldStop) {
        *stop = YES;
}

这种写法稍微多了几行代码,不过依然明晰,而且遍历时既能获取对象,也能知道其下标。此方法还提供了一种优雅的机制,用于终止遍历操作,开发者可以通过设定stop变最值来实现,当然,使用for等遍历方式时,也可以通过break来终止循环,那样做也很好。

此方式不仅可用来遍历数组。NSSet里面也有同样的块枚举方法,NSDictionary也是这样, 只是略有不同:

-(void)enumerateKeysAndObjectsUsingBlock:
        (void(^)(id key, id object, BOOL *stop))block

因此,遍历字典与set也同样简单:

// Dictionary
NSDictionary *aDictionary = /* ... */;
[aDietionary enumerateKeysAndObjectsUsingBlock: 
        ^(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)时也一样。而在遍历字典时,无须额外编码,即可同时获取键与值,因而省去了根据给定键来获取对应值这一步。用这种方式遍历字典,可以同时得知键与值,这很可能比其他方式快很多,因为在字典内部的数据结构中,键与值本来就是存储在一起的。

另一个好处就是:能够修改块的方法签名,以免进行类型转换操作,从效果上讲,相当于把本来需要执行的类型转换操作交给块方法签名来做。

比方说,要用“快速遍历法”来遍 历字典。若已知字典中的对象必为字符串,则可以这样编码:

for (NSString *key in aDictionary) {
    NSString *object = (NSString*)aDictionary[key];
    //Do something with 1 key1 and 1 object1
)

如果改用基于块的方式来遍历,那么就可以在块方法签名中直接转换:

NSDictionary ^aDictionary = /* ••• */;
[aDictionary enumerateKeysAndObjectsUsingBlock:
        ^(NSString *key, NSString *obj, BOOL *stop){
         //Do something with 'key' and 'obj'
}];

之所以能如此,是因为id类型相当特殊,它可以像本例这样,为其他类型所覆写。要是原来的块签名把键与值都定义成NSObject*,那这么写就不行了。此技巧初看不甚显眼,实 则相当有用。指定对象的精确类型之后,编译器就可以检测出开发者是否调用了该对象所不 具备的方法,并在发现这种问题时报错。如果能够确知某collection里的对象是什么类型, 那就应该使用这种方法指明其类型。

用此方式也可以执行反向遍历。数组、字典、set都实现了前述方法的另一个版本,使开发者可向其传入“选项掩码”(option mask):

-(void)enumerateObjectsWithOptions:
            (NSEnumerationOptions)options 
            usingBlock: 
            (void(^)(id obj, NSUInteger id, BOOL *stop))block

-(void)enumerateKeysAndObjectsWithOptions:
            (NSEnumerationOptions)options
            usingBlock:
            (void(^)(id key, id obj, BOOL *stop))block

NSEnumerationOptions类型是个enum,其各种取值可用“按位或”(bitwiseOR)连接,用以表明遍历方式。具体选项不再过多介绍。

总体来看,块枚举法拥有其他遍历方式都具备的优势,而且还能带来更多好处。与快速遍历法相比,它要多用一些代码,可是却能提供遍历时所针对的下标,在遍历字典时也能同时提供键与值,而且还有选项可以开启并发迭代功能,所以多写这点代码还是值得的。

要点

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

第三十四条 : 以“自动释放池降低内存峰值”

Objective-C的引用计数架构中,有一项特性叫做“自动释放池”(autoreleasepool)。释放对象有两种方式:一种是 调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加人“自动 释放池”中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会向其中的对象发送release消息。

创建自动释放池的语句为:

@autoreleasepool{
    // ....
}

我们来看下面这个例子:

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

上面这个例子展示了基本的用法,自动释放池于左花括号处创建, 并于对应的右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到release消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。

autoreleasepool的常见用法为降低程序的内存峰值,比如下面这个代码:

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

上述代码,因为for语句会不断的创建person对象,造成应用程序内存不断上涨,在执行完for语句后又会将所有临时对象都释放,造成内存的突然上涨与下降。这些临时对象本可以提前回收的,所以我们应该这么写:

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

加上这个自动释放池之后,应用程序在执行循环时的内存峰值就会降低,不再像原来那么高了。内存峰值(high-memory waterline)是指应用程序在某个特定时段内的最大内存用量 (highest memory footprint)。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。

自动释放池机制就像“栈”(stack) 一样。系统创建好自动释放池之后,就将其推入栈中, 而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。

现在我们创建一个iOS程序之后,系统会默认有一个main.m文件其中有代码:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

我们用自动释放池来包裹应用程序的主入口点(main application entry point),除了上述主动添加了一个释放池,我们一般不需要主动添加,系统创建的线程一般默认都有自动释放池。

@autordeaSepool语法还有个好处:每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已为系统所回收的对象。例如:

@autoreleasepool {
    id object = [self createObject];
}
[self useObject:object];

上述代码无法编译,因为object变量出了自动释放池块的外围后就不可用了(已经被释放),所以在调用“useObject:”方法时不能用它做参数。

要点

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

第四十五条:使用dispatch_once来执行只需运行一次的代码

单例模式顾名思义,就是一个类一般只会同时存在一个实例,常见的实现方式为在类中编写名为sharedInstance的类方法。例如:

+(id)sharedImstance{
    static EOCClass *sharedlnstance = nil;
        @synchronized(self){
            if (!sharedlnstance) {
                sharedlnstance = [[self alloc] init];
            }
    }
    return sharedlnstance;
}

该方法只会返回全类共用的单例实例,而不会在每次调用时都创建新的实例。

为保证线程安全,上述代码将创建单例实例的代码包裹在同步块里。

GCD中的一个函数可以更简单的执行这种只需执行一次的代码。为:

void dispatch_once (dispatch_once_t *token,
                    dispatch_block_t block);

此函数接受类型为的dispatch_once_t的特殊参数token,此外还接受块参数,在块里面执行我们所需要运行一次的代码。对于给定的token来说,该函数保证相关的块必定会执行,且仅执行一次。首次调用该函数时,必然会执行块中的代码,最重要的一点在于,此操作完全是线程安全的。请注意,对于只需执行一次的块来说,每次凋用函数时传入的标记都必须完全相同。因此,开发 者通常将标记变量声明在staticglobal作用域里。

将上面的代码改为dispatch_once来执行,就可以换为:

+ (id)sharedInstance {
    static EOCClass ^sharedInstance = nil; 
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedlnstance = [[self alloc] initJ;
    });
    return sharedInstance;
}

使用dispatch_once可以简化代码并且彻底保证线程安全,开发者根本无须担心加锁或同步。所有问题都由GCD在底层处理。

相比于同步机制的繁琐,dispatch_once更高效,此函数采用“原子访问”(atomic access)来査询token,以判断其所对应的代码原来是否已经执行过。速度一般是同步机制的两倍。

要点

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

第四十三条 掌握GCD和操作队列(NSOperation)的使用时机

虽然GCD很棒,但是我们有时候也应该选用正确的工具来使用。所以我们这里来介绍下NSOperation

很少有其他技术能与GCD的同步机制相媲美。对于那些只需执行一次的代码来说,使用GCDdispatch_once最为方便。

但是如果执行后台任务,NSOperation也许更适合。NSOperation是在GCD出现之前就有的一种技术。甚至现在操作队列在底层是用GCD来实现的。

在两者的诸多差别中,首先要注意:GCD是纯C的API,而操作队列则是Objective-C的对象。GCD中,任务用块来表示,而块是个轻量级数据结构(参见第37条)。与之相 反,“操作"(operation)则是个更为重量级的Objective-C对象。虽说如此,但GCD并不总是最佳方案。有时候采用对象所带来的开销微乎其微,使用完整对象所带来的好处反而大大超过其缺点。

所以我们来介绍下它的好处:

  • 取消某个操作。如果使用操作队列,那么想要取消操作是很容易的。运行任务之前, 可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表明此任务不需执行,不过,已经启动的任务无法取消。若是不使用操作队列,而是把块安排到GCD队列,那就无法取消了。那套架构是“安排好任务之后就不管了"(fire and forget)。

  • 指定操作间的依赖关系。一个操作可以依赖其他多个操作。开发者能够指定操作之间的依赖体系,使特定的操作必须在另外一个操作顺利执行完毕后方可执行。比方说,从服务器端下载并处理文件的动作,可以用操作来表示,而在处理其他文件之前,必须先下载“清单文件”(manifest file)。后续的下载操作,都要依赖于先下载清单文件这一操作。如果操作队列允许并发的话,那么后续的多个下载操作就可以同时执行,但前提是它们所依赖的那个清单文件下载操作已经执行完毕。

  • 通过键值观测机制监控NSOperation对象的属性。NSOperation对象有许多属性都适合通过键值观测机制(简称KVO)来监听,比如可以通过isCancelled属性来判断任务是否已取消,又比如可以通过isFinished属性来判断任务是否已完成。如果想在某个任务变更其状态时得到通知,或是想用比GCD更为精细的方式来控制所要执行的 任务,那么键值观测机制会很有用。

  • 指定操作的优先级。操作的优先级表示此操作与队列中其他操作之间的优先关系。优先级高的操作先执行,优先级低的后执行。操作队列的调度算法(scheduling algorithm)“不透明"(opaque),但必然是经过一番深思熟虑才写成的。反之,GCD则没有直接实现此功能的办法。GCD的队列确实有优先级,不过那是针对整个队列来说的,而不是针对每个块来说的。而令开发者在GCD之上自己来编写调度算法,又不太合适。因此,在优先级这一点上,操作队列所提供的功能要比GCD更为便利。

NSOperation对象也有“线程优先级”(thread priority),这决定了运行此操作的线程处在 何种优先级上。用GCD也可以实现此功能,然而采用操作队列更简单,只需设置一个属性。

  • 重用 NSOperation对象。系统内置了一些NSOperation的子类(比如NSBlockOperation)供开发者调用,要是不想用这些固有子类的话,那就得自己来创建了。这些类就是普通的Objective-C对象,能够存放任何信息。对象在执行时可以充分利用存于其中的信息,而且还可以随意调用定义在类中的方法。这就比派发队列中那些简单的块要强大许多。这些NSOperation类可以在代码中多次使用,它们符合软件开发中的“不重复”原则。

正如大家所见,操作队列有很多地方胜过派发队列。操作队列提供了多种执行任务的方式,而且都是写好了的,直接就能使用。

所以我们说,某些功能确实可以用高层的Objective-C方法来做,但这并不等于说它就一定比底层实现方案好。要想确定哪种方案更佳,最好还是测试一下性能。

要点

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

第四十二条 多用GCD,少用performSelector系列方法

Objective-C本质上是一门非常动态的语言(参见第11条),NSObject定义了几个方法, 令开发者可以随意调用任何方法。这几个方法可以推迟执行方法调用,也可以指定运行方法所用的线程。这些功能原来很有用,但是在出现了GCD之后,这些功能就尽量不要使用了,尽量用GCD来取代他们。

其中他们又如下几个方法:

-(id)performSelector : (SEL) selector
//可以传两个参数
-(id)performSelector : (SEL) selector
           withObject:(id)objectA
           withObject:(id)objectB
//传一个参数
-(id)performSelector : (SEL) selector
           withObject:(id)object

具体用法如:

[object performSelector: @selector(selectorName)];

[object performSelector: @selector(setValue:)
             withObject: newValue];

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

另一个局限性是:返回值只能是void或对象类型。尽管所要执行的 选择子也可以返回void,但是performSelector方法的返回值类型毕竟是id。如果想返回整数或浮点数等类型的值,那么就需要执行一些复杂的转换操作了,而这种转换很容易出错。

第三个局限是:这个方法最多只能传递两个参数,当选择子得到参数超过两个时,只能运用字典来传送数据(但是容易增加开销和造成bug)。

所以我们的解决方法是:我们使用来代替,并且performSelector系列方法都可以使用GCD机制使用块来实现。我们来举几个例子说明:

如果我们想要延后执行某个任务:

//Using performSelector:withObjectrafterDelay: 
[self performSelector:@selector(doSomething)
           withObject:nil 
           afterDelay:5.0];

//Using dispatch_after
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];
});

后者想要在主线程执行某个任务:

// Using performSelectorOnMainThread: withObject .-waitUntilDone :
[self performSelectorOnMainThread:@selector(doSomething)
                      withObject:nil 
                      waitUntilDone:NO];
//Using dispatch_async
// (or if waitUntilDone is YES, then dispatchasync) 
dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomething];
});

要点

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

第四十一条 多用派发队列,少用同步锁

OC中,如果有多个线程要执行同一份代码,那么有时可能会出问题。这种情况下,通常要使用锁来实现某种同步机制。在GCD出现之前,有两种办法,第一种是采用内置的“同步块”(synchronization block):

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

这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁就释放了。在本例中,同步行为所针对的对象是self。这么写通常没错,因为它可以保证每个对象实例都能不受干扰地运行其synchronizedMethod方法。然而,滥用 @SynChronized(self)则会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行。若是在self对象上频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码,这样做其实并没有必要。

另一种加锁的办法是NSLock对象:

_lock = [[NSLockalloc] init];

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

也可以使用NSRecursiveLock这种“递归锁’(recursive lock),线程能够多次持有该锁,而不会出现死锁(deadlock)现象。

虽然上面两种方法可以用,但是也有一些缺陷,比如:在某些情况下,同步快会导致死锁,另外,效率不是很高。而如果直接使用锁对象的话,一旦遇到死锁,就会非常麻烦。

所以我们一般使用GCD来替代,它的优点就是可以简单,高效的为代码加锁。属性因为需要经常性的同步,所以当要线程安全的时候,加上“atomic”特质来修饰属性。

但是如果我们想要自己实现属性访问方法时,可以:

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

但是滥用@synchronized(self)会很危险,因为所有同步块都会彼此抢夺同一个锁。要是有很多个属性都这么写的话,那么每个属性的同步块都要等其他所有同步块执行完毕才能执行,这也许并不是开发者想要的效果。我们只是想令每个属性各自独立地同步。

而且上述做法并不是绝对的线程安全。因为在两次访问操作之间,其他线程可能会写入新的属性值。

这里我们使用“串行同步队列”(serial synchronization queue)。将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。:

_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的串行队列/并发队列与iOS多线程这里不详细讲解了,后续深入探讨)。

多个获取方法可以并发执行,而获取方法与设置方法之间不能并发执行,利用这个特点,还能写出更快一些的代码来。此时正可以体现出GCD写法的好处。用同步块或锁对象,是无法轻易实现出下面这种方案的。这次不用串行队列,而改用并发队列(concurrentquene):

_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 = someScring;
    });
}

光是上面这些代码还不够,所有读取操作与写入操作都会在同一个队列上执行,不过由于是并发队列,所以读取与写人操作可以随时执行。而我们恰恰不想让这些操作随意执行。这就要用到栅栏(barrier),是GCD中的一个功能:

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

在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个来执行的。并发队列如果发现接下来要处理的块是个栅栏块 (barrier 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, ^{
        _someString = someScring;
    });
}

执行的顺序如图所示:

测试一下性能,你就会发现,这种做法肯定比使用串行队列要快。注意,设置函数也可以改用同步的栅栏块(synchronous barrier)来实现,那样做可能会更髙效,其原因刚才已经 解释过了。最好还是测一测每种做法的性能,然后从中选出最适合当前场景的方案

要点

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