Objective-C 中 Block 有三种类型:
NSStackBlock 存储于栈区
NSGlobalBlock 存储于程序数据区
NSMallocBlock 存储于堆区
MRC 下
@property (nonatomic, copy ) void(^block)();
int value = 10;
void(^blockA)() = ^{
NSLog(@"value: %d",value);
};
NSLog(@"MRC 引用计数: %ld, block is: %@",[blockA retainCount], blockA);
void(^blockB)() = ^{
NSLog(@"blockB");
};
NSLog(@"MRC 引用计数: %ld, block is: %@",[blockB retainCount], blockB);
_block = [blockA copy];
NSLog(@"MRC 引用计数: %ld, block is: %@",[self.block retainCount],self.block);
[_block retain];
NSLog(@"MRC 引用计数: %ld, block is: %@",[self.block retainCount],self.block);
[_block release];
NSLog(@"MRC 引用计数: %ld, block is: %@",[self.block retainCount],self.block);
打印结果:
MRC 引用计数: 1, block is: <__NSStackBlock__: 0x7fff59038bc8>
MRC 引用计数: 1, block is: <__NSGlobalBlock__: 0x106bc70e0>
MRC 引用计数: 1, block is: <__NSMallocBlock__: 0x610000058330>
MRC 引用计数: 1, block is: <__NSMallocBlock__: 0x610000058330>
MRC 引用计数: 1, block is: <__NSMallocBlock__: 0x610000058330>
可以看到,blockA 与 blockB 的差异只在于有没有调用外部变量,这点差异导致它们的类型不同,存储位置不同。
- NSGlobalBlock
block 内部没有引用外部变量的 Block 类型都是 NSGlobalBlock 类型,存储于全局数据区,由系统管理其内存,retain、copy、release操作都无效。 - NSStackBlock
block 内部引用外部变量,retain、release 操作无效,存储于栈区,变量作用域结束时,其被系统自动释放销毁。MRC 环境下,[[mutableAarry addObject: blockA],(在arc中不用担心此问题,因为arc中会默认将实例化的block拷贝到堆上)在其所在作用域结束也就是函数出栈后,从mutableAarry中取到的blockA已经被回收,变成了野指针。正确的做法是先将blockA copy到堆上,然后加入数组。支持copy,copy之后生成新的NSMallocBlock类型对象。 - NSMallocBlock
如上例中的_block
,[blockA copy]
操作后变量类型变为 NSMallocBlock,支持retain、release,虽然 retainCount 始终是 1,但内存管理器中仍然会增加、减少计数,当引用计数为零的时候释放(可多次retain、release 操作验证)。copy之后不会生成新的对象,只是增加了一次引用,类似retain,尽量不要对Block使用retain操作。
ARC 下:
@property (nonatomic, copy ) void(^block)();
int value = 10;
void(^blockA)() = ^{
NSLog(@"value: %d",value);
};
NSLog(@"ARC 引用计数: %ld, block is: %@",CFGetRetainCount(((__bridge CFTypeRef)blockA)), blockA);
void(^blockB)() = ^{
NSLog(@"blockB");
};
NSLog(@"ARC 引用计数: %ld, block is: %@",CFGetRetainCount(((__bridge CFTypeRef)blockB)), blockB);
_block = blockA;
NSLog(@"ARC 引用计数: %ld, block is: %@",CFGetRetainCount(((__bridge CFTypeRef)_block)), _block);
打印结果:
ARC 引用计数: 1, block is: <__NSMallocBlock__: 0x6080000536b0>
ARC 引用计数: 1, block is: <__NSGlobalBlock__: 0x106bc7140>
ARC 引用计数: 1, block is: <__NSMallocBlock__: 0x6080000536b0>
我们发现,同样的 Block 变量 blockA 在 MRC 下是 NSStackBlock 类型,而在 ARC 下是 NSMallocBlock 类型。再来看看下面ARC下的测试代码:
int value = 10;
NSLog(@"%@",^{
NSLog(@"value: %d",value);
});
void(^blockA)() = ^{
NSLog(@"value: %d",value);
};
NSLog(@"ARC 引用计数: %ld, block is: %@",CFGetRetainCount(((__bridge CFTypeRef)blockA)), blockA);
//打印结果
//<__NSStackBlock__: 0x7fff592aebd8>
//ARC 引用计数: 1, block is: <__NSMallocBlock__: 0x6180000487f0>
由此看出,block 变量在赋值的时候系统自动将其拷贝到堆区了,造成我们看到变量 blockA 是 NSMallocBlock 类型。
我们再定义两个 block 属性:
@property (nonatomic, /*copy strong assign retain*/assign) void(^block)();
@property (nonatomic, copy) void (^nameAge)();
通过 clang 查看cpp源码
static void(* _I_OneViewController_block(OneViewController * self, SEL _cmd) )(){ return (*(void (**)())((char *)self + OBJC_IVAR_$_OneViewController$_block)); }
static void _I_OneViewController_setBlock_(OneViewController * self, SEL _cmd, void (*block)()) { (*(void (**)())((char *)self + OBJC_IVAR_$_OneViewController$_block)) = block; }
static void(* _I_OneViewController_nameAge(OneViewController * self, SEL _cmd) )(){ return (*(void (**)())((char *)self + OBJC_IVAR_$_OneViewController$_nameAge)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_OneViewController_setNameAge_(OneViewController * self, SEL _cmd, void (*nameAge)()) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct OneViewController, _nameAge), (id)nameAge, 0, 1); }
可以看到两个属性的setter getter 方法,assign 修饰的 block 属性的setter 方法是直接赋值的,这样我们就可以理解语句_block = blockA;
,局部变量 blockA 赋值给 属性 block,是等同于基本数据类型的值赋值,在其所在的作用域结束后,属性block 指向的内存会自动释放的,属性 block 就是野指针了,在方法外调用属性block,会奔溃。
- (IBAction)tapBtnOne:(UIButton *)sender {
NSLog(@"%@",_block);
}
调用方法tapBtnOne会奔溃。
可是我们打印出变量 blockA 的类型是NSMallocBlock,其内存分配在堆区,_block = blockA;
后,属性 block 指向同一份的blockA所指的内存块,按理说其分配在堆区,作用域结束也不会被系统自动释放。
其实很好理解,属性 _block 修饰限定符是 assign,表明其任何操作不会引用计数增加的,也就是说_block = blockA;
语句后,_block的引用计数没有增加并且指向的还是同一份内存,作用域结束的时候,ARC 下会自动补全 release 操作语句来释放变量blockA,所以 _block 就成了野指针了。
所以Block属性的特性限定符一般都是 copy,因为其setter方法中会拷贝一份新的副本到堆区。