[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)

[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)

参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway

先睹为快

41.多用派发队列,少用同步锁

42.多用GCD,少用performSelector系列方法

43.掌握GCD及操作队列的使用时机

44.通过Dispatch Group机制,根据系统资源状况来执行任务

45.使用dispatch_once来执行只需要运行一次的线程安全代码

46.不要使用dispatch_get_current_queue

目录

  • 编写高质量iOS代码的52个有效方法十Grand Central DispatchGCD

    • 先睹为快
    • 目录
    • 第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这个已经废弃的方法,只是别把它编译到发行版程序里就行。

时间: 2024-10-26 02:51:00

[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)的相关文章

[编写高质量iOS代码的52个有效方法](三)消息和运行期

[编写高质量iOS代码的52个有效方法](三)消息和运行期 参考书籍:<Effective Objective-C 2.0> [英] Matt Galloway 先睹为快 11.理解objc_msgSend的作用 12.理解消息转发机制 13.用"方法调配技术"调试"黑盒方法" 14.理解"类对象"的用意 目录 编写高质量iOS代码的52个有效方法三消息和运行期 先睹为快 目录 第11条理解objc_msgSend的作用 第12条理解

[编写高质量iOS代码的52个有效方法](十一)系统框架

[编写高质量iOS代码的52个有效方法](十一)系统框架 参考书籍:<Effective Objective-C 2.0> [英] Matt Galloway 先睹为快 47.熟悉系统框架 48.多用块枚举,少用for循环 49.对自定义其内存管理语义的容器使用无缝桥接 50.构建缓存时选用NSCache而非NSDictionary 51.精简initialize与load的实现代码 52.别忘了NSTimer会保留其目标对象 目录 编写高质量iOS代码的52个有效方法十一系统框架 先睹为快

[编写高质量iOS代码的52个有效方法](一)Objective-C基础

[编写高质量iOS代码的52个有效方法](一)Objective-C基础 参考书籍:<Effective Objective-C 2.0> [英] Matt Galloway 先睹为快 1.了解Objective-C语言的起源 2.在类的头文件中尽量少引入其他头文件 3.多用字面量语法,少用与之等价的方法 4.多用类型常量,少用#define预处理器指令 5.用枚举表示状态.选项.状态码 目录 编写高质量iOS代码的52个有效方法一Objective-C基础 先睹为快 目录 第1条了解Obje

【iOS】编写高质量iOS代码的52个有效方法

<编写高质量iOS与OS X代码的52个有效方法>的笔记,通读了一遍,感觉印象不是特别深刻,写下笔记记录下吧. 这个数的结构很清晰,5章52条建议,分点介绍了编写高效代码的建议,每点后面有总结,这里简单的记录下这些总结,方便温习. 因时间原因,先写5条,有时间逐渐加进来. 第一章:熟悉oc 1,了解oc起源 oc为C语言添加了面向对象特性,是其超集.oc使用动态绑定的消息结构,也就是说已在运行时才会检查对象类型.接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定. 理解C语言的

编写高质量JavaScript代码的68个有效方法

简介: <Effective JavaScript:编写高质量JavaScript代码的68个有效方法>共分为7章,分别涵盖JavaScript的不同主题.第1章主要讲述最基本的主题,如版本.类型转换要点.运算符注意事项和分号局限等.第2章主要讲解变量作用域,介绍此方面的一些基本概念,以及一些最佳实践经验.第3章主要讲解函数的使用,深刻解析函数.方法和类,并教会读者在不同的环境下高效使用函数.第4章主要讲解原型和对象,分析JavaScript的继承机制以及原型和对象使用的最佳实践和原则.第5章

Effective Python之编写高质量Python代码的59个有效方法

                                                     这个周末断断续续的阅读完了<Effective Python之编写高质量Python代码的59个有效方法>,感觉还不错,具有很大的指导价值.下面将以最简单的方式记录这59条建议,并在大部分建议后面加上了说明和示例,文章篇幅大,请您提前备好瓜子和啤酒! 1. 用Pythonic方式思考 第一条:确认自己使用的Python版本 (1)有两个版本的python处于活跃状态,python2和pyt

编写高质量Python代码的59个有效方法

作者Brett Slatkin是 Google公司高级软件工程师.他是Google消费者调查项目的工程主管及联合创始人,曾从事Google App Engine的Python基础架构工作,并利用Python来管理众多的Google服务器.Slatkin也是PubSubHubbub协议的联合创始人,还用Python为Google实现了针对该协议的系统.他拥有哥伦比亚大学计算机工程专业学士学位. 精彩书评 "Slatkin所写的这本书,其每个条目(item)都是一项独立的教程,并包含它自己的源代码.

JavaScript手札:《编写高质量JS代码的68个有效方法》(一)(1~5)

编写高质量JS代码的68个有效方法(一) *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* BLOCKS =============================================================================*/ p, blockquote, ul, ol, dl, table, pre { marg

编写高质量JS代码的68个有效方法(三)

[20141030]编写高质量JS代码的68个有效方法(三) *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* BLOCKS =============================================================================*/ p, blockquote, ul, ol, dl, table,