学会使用Objective-C中的block
Apple从OS X 10.4和iOS 4以后开始支持block,相对于delegate,block有很多便捷之处,使得代码更简洁,可读性更强。但是如果使用不当,则会造成很多问题。本文结合自己的经验和《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》书中的知识点,介绍block的相关知识点。
block语法
我们通过以下图来了解block的语法,图片来自这里
我们来看看上面的图,代码如下
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
根据图中的解释,我们从左向右来看,该block返回值为int类型,'^'符号声明一个名为myblock的block,该block有一个int类型的入参,等号右边则为block的定义,block有一个名为num的int类型的入参,{return num * multiplier;};
则为该block的block实现部分。
该block的调用方法如下,看起来像C的函数调用。
int result = myBlock(2); //reslut = 14;
我们来看看复杂一点的情况:
- (void)startWithBlock:(void(^)())block {
block();
}
- (void)testBlock {
NSString *strBlock = @"NSStackBlock";
[self startWithBlock:^{
NSLog(@"%@",strBlock);
}];
}
控制台输出
NSStackBlock
以上代码,新手看起来可能是会有些费劲的。我们一步一步来,首先,我们调用testBlock函数,在该函数中,
^{
NSLog(@"%@",strBlock);
}];
该代码块实际上是传给了startWithBlock函数的参数block,当执行startWithBlock函数时,调用block()
,实际上就是执行了以上代码块。
使用typedef定义block类型
以上代码可以通过typedef来定义block,以便阅读,如下
typedef int (^myBlock)(int num);
在定义某个block类型时,可以使用
myBlock aBlock = ^(int num) {
//Implemention
};
这样看起来,要比之前简单得多。
block捕获外部变量
block内可以访问block之前定义的变量:
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
int result = myBlock(2); //reslut = 14;
但是,如果想在block内部改变multiplier的值,编辑器则会报错
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
multiplier = 5;
return num * multiplier;
};
编辑器会提示: 变量不能被赋值,需要加上__block
修饰符
error: variable is not assignable (missing __block type specifier)
此时,需要将该变量使用__block
修饰:
__block int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
multiplier = 5;
return num * multiplier;
};
如果multiplier变量是static、static global或者global变量,则不需要添加__block
,该值也是可以在block内部修改的。
static int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
multiplier = 5;
return num * multiplier;
};
因为static、static global或者global变量都是存储在内存中的全局区(静态区),对于这三种类型变量,block内部是捕获了其指针,则可以直接访问修改;而对于之前的临时变量,block则只是捕获了该变量的值,无法修改到外部的变量。
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 的数据结构定义如下(图片来自 [这里](http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/)):
![block内存布局](http:https://img.haomeiwen.com/i809937/6d6f7c759832c3a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
对应的结构体定义如下:
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类型的类的指针,
每个Objective-C对象,都有一个`isa`指针,指向对象的类,而Class里也有个`isa`的指针, 指向meteClass(元类)。元类保存了类方法的列表。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身。如下图
![图片来自《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》](http:https://img.haomeiwen.com/i809937/d400b23575e007d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
对于block,isa指针可以指向
`_NSConcreteStackBlock`、`_NSConcreteMallocBlock`、`_NSConcreteGlobalBlock`这三种类型
* **flags**:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
* **reserved**:保留变量,我的理解是表示block内部的变量数。
* **invoke**:函数指针,指向block的实现代码地址。
* **descriptor**:指向结构体的指针,block的附加描述信息,比如保留变量数、block的大小、copy和dispose辅助函数的函数指针指针
*copy函数为当block执行copy操作或者当block从栈上拷贝到堆上时调用,dispose函数则是block在堆上释放时调用*。
* **variables**:block内部捕获的对象,如
```objective-c
void (^blk)(void) = ^{print(fmt,val)};
此时,variables中则为fmt和val这两个变量
block的类型
block有_NSConcreteStackBlock
、_NSConcreteMallocBlock
、_NSConcreteGlobalBlock
这三种类型。
三种block在内存中存储位置如下图
block类型的区分
以下情况,block为_NSConcreteGlobalBlock
类型
- block内部只使用了全局变量
- block内部没有使用任何外部的局部变量
除了以上两种情况,其他的block为_NSConcreteStackBlock
类型。
而对于_NSConcreteMallocBlock
,只有当_NSConcreteStackBlock
类型的block执行copy操作(手动或者系统执行)时,该block才会是_NSConcreteMallocBlock
类型
我们来看看代码,直观的看看这三种类型的block
-
_NSConcreteGlobalBlock
由类型名字可以得知,该block是存储在在内存中全局区的。
void (^block)() = ^{
NSLog(@"This is a global block");
};
NSLog(@"%@",block);
控制台输出
<__NSGlobalBlock__: 0x104fd52d0>
或者
int globalVal = 1; //此处为全局变量
int (^myBlock)(int) = ^(int num) {
return num * globalVal;
};
NSLog(@"%@",block);
控制台输出
<__NSGlobalBlock__: 0x104fd5310>
该block所需要的全部信息都能在编译期确定。该block是全局存在的,相当于单例了。
-
_NSConcreteStackBlock
由该类型的名字可以看出,该block所占的内存区域是分配在栈(stack)中的。也就是说,块只在定义它的那个范围内(作用域)内有效。如下面代码:
int multiplier = 7;
NSLog(@"%@",^(int num) {
return num * multiplier;
};);
控制台输出
<__NSStackBlock__: 0x7fff59615a18>
以上代码,block内部捕获了multiplier这个外部的局部变量,所以是_NSConcreteStackBlock
类型。
因为该block存在在栈上,在超过block的作用域时,该block就会被系统释放,就有可能会出现block内部的代码还没有走完,就被释放掉的情况。对于这种情况,应该对block执行copy操作,将block复制到堆上。
注:在ARC下,系统在大部分情况下,会将block从栈上复制到堆上,这个后面会细说
-
_NSConcreteMallocBlock
对以上代码中的block执行copy操作,block就变成了_NSConcreteMallocBlock
类型,如下
int multiplier = 7;
NSLog(@"mallocBlock:%@",[^(int num) {
return num * multiplier;
} copy]);
控制台输出
<__NSMallocBlock__: 0x6000000486a0>
拷贝到堆后,block的生命周期就与一般的OC对象一样了。
ARC 下 block 的自动拷贝和手动拷贝
ARC下,以下几种情况,系统会将block从栈上自动复制到堆上
- 当 block 作为函数返回值返回时;
- 当 block 被赋值给
__strong
修饰的 id 类型的对象或 block 对象时; - 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时(比如使用NSArray的enumerateObjectsUsingBlock和GCD的dispatch_async方法时,其block不需要我们手动执行copy操作)
注:系统方法内部对block进行了copy操作
因为在ARC下,对象默认是用__strong
修饰的,所以大部分情况下编译器都会将 block从栈自动复制到堆上,除了以下情况
- block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法;
- block 作为临时变量,没有赋值给其他block
看看代码
block作为函数的返回值,如下
- (void(^)())blockReturn {
NSString *strBlock = @"NSMallocBlock";
return ^(){
NSLog(@"%@",strBlock);
};
}
NSLog(@"%@",[self blockReturn]);
控制台输出
<__NSMallocBlock__: 0x7fa161f081f0>
block赋值给强引用block
typedef void(^block)();
NSString *strBlock = @"NSMallocBlock";
block mallocBlock = ^(){
NSLog(@"%@",strBlock);
};
NSLog(@"%@",mallocBlock);
控制台输出
<__NSMallocBlock__: 0x7fedd0d26110>
将block作为临时变量
NSString *strBlock = @"NSStackBlock";
NSLog(@"%@",^(){
NSLog(@"%@",strBlock);
});
控制台输出
<__NSStackBlock__: 0x7fff563aa9b0>
block作为函数参数
- (void)startWithBlock:(void(^)())block {
NSLog(@"%@",block);
}
- (void)testBlock {
NSString *strBlock = @"NSStackBlock";
[self startWithBlock:^{
NSLog(@"%@",strBlock);
}];
}
执行testBlock方法,控制台输出
<__NSStackBlock__: 0x7fff563aa988>
此处可能会有疑问:既然当block作为函数参数时为_NSConcreteStackBlock
类型,超出其作用域时,block会被释放掉,那会不会出现函数先退出了,block还是没有执行完毕的?
经过我测试,我发现,其实在函数中,在block执行完毕前,函数是不会退出的。因为函数中按顺序执行的,函数中block后的代码会等待block执行完毕,所以在block块代码未执行完毕时,该函数不会退出,从而没有超过block的作用域,block不会被释放。看下面的例子就可以明白了。
- (void)startWithBlock:(void(^)())block {
block();
NSLog(@"%@",block);
}
- (void)testBlock {
NSString *strBlock = @"NSStackBlock";
[self startWithBlock:^{
NSLog(@"%@",strBlock);
}];
}
控制台输出
NSStackBlock
<__NSStackBlock__: 0x7fff54ba4a20>
从打印结果可以看出,当我们执行startWithBlock函数时,先是执行了block内的代码,再是执行函数中block后的代码,所以可以保证block执行完毕。
可能,还有人会问,如果把block()放在子线程中执行呢,这样就不是按顺序执行了,在block块代码执行之前,函数就退出了,这样是不是block就不能执行完毕呢?
其实,把block放子线程中,无非是通过GCD和performSelectorInBackground方法,系统会自动GCD的block进copy操作,而performSelectorInBackground需要传一个selector,又相当于走进了函数里,还是按顺序执行了,函数还是会等待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
block循环引用问题
可以使用__weak
、__unsafe_unretained
、__block
修饰词修饰被block持有的对象来打破循环,还有就是在block执行完毕的时候,将block置nil的方法。具体细节这里就不讲了,有兴趣的童鞋可以看看我简书上写了另一篇文章。