Apple从OS X 10.4和iOS 4以后开始支持block,相对于delegate,block有很多便捷之处,使得代码更简洁,可读性更强。下面我们来了解一下block。
一、block的基础知识
block语法
我通过以下图来了解block的语法,图片来自这里
block语法结构图
下面是一个简单的block
NSInteger (^addBlock)(NSInteger a, NSInteger b) = ^(NSInteger a, NSInteger b){
return a + b;
};
这段代码定义了一个名为addBlock的block。该bolck有两个入参a和b,返回值值为NSInteger类型。它的调用方法如下,看起来像C的函数调用。
NSInteger add = addBlock(2, 5); // add = 7
使用typedef定义block类型
以上可以通过typedef来定义block,以便阅读
typedef NSInteger (^addBlock)(NSInteger a, NSInteger b);
在定义某个block类型时,可以使用
addBlock aBlock = ^ (NSInteger a, NSInteger b) {
//Implemention
};
这样看起来,要比之前简单得多。
block的强大
block内可以访问block之前定义的变量:
NSInteger additional = 5;
NSInteger (^addBlock)(NSInteger a, NSInteger b) = ^(NSInteger a, NSInteger b) {
return a + b + additional;
};
NSInteger add = addBlock(2, 5); // add = 12
在block中改变block外定义的变量
想要在block中改变block外定义的变量的话,需要将该变量使用__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
以上代码用block的方法判断array中有多少个大于2的数,通过__block的修饰,在block内改变了count的值。
对于static变量,同样适用
NSArray *array = @[@0,@1,@2,@3,@4,@5];
static NSInteger count = 0;
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop) {
if ([number compare:@2] == NSOrderedAscending) {
count++;
}
}];
//count = 2
block可以访问类的实例变量和self变量
@interface EOCClass : NSObject
@property (nonatomic, copy) NSString *anInstanceVariable;
@end
@implementation EOCClass
- (void)anInstanceMethod {
void (^someBlock)() = ^ {
self.anInstanceVariable = @"Something";
};
someBlock();
NSLog(@"self.aninstanceVaraible = %@", self.anInstanceVariable);
//self.aninstanceVaraible = Something
}
@end
block的内部结构
block 的数据结构定义如下(图片来自 这里):
block内存布局
对应的结构体定义如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
从上面代码看出,一个 block 实例实际上由 6 部分构成:
- isa指针:指向该block类型的类的指针。
- flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
- reserved:保留变量,我的理解是表示block内部的变量数。
- invoke:函数指针,指向block的实现代码地址。
- descriptor:指向结构体的指针,block的附加描述信息,比如保留变量数、block的大小、copy和dispose辅助函数的函数指针指针,copy为保留捕获的对象,dispose为block释放时释放捕获的对象。
- variables:因为block有闭包性,所以可以访问block外部的局部变量。这里为block捕获所有对象的指针。
block的类型
block有全局block、栈block以及堆block三种类型
-
栈block(NSConcreteStackBlock)
定义块时,其所占的内存区域是分配在栈中的。也就是说,块只在定义它的那个范围内(作用域)内有效。如下面代码:
以上代码,定义在if和else中的两个block都分配在栈内存中。编译器会给每个block分配好栈内存,然而等离开了相应的范围(对应于if和else的内的作用域)后,编译器有可能把分配给block的内存覆写掉。所以这两个block只能在保证在自己对应的if和else中的作用域有效。这样的代码在编译器没有覆写其block对应的内存的话,运行是正常的,如果被覆写了,则会crash。void (^block)(); if (/* DISABLES CODE */) { block = ^ { NSLog(@"Block A"); }; } else { block = ^{ NSLog(@"Block B"); }; } block();
为了解决此问题,应用对改block进行copy操作,将其复制到堆上。 -
堆block(NSConcreteMallocBlock)
想要将block复制到堆上,我们对block进行copy操作即可。如下
拷贝到堆后,block的生命周期就与一般的OC对象一样了,在ARC下,我们通过引用计数来对其进行内存管理。void (^block)(); if (/* DISABLES CODE */) { block = [^ { NSLog(@"Block A"); } copy]; } else { block = [^{ NSLog(@"Block B"); } copy]; } block(); -
全局block(NSConcreteGlobalBlock)
全局block,它不会捕捉任何状态(比如外围的变量等),运行时也无需有状态来参与。block所使用的整个内存区域,在编译期已经完全确定了,因此,全局block可以声明在全局内存里,而不需要在每次用到的时候于栈中创建。另外,全局block的copy操作是个空操作,因为全局block绝不可能为系统所回收。这种块实际上相当于单例。如下:
该block所需要的全部信息都能在编译期确定。void (^block)() = ^ { NSLog(@"This is a block); }; -
ARC 对 block 类型的影响
在 ARC下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。
针对不同block类型的copy、retain、release操作
- 对block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1;
- 针对NSConcreteGlobalBlock:retain、copy、release操作都无效;
- 针对NSConcreteStackBlock:retain、release操作无效
注意的是,NSConcreteStackBlock离开其作用域后,该block内存将被回收,即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],在stackBlock离开其作用域失效后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[stackBlock copy]]。 - NSConcreteMallocBlock支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain;
- 尽量不要对block使用retain操作。因为从上可以看出,retain操作对NSConcreteStackBlock并没有效果,这样会误以为retain生效了,在后续调用block的时候,其实block早就被释放了,从而导致crash