第四十二条 多用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

第四十条 用块引用其所属对象时不要出现保留环

我们使用块的时候,如果不仔细思索,很容易出现“保留环”,我们来举个例子,下面这个类就提供了一套接口,调用者可由此从某个URL中下载数据。在启动获取器时,可设置 completion handler,这个块会在下载结束之后以回调方式执行。为了能在下载完成后通过p_requestCompleted方法执行调用者所指定的块,这段代码需要把completion handler保存到实例变量里面。

// EOCNetwor kFetcher. h
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler) (NSData *data);

@interface EOCNetworkFetcher : NSObject 
@property (nonatomic, strong, readonly) NSURL *url;
-(id)initWithURL:(NSURL^)url;
-(void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;



// EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher ()
@property(nonatomic, strong, readwrite) NSURL *url;
@property(nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler; (nonatomic, strong) NSData *downloadedData;
@implementation EOCNetworkFetcher

-(id)initWithURL:(NSURL*)url {
    if ((self = [super init])) {
        _url = url;
    }
    return sel£;
}

-(void)startWithCompletionHandler:
        (EOCNetworkFetcherCompletionHandlei) completion
{
    self.completionHandler = completion;
    //Start the request
    // Request sets downloadedData property
    //When request is finished, p_requestCompleted is called
}

-(void)p_requestCompleted { 
    if (_completionHandler){ 
    _completionHandler(_downloadedData);
    }
}
@end

某个类可能会创建这种网络数据获取器对象,并用其从URL中下载数据:

@implemantation EOCClass {
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetchedData;
}
-(void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:
                    @"http://www.example.com/something.dat"];
    _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog (@"Request URL %@ finished", _networkFetcher.url);
        _fetchedData = data;
    }];
}
@end

这段代码没有什么问题,但是里面有一个隐蔽的保留环,因为completion handler块要设置_fetchedData实例变量,所以它必须捕获self变量(变量捕获问题详见第37条。这就是说,handler块保留了创建网络数据获取器的那个EOCClass实例。而EOCClass实例则通过strong实例变量保留了获取器,最后,获取器对象又保留了handler块。

如下图所示:

要打破保留环也很容易:要么令_networkFetcher实例变量不再引用获取器,要么令获取器的completionHaiidler属性不再持有handler块。在网络数据获取器这个例子中,应该等 completion handler块执行完毕后,再去打破保留环,以便使获取器对象在handler块执行期间保持存活状态。比方说,completion handler块的代码可以这么修改:

[_networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog (@"Request URL %@ finished", _networkFetcher.url);
        _fetchedData = data;
        _networkFetcher = nil;
    }];

如果设计API时用到了completion handler这样的同调块,那么很容易形成保留环,所以必须意识到这个重要问题。一般只要适时的清理环中的某个引用即可。

但是上例如果不执行completion handler,那么保留环就无法打破,于是内存就会泄漏。

所以我们使调用API的那段代码无须在执行期间保留指向网络数据获取器的引用,而是设定一套机制,令获取器对象自己设法保持存活。要想保持存活,获取器对象可以 在启动任务时把自己加到全局的collection中(比如用set来实现这个collection),待任务完成后,再移除。

所以我们改写一下:

-(void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:
                    @"http://www.example.com/something.dat"];
    EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog (@"Request URL %@ finished", networkFetcher.url);
        _fetchedData = data;
    }];
}

大部分网络通信库都采用这种办法,因为假如令调用者自己来将获取器对象保持存活的话,他们会觉得麻烦。Twitter框架的TWRequest对象也用这个办法。

但是上面这个例子仍然有保留环,completion handler块会通过获取器对象来引用其中的URL,之后获取器会反过来经由CompletionHandler属性保留这个块。我们把块保留在属性里的目的是想稍后来使用这个块。所以一旦我们运行过completion handler之后就可以将它释放了。我们消除保留环可以按照下面修改:

-(void)p_requestCompleted { 
    if (_completionHandler){ 
        _completionHandler(_downloadedData);
    }
    self.completionHandler = nil;
}

这样只要下载请求执行完毕,保留环就解除了。

请注意,之所以要在start方法中把completion handler作为参数传进去,这也是一条重要原因。假如把completion handler暴露为获取器对象的公共属性,那么就不便在执行完下载请求之后直接将其淸理掉了,因为既然已经把handler作为属性公布了,那就意味着调用者可以自由使用它,若是此时又在内部将其清理掉的话,则会破坏“封装语义” (encapsulation semantic)

这两种保留环都很容易发生。使用块来编程时,一不小心就会出现这种bug,反过来说,只要小心谨慎,这种问题也很容易解决。关键在于,要想清楚块可能会捕获并保留哪些对象。如果这些对象又直接或间接保留了块,那么就要考虑怎样在适当的时机解除保留环。

要点

  • 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
  • 一定要找个适当的时机解除保留环,而不能把责任推给API的调用者。
2017/9/5 posted in  第六章 块与GCD

第三十九条 用handler块降低代码分散程度

iOS中我们经常采用异步执行任务的方式,来避免主线程的阻塞。因为“系统监控器”(system watchdog)在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。导致程序崩溃。

但是异步方法执行任务后,需要以某种手段来通知相关代码。实现这一功能有很多方法,常用的技巧是设计一个委托协议,令关注此事件的对象遵从该协议。对象成为delegate之后,就可以在相关事件发生时(例如某个异步任务执行完毕时)得到通知了。例如:

#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

其它类可以像下面这样来使用:

-(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;
}

上面在EOCNetworkFetcher类中声明了一个协议,协议中有一个方法用于通知对象已获取完数据。

之后想要获取到通知的对象遵守该协议,成为它的委托对象。这样在执行完start方法之后,EOCNetworkFetcher会调用委托对象所遵守的协议方法,让委托对象获取收到的数据(也就是通知它)。

上面这种做法没有错误,确实可行。但是如果我们改用块来写的话,代码会更清晰。就是把completion handler定义为块类型,将其当作参数直接传给start方法:

#import <Foundation/Foundation.h>

typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject 
-(id)initWithURL:(NSURL*)url;
-(void)startWithCompletionHandler:
            (EOCNetworkFetcherCompletionHandler)handler;
@end

这和使用委托协议很想,不过多了个好处,就是可以在调用start方法时直接以内联形式 定义completion handler,以此方式来使用“网络数据获取器”(network fetcher),可以令代码比原先易懂很多。例如,下面这个类就以块的形式来定义completion handler,并以此为参数调用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;
});

与使用委托模式的代码相比,用块写出来的代码显然更为整洁。异步任务执行完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。而且,由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。

这种写法其实最重要的用途是处理错误。现在很多基于块的API都使用块来处理错误,可以分别用两个处理程序来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码,与处理正常情况所用的代码,都封装到同一个completion handler块里,我们建议使用后者,因为苹果公司也是这样设计API的。我们举例来说:

#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
typedef void(^EOCNetworkFetcherCompletionHandler) 
                                (NSData *data, NSError *error);

@interface EOCNetworkFetcher : NSObject
-(id)initWithURL:(NSURL*)url;
-(void)startWithCompletionHandler:
                (EOCNetworkFetcherCompletionHandler)completion;
@end

此种API的调用方式如下:

EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher alloc] initWithURL:url]; 
[fetcher startWithCompletionHander:
    ^(NSData *data, NSError *error){ 
    if (error) {
        //Handle failure 
    }else {
        // Handle success
    }
}];

要点

  • 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切 换,而若改用handler块来实现,则可直接将块与相关对象放在一起。
  • 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
2017/9/4 posted in  第六章 块与GCD

第三十八条 为常用的块类型创建typedef

我们在定义一个块时,语法是这样的:

int^(variableName)(BOOL flag,int value) = ^(BOOL flag, int value){
    //Implemention
    return someInt;
}

此块接受两个类型分别为BOOLint的参数,并返回类型为int的值。并且把它赋给了一个变量。

与其他类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不要放在右侧。这种语法非常难记,也非常难读。鉴于此,我们应该为常用的块类型起个别名,尤其是打算把代码发布成API供他人使用时,更应这样做。开发者可以起个更为易读的名字来表示块的用途,而把块的类型隐藏在其后面。例如:

//定义
typedef int(^EOCSomeBlock)(BOOL flag, int value);

声明变量时,要把名称放在类型中间,并在前面加上“^”符号,而定义新类型时也得这么做。上面这条语句向系统中新增了一个名为EOCSomeBlock的类型。此后,不用再以复杂的块类型来创建变量了,直接使用新类型即可:

EOCSomeBlock block = ^(BOOL flag, int value){
    // Implementation
};

这次代码读起来就顺畅多了:与定义其他变量时一样,变量类型在左边,变量名在右边。

我们可以利用这个将使用块的API做的简单易用些,例如:

-(void)startWithCompletionHandler:
            (void(^)(NSData *data, NSError *error))completion;

上面代码接受了一个块作为参数,所以我们可以用上面定义块的方法来改写它:

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);

-(void)startWithCompletionHandler: (EOCCompletionHandler)completion;

现在参数看上去就简单多了,而且易于理解。

我们在定义块的时候要注意,最好在使用块类型的类中定义这些typedef,而且还应该把这个类的名字加在由typedef所定义的新类型名前面,这样可以阐明块的用途。还可以用typedef给同一个块签名类型创建数个别名。在这件事上,多多益善。因为,开发者看到类型的别名以及签名中的参数之后,可以很容易的理解类型的用途。

与此相似,如果有好几个类都要执行相似但各有区别的异步任务,而这几个类又不 能放人同一个继承体系,那么,每个类就应该有自己的completion handler类型。这几个completion handler的签名也许完全相同,但最好还是在每个类里都各自定义一个别名,而不 要共用同一个名称。反之,若这些类能纳人同一个继承中,则应该将类型定义语句放在超类中,以供各子类使用。

要点

  • typedef重新定义块类型,可令块变量用起来更加简单。
  • 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
  • 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef
2017/9/4 posted in  第六章 块与GCD

第三十七条 理解“块”这一概念

首先,块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一 个范围内的东西。块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码。 例如,下面就是个简单的块:

^{
    //Block implementation here
}

块其实就是个值,而且自有其相关类型。与intfloatObjective-C对象一样,也可以把块赋给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:

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

这段代码定义了一个名为someBlock的变量。由于变量名写在正中间,所以看上去也许 =有点怪,不过一旦理解了语法,很容易就能读懂。块类型的语法结构如下:

return_type (^block_name)(parameters)

我们来举个例子,下面这种写法所定义的块,返回int值,并且接受两个int做参数:

int (^addBlock) (int a, int b) = ^(int a, int b){
     return a + b;
};

定义好之后,就可以像函数那样使用了。比方说,addBlock块可以这样用:

int add = addBlock (2, 5) ;  //< add = 12

块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。这也就是说,那个范围里的全部变量,在块里依然可用。比如,下面这段代码所定义的块,就使用了块以外的变量:

int additional = 5;
int (^addBlock) (int a, int b) = ^(int a, int b){ 
    return a + b + additional;
};

int add = addBlock (2, 5);  //< add = 12

默认情况下,为块所捕获的变量,是不可以在块里修改的。在本例中,假如块内的代码改动了additional变量的值,那么编译器就会报错。不过,声明变量的时候可以加上__block 修饰符,这样就可以在块内修改了。

例如:

__block int additional = 5;
int (^addBlock) (int a, int b) = ^(int a, int b){   
    additional++;
    return a + b + additional;
};

int add = addBlock (2, 5);  //< add = 13

块的另一个用法是“内联块”(inline 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++;
    }
}];
//count = 2

这段范例代码也演示了“内联块”(inline block)的用法。传给“numerateObjectsUsingBlock:”方法的块并未先賦给局部变量,而是直接内联在函数调用里了。

然后我们在声明和使用块的时候,要注意它的作用范围。定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。比如下面这个:

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

定义在ifelse语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存, 然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只 能保证在对应的ifelse语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。

我们为了解决这个问题可以给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。

拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应将其释放,在ARC环境下会自动释放。

改动后跟下面一样:

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

除了“桟块”和“堆块”之外,还有一类块叫做“全局块”(global block)。这种块不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要在每次用到 的时候于栈中创建。另外,全局块的拷贝操作是个空操作,因为全局块决不可能为系统所回收。这种块实际上相当于单例。下面就是个全局块:

void (^block)() = ^{
    NSLog(@"This is a block");
};

由于运行该块所需的全部信息都能在编译期确定,所以可把它做成全局块。这完全是种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及丢弃该块时执行一些无谓的操作。

要点

  • 块是C、C++、Objective-C中的词法闭包。
  • 块可接受参数,也可返回值。
  • 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。
2017/9/2 posted in  第六章 块与GCD