【Effective Objective-C 2.0读书笔记】第六章:块(Blocks)和大中枢派发(GCD)

来源:互联网 发布:微信机器人 java 编辑:程序博客网 时间:2024/04/30 15:38

继续这本书的读书笔记,希望在其中也加入自己的一些总结,以加深理解。之前这一章写了很多了,保存到草稿箱中,不知道为何丢失了,真是可惜,看来CSDN的MarkDown编辑器还存在一些bugs,在它打上补丁之前还是写一点发表一下吧。Let’s begin.

多线程编程是每个开发者在开发现代应用程序的时候经常碰到的问题。系统框架经常在你意想不到的时候在UI线程之外使用额外的线程来处理各类工作。没什么比由于UI线程阻塞而导致应用程序挂起更糟糕的了。这种现象表现在Mac OS X上,就会出现一个一直旋转不停的彩球;表现在iOS上,就会因为阻塞太久而导致应用程序被终止。

幸运地是,Apple公司以一种全新的方式来思考多线程问题。现代编程中关键的技术是block和Grand Central Dispatch(GCD),尽管技术迥异。block为C, C++, and Objective-C等语言提供了词法闭包(lexical closures),而且非常有用,主要因为它提供了在不同的上下文环境下就像对待一个对象一样地来传递代码的机制。而且至关重要的是,block可以使用定义它的范围内的所有变量。

GCD基于所谓的派发队列(dispatch queues),为多线程提供了一个抽象机制。block被放置到这些队列里,GCD会为你处理所有的执行计划。GCD可以创建,重用和销毁背景线程,在它认为合适的时候或者基于系统资源状况来处理每个队列。此外,GCD为常规的编程任务提供了易用的解决方案,譬如线程安全和基于目前的系统资源状况来并行处理任务。

第37条:理解“块”(blocks)这一概念

block提供了闭包功能,这一语言特性作为一个扩展被添加到GCC编译器中,存在于所有现代Clang版本中(这个编译器工程被Mac OS X和iOS开发所使用)。block所需的运行时组件在Mac OS X 10.4和iOS 4.0和之后的所有版本中都可用.由于这个语言特性属于C语言级别的特性,因此在C,C++,Objective-C和Objective-C++代码中都可用,这些代码在一个支持该特性的编译器下编译,并运行在block运行时中。

block基础知识

block和函数相似,只不过是定义在另一个函数里面,和定义它的函数共享同一个范围内的东西。block用“^”符号来表示,后面跟着一对花括号,花括号里面是实现代码。

^{    // Block implementation here}

block是一个数据类型,可以被赋值。与int,float或其他Objective-C对象一样,一个block也可以被赋值到一个变量,与其他类型的变量一样被使用。block类型的语义类似于函数指针。下面是一个无输入参数和返回参数的block例子:

void (^someBlock)() = ^{    // Block implementation here};

下面是block的原型:

NSString * ( ^ myBlock )( int );

上面的代码是声明了一个block(^)原型,名字就叫做myBlock,携带一个int参数,返回只为NSString类型的指针。

下面来看看block的定义:

myBlock = ^( int number ){    return [ NSString stringWithFormat: @"Passed number: %i", number ];};

如上所示,将一个函数体赋值给了myBlock变量,其接收一个名为number的参数。该函数返回一个NSString对象。

如果不把block赋值给变量的话,可以忽略掉block原型的声明,例如直接将block当做实参进行传递。如下所示:

someFunction( ^ NSString * ( void ) { return @"hello, world" } );

注意,上面这种情况必须声明返回值的类型——这里是返回NSString对象。

将block当做形参来传递

由于Objective-C是强制类型语言,所以作为函数参数的block也必须要指定返回值的类型,以及相关参数类型(如果需要的话)。

- ( void )logBlock: ( NSString * ( ^ )( int ) )theBlock;

block的强大之处在于:在声明它的范围内,所有变量都可以为其所捕获(capture)。也就是说,在此范围内的所有变量,在block里依然可用。默认情况下,block所捕获的任何变量在block里都不能修改,否则编译器会报错,除非将这样想要被修改的变量声明为__block修饰符。下面是一个block用来统计数组中小于2的数量的例子:

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++;    }}];// count = 2

上面这段代码也展示了内联块(inline block)的用法。在Objective-C引入block之前,想要实现这样的功能只能通过传入函数指针和选择子(selector)的名称,但是这样做需要传入传出状态,经常通过一个“不透明的void指针“(an opaque void pointer)”来实现,从而导致添加额外的代码,也会令方法变得松散。而声明一个内联块,可把所有业务逻辑都放在一处。

当block捕获到一个对象类型的变量时,它会隐式地保留(retain)该对象变量。系统在释放(release)这个block的时候,也会将该对象一并释放。block本身可看做一个对象,也与其他对象一样有引用计数。当最后一个指向block的引用被移除之后,block会被回收,回收时也会释放它所捕获的对象变量,以平衡捕获时所执行的保留操作。

如果block被定义在一个类的实例方法中,则self变量跟该类的所有实例变量一样都可以被block访问。需要特别注意的的是,这些实例变量都可以被block修改,而无需用__block修饰符来声明。但是,如果一个实例变量被block捕获,无论是读还是写,self变量都会被隐式地捕获,因为实例变量跟self所指代的实例关联在一起。这样就会引起self变量被block所retain。这种情况下,如果block本身再被self所指代的实例retain的话,就会经常导致“保留环”(retain cycles)。

block的内部结构

一个block是一个对象,因为定义它的内存区域中的第一个变量就是一个指向一个Class对象指针,就是所谓的isa指针。这个内存区域的剩余部分包含维持block正常运行的各种信息。一个block对象的内存布局如下:

表一:

Block Layout void * isa int flags int reserved void (*)(void *, …) invoke struct * descriptor Captured variables

表二:

Block Descriptor(上表中的descriptor结构体) unsigned long int reserved unsigned long int size void (*)(void *, void *) copy void (*)(void *, void *) dispose

布局中最重要的变量是invoke,它是指向block的实现代码的函数指针,这里的函数原型至少传入一个void *参数,代表block本身。

descriptor是指向结构体的指针,该结构体中声明了block对象的大小,还声明了copy和dispose这两个辅助函数所对应的函数指针。copy和dispose辅助函数在拷贝和释放block对象时运行,其中会执行一些操作,例如前者会retain所捕获的对象,后者会将它们release。

block会包含所有它捕获的变量的拷贝,存储在descriptor变量之后,并分配足够多的空间以存储这些对象变量。需要注意的是,这并不意味着对象本身被拷贝,而是只拷贝指向这些对象的指针变量。当block执行的时候,这些捕获的变量需要从内存区域中读取出来,这也是为什么需要向invoke函数传递block参数的原因。

全局块,栈块和堆块

根据Block在内存中的位置分为三种类型:NSGlobalBlock,NSStackBlock, NSMallocBlock,即分别为全局块、栈块和堆块。

当定义block的时候,block的内存区域分配在栈中,即为栈块(stack block)。这就容易引起一些错误:

void (^block)();if ( /* some condition */ ) {    block = ^{        NSLog(@"Block A");    };} else {    block = ^{        NSLog(@"Block B");    };}block();

上面这段代码中,block是在if与else语句中定义的,当超过这个语句范围之后,这段内存区域就有可能被覆盖,从而导致意想不到的错误。

解决这个问题的方法是拷贝block,就向block对象发送copy消息。这样就将block从栈(stack)内存中拷贝到了堆(heap)内存中,从而可以在定义该block对象的范围之外使用它。此外,当被拷贝到堆中后,block就变成一个具有引用计数的对象。之后再对它进行copy操作,并不会真的执行copy操作,而是只增加它的引用计数。如果不再使用堆块(heap block)的话,在ARC(atomatic reference count)下就会自动释放,在MRC(manual reference count)下需要显式调用release函数。当引用计数为0的时候,堆块就会和其他对象一样被释放。然而,栈块(stack block)不需要显式释放,因为栈内存本来就会自动回收。

上面的代码加上两个copy方法的调用就会安全了,如下:

void (^block)();if ( /* some condition */ ) {    block = [^{        NSLog(@"Block A");    } copy];} else {    block = [^{        NSLog(@"Block B");    } copy];}block();

如果是手动管理引用计数(MRC)的话,上面的代码在用完块之后还需将其释放。

除了栈块和堆块,另一种块类型是全局块(global block)。这种块不需要捕获任何状态(比如外围的变量等),运行时也无需状态来参与。全局块的全部内存区域在编译时就已经完全确定,因此全局块被声明在全局内存里,而不需要每次用到的时候在栈中创建。这种块,实际上相当于单例(singletons)。这是一种减少不必要工作量的优化策略,如果把如此简单的块当做复杂的块来处理的话就会在copy和dispose该块时执行不必要的操作。

需要注意的是,不同于NSObjec的copy、retain、release操作:

  • Block_copy与copy等效,Block_release与release等效;

  • 对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1;

  • NSGlobalBlock:retain、copy、release操作都无效;

  • NSStackBlock:retain、release操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],(补:在ARC中不用担心此问题,因为ARC中会默认将实例化的block拷贝到堆上)在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之后生成新的NSMallocBlock类型对象。

  • 尽量不要对Block使用retain操作。1

block作为属性时应使用copy进行声明

typedef void (^XYZSimpleBlock)(void);@interface XYZObject : NSObject@property (copy) XYZSimpleBlock blockProperty;@end

block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。2

block是C、C++、Objective-C中的词法闭包。

block可以接受参数,也可返回值。

block可以分配在栈或堆上,也可以是全局的。分配在栈上的block可拷贝到堆里,这样就合标准的Objective-C对象一样具有引用计数了。

第38条:为常用的块类型创建typedef

每个块具有其“固有类型”(inherent type),即它们可以赋值到恰当类型的变量。这个类型由输入参数和返回类型组成。

我们可以为常用的块类型起个别名。为了隐藏复杂的块类型,需要用到C语言中的“类型定义”(type definitions)的语言特性,即使用typedef关键字来定义一个易读的别名,如下:

typedef return_type (^block_name)(parameters);block_name var = ^(parameters){...};

最好与使用块类型的类一起来定义这些typedef,而且命名别名时还应该把这个类的名字作为前缀,这样可以阐明块的用途。还可以用typedef为同一个块签名类型创建多个别名。

以typedef重新定义block类型,可以令block变量使用起来更简单。

定义新类型时,应遵从现有命名习惯,勿使其名称与别的类型相冲突。

不妨为同一个block签名类型定义多个别名。即使要重构的代码使用了block签名类型的某一别名,也只需修改相应typedef中的block签名即可,无需改动其他的别名。

第39条:用handler blocks降低代码分散程度

为界面编程时经常遇到的一种范式是“异步执行任务”(perform tasks
asynchronously)。这样做的好处是:处理用户界面显示及触摸操作的主线程不会因为执行I/O或网络通信这类耗时的任务而阻塞。

在执行异步操作时,需要以某种手段通知相关代码。实现此功能的方法有很多,如设计一个委托协议(delegate protocol),令关注此事件的对象遵从该协议,这样在事件发生时该对象就会得到通知。例如:

#import <Foundation/Foundation.h>@class EOCNetworkFetcher;@protocol EOCNetworkFetcherDelegate <NSObject>- (void)networkFetcher:(EOCNetworkFetcher*)networkFetcher    didFinishWithData:(NSData*)data;@end@interface EOCNetworkFetcher : NSObject@property (nonatomic, weak)id <EOCNetworkFetcherDelegate> delegate;- (id)initWithURL:(NSURL*)url;- (void)start;@end

而使用此类提供的API的类像这样:

- (void)fetchFooData {    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];    EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];    fetcher.delegate = self;    [fetcher start];}// ...- (void)networkFetcher:(EOCNetworkFetcher*)networkFetcher    didFinishWithData:(NSData*)data{    _fetchedFooData = data;}

与使用委托模式的代码相比,用block写出的代码更为简洁,使得异步任务执行完毕后所需运行的业务逻辑,和启动异步任务的代码放到了一起。而且,由于block声明在创建启动异步任务的范围里,它可以访问该范围内的所有变量。例如:

#import <Foundation/Foundation.h>typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);@interface EOCNetworkFetcher : NSObject- (id)initWithURL:(NSURL*)url;- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;@end

而使用此类提供的API的类像这样:

- (void)fetchFooData {    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];    EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];    [fetcher startWithCompletionHandler:^(NSData *data)     {            _fetchedFooData = data;        }];}

在创建对象时,可以使用内联的handler block与相关业务逻辑一起声明。

在有多个实例需要监控时,若采用委托模式,则经常需要根据传入的对象来切换,而若改用handler block,则可直接将block与相关对象放在一起。

设计API时,如果用到了handler block,那么可以增加一个参数,使调用者可通过此参数来决定把block安排在哪个队列(NSOperationQueue)上执行。

第40条:在使用block引用其所属的对象时,避免出现保留环

当block被定义在一个类的实例方法中,如果一个实例变量被block捕获,无论是读还是写,self变量都会被隐式地捕获。这样就会引起self变量被block所retain。这种情况下,如果block本身再被self所指代的实例retain的话,就会经常导致“保留环”(retain cycles)。解决的思路是在block运行快结束时,将此实例变量置为null或将self.completionHandler置为null,从而打破这个环。关键是找出block捕获并因此retain的变量是哪些。如果这些变量中有对象直接或间接地retain了这个block,就需要考虑在合适的时机打破这个保留环。

如果block所捕获的对象直接或间接地保留了block本身,那么就得当心保留环问题。

一定要找个合适得时机来解除保留环,而不能把责任推给API的调用者。

第41条:多用派发队列,少用同步锁

锁的应用场景:多个线程对同一份资源进行修改,需要使用锁来实现同步机制。

OS X和iOS中锁的实现方法:原子操作(atomic operations),pthread_mutex_tNSLock@synchronized,GCD,NSOperationQueue。

atomic operations

atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。

atomic设置成员变量的@property属性时,默认为atomic,提供多线程安全。加了atomic,setter函数会变成下面这样:

{lock}    if(property!=newValue){        [property release];        property = [newValue retain];    }{unlock}

nonatomic禁止多线程,变量保护,提高性能。

atomic是Objc使用的一种线程保护技术,防止在写操作未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的,所以在iPhone这种小型设备上,如果没有使用多线程间的通讯编程,那么nonatomic是一个非常好的选择。

pthread_mutex_t

pthread_mutex_t来源于C语言,定义在pthread.h,所以使用时需要用#include来添加头文件。

使用pthread时常用的函数有:

pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL);pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex);pthread_mutex_destroy(&mutex);  

NSLock

NSLock是Cocoa提供的最基本的锁对象,这也是我们经常所使用的。除lock和unlock方法之外,NSLock还提供了tryLock和lockBeforeDate:两个方法。tryLock方法会尝试加锁,如果锁不可用(已经被锁住),则不会阻塞线程,并返回NO。lockBeforeDate:方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。

BOOL moreToDo = YES;NSLock *theLock = [[NSLock alloc] init];...while (moreToDo) {    /* Do another increment of calculation */    /* until there’s no more to do. */    if ([theLock tryLock]) {        /* Update display used by all threads. */        [theLock unlock];    }}

也可以使用NSRecursiveLock这种递归锁,使得线程能够多次持有该锁,却不会出现死锁现象。

同步块(synchronization block)

@synchronized接收一个对象参数,作为区分它所保护的block的唯一标识符。如果在多个线程里对@synchronized同步块都传递了同一个对象作为参数,则首先获得该锁的线程将阻塞其他线程,直到运行完它的临界区。

作为一种预防错误,@synchronized块添加了异常处理例程来保护代码,保证在异常抛出时自动释放同步锁。这意味着为了使用@synchronized指令,必须在代码中启动异常处理。如果不想让隐式的异常处理例程带来额外开销,应该考虑使用锁的类。

GCD

派发队列可用来表述同步语义,比使用@synchronized块或者NSLock对象更简单。

将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这样做却不会阻塞执行异步派发的线程。

使用同步队列及栅栏块,可以令同步行为更加高效。可以使用栅栏块来实现属性的设置方法。

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

第42条:多用GCD,少用performSelector系列方法

在Objective-C中,向对象发送消息,是通过动态绑定机制来决定需要调用的方法。在底层,由于Objective-C是C的超集,所有方法都是普通的C语言函数,然而对象受到消息之后,究竟调用哪个方法则完全在运行期才能决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。NSObject中定义了几个方法,令开发者可以随意调用任何方法,推迟执行方法调用。这些方法其中包括performSelector

- (id)performSelector:(SEL) selector;

performSelector方法的缺陷有以下几个方面:

  • 在ARC下,由于编译器事先不知道方法名,无法判断选择子的返回值是否应该释放,因此就没有办法自动插入内存管理语义,从而容易导致内存泄漏。
  • 选择子的返回值只能是void或对象类型。
  • 选择子最多只能接受两个参数。

如果改用其他替代方案,如使用块,即把任务封装在块里,通过调用GCD机制的相关方法来实现,则就不受这些限制了。

第43条:掌握GCD及操作队列(NSOperationQueue)的使用时机

操作队列出现在GCD之前,GCD是基于因操作队列而流行的原理而构建的。实际上,自iOS 4和Mac OS X 10.6以后,操作队列在底层上是用GCD来实现的。

GCD是纯C的API,任务用block来表示,而block是个轻量级的数据结构;而操作队列则是Objective-C的对象,更为重量级。

用NSOperationQueue类的addOperationWithBlock:方法搭配NSBlockOperation类来使用操作队列,其语法与纯GCD方式非常类似。使用NSOperation和NSOperationQueue的好处在于:

  • 支持取消操作。
  • 指定操作之间的依赖关系。
  • 通过键值观测机制监控NSOperation对象的属性,例如通过isCancelled或isFinished属性来判断任务是否已取消或完成。
  • 指定操作的优先级。操作的优先级指的是一操作与队列中的其他操作之间的优先关系,而GCD队列的优先级是针对整个队列而言的,无法针对每个块。
  • 重用NSOperation对象。

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

dispatch group是GCD的一项特性,能够将任务分组。调用者可以等待这组任务执行完成,也可以在提供回调函数之后继续往下执行,在这组任务完成后,调用者会得到通知。

通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务。若开发者自己实现此功能,则需要编写大量代码。

第45条:使用dispatch_once来执行只需运行一次的线程安全代码

单例模式的实现代码:

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

通过GCD提供的dispatch_once函数,很容易就能实现“只需执行一次的线程安全代码”。

标记应该声明在static或global作用域中,这样的话,在把只需要执行一次的块传给dispatch函数时,传进去的标记也是相同的。

第46条:不要使用dispatch_get_current_queue

使用GCD时,经常需要判断当前代码正在哪个队列上执行。向多个队列派发任务时,更是如此。

dispatch_queue_t dispatch_get_current_queue()

此函数返回当前正在执行代码的队列。但实际上,从iOS 6.0开始,已经正式启用此函数。不过Mac OS X系统直到10.8版本还尚未将其废弃。虽说如此,但在Mac OS X系统里还是要避免使用它。

该函数的应用场景:检测当前队列是不是某个特定的队列,以避免执行同步派发时导致死锁。但这样做,当派发队列之间有层级关系,即发生嵌套时依然会出现死锁。

解决方案:最好的办法是通过GCD来设定“队列特有数据”(queue-specific data),此功能可以把任意数据以键值对的形式关联到队列里。最重要之处是,假如根据指定的键获取不到关联数据,那么系统就会沿着队列的层级体系向上查找,直到找到数据或到达根队列为止。例如:

dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);dispatch_set_target_queue(queueB, queueA);static int kQueueSpecific;CFStringRef queueSpecificValue = CFSTR("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);    }});

本例创建了两个队列,将队列B的目标队列设置为队列A,队列A的目标队列仍然是默认优先级的全局并发队列。“队列特定数据”提供的这套简洁易用的机制,避免了使用dispatch_get_current_queue时经常遭遇的一个陷阱。此外,调试程序时也许会经常用到dispatch_get_current_queue,只是记住不要把它编译到发行版的程序里就行。


  1. http://www.cnbluebox.com/?p=255 “block使用小结、在arc中使用block、如何防止循环引用” ↩
  2. “Objects Use Properties to Keep Track of Blocks” “https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithBlocks/WorkingwithBlocks.html#//apple_ref/doc/uid/TP40011210-CH8-SW12” ↩
0 0
原创粉丝点击