block是什么? block本质其实是能够截获自动变量的匿名函数,是一个oc对象(结构体),结构体里面的isa指针指向自己的类,但是它和对象又有一定的区别. 对象一般分配在堆上,而block默认分配在栈上(其他情况下面会说明),Block是C的扩展,指向结构体
__main_block_impl_0
的指针,创建__main_block_impl_0
结构体时会根据变量环境初始化结构体成员变量,由此来分配内存区域,截获变量,管理内存,以及一些copy操作.
block的结构
通过clang工具翻译oc代码可以看到block的结构体信息,具体含义见注释
#import "test.h"
@implementation test
//oc代码
- (instancetype)init {
if (self == [super init]) {
void(^testBlock)() = ^{
NSLog(@"hello world!");
};
testBlock();
}
return self;
}
@end
clang -rewrite-objc test.m
//clang翻译之后的截取代码
// @implementation test
//block指向__test__init_block_impl_0此结构体,block的入口
struct __test__init_block_impl_0 {
struct __block_impl impl; //见下面
struct __test__init_block_desc_0* Desc; //描述block信息的结构体
//结构体的构造函数,含有初始化参数
__test__init_block_impl_0(void *fp, struct __test__init_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; //block 创建在栈区,因此父类为_NSConcreteStackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//block实现的结构体
struct __block_impl {
void *isa; //指向所属的类
int Flags; //按位承载 block 的附加信息;
int Reserved; //保留变量
void *FuncPtr; //函数指针,指向block执行的函数
};
//block要执行的函数代码
static void __test__init_block_func_0(struct __test__init_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gd_3sky2l_d5jv0t9ww_t8_w3680000gp_T_test_87f55d_mi_0);
}
//描述block信息结构体
static struct __test__init_block_desc_0 {
size_t reserved;
size_t Block_size;
}
//结构体变量
__test__init_block_desc_0_DATA = { 0, sizeof(struct __test__init_block_impl_0)};
// @end
block截获自动变量值
int number = 1;
void(^block)() = ^{
NSLog(@"number = %d",number);
};
number++;
block();
上面结果是1,原因就是block截获了自动变量值;到底是如何截获的呢?
根据block结构分析(见注释):
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int number; //形成同名成员变量
//通过下面的构造函数中的numberValue(_number)把变量值保存到结构体中的同名成员变量number中
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int flags=0) : numberValue(_number)
{
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//取出同名的成员变量值
int number = __cself->number; // bound by copy
NSLog(@"number = %d",number);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
截获的变量只能使用不能修改,要想修改得加上__block;
//block截获的变量可以使用不能修改
- (IBAction)test_5:(id)sender {
NSMutableArray *mutArray = [[NSMutableArray alloc]init];
// 此block在栈上,不会对对象的引用计数产生影响
void(^block)() = ^{
#if 1
[mutArray addObject:@"hello - 1"];
[mutArray addObject:@"hello - 2"];
[mutArray addObject:@"hello - 3"];
#else
//被修改会报错,修改要在变量前加__block修饰
NSMutableArray *array = [[NSMutableArray alloc]initWithObjects:@"hello", nil];
mutArray = array;
#endif
};
block();
NSLog(@"count:%ld\n firstObject: %@",mutArray.count,mutArray.firstObject);
}
打印:
count:3
firstObject: hello - 1
block的存储域
根据Block在内存中的位置分为三种类型_NSConcreteGlobalBlock,_NSConcreteStackBlock, _NSConcreteMallocBlock。
- _NSConcreteGlobalBlock:类似函数,位于text段;
- _NSConcreteStackBlock:位于栈内存,作用域结束后后Block将无效;
- _NSConcreteMallocBlock:位于堆内存。
1、当 block 写在全局作用域时,即为 global block;
2、当 block 表达式不引用任何外部变量时,即为 global block;
在以上两种情况下生成的block为_NSConcreteGlobalBlock,其余均为_NSConcreteStackBlock,当 block 从栈拷贝到堆后,当栈上变量作用域结束时,仍然可以继续使用 block,因为此时的block位于堆上,block的类型为:_NSConcreteMallocBlock;所以会将 _NSConcreteMallocBlock 写入 isa如:
impl.isa = &_NSConcreteMallocBlock;
- 栈上的block:
pragma mark - 栈上的block
-(IBAction)test_2:(id)sender {
NSArray *array = [self getBlockArrayFromStack];
// [self test];
void(^block)(void);
block = array[2];
block();
}
-(void)test {
NSLog(@"hello world!");
}
- (NSArray *)getBlockArrayFromStack {
NSArray *blockArray = [[NSArray alloc]initWithObjects: ^{NSLog(@"block-1");},
^{NSLog(@"block-2");},
^{NSLog(@"block-3");},nil];
return blockArray;
}
上面代码中注释的[self test]
看似对程序无影响,但是打开注释就会导致程序崩溃,关闭则无能正确执行;原因在于:一个方法调用的栈帧没有被新的数据覆盖,仍然保留原来block数据的原因所致。这样显然是不安全的,是不能保证block数据可用的。
- 堆上的block
上述代码若是把数组中的block copy到堆上,即把getBlockArrayFromStack
替换为下面方法,那么注释代码打开与否都无影响,原因在于copy到堆上后block仍能脱离作用域继续存在,并指向堆上的block;
-(NSArray *)getBlockArrayFromHeap {
NSArray *blockArray = [[NSArray alloc]initWithObjects:[^{NSLog(@"block-1");} copy],
[^{NSLog(@"block-2");} copy],
[^{NSLog(@"block-3");} copy],nil];
return blockArray;
}
__block变量
__block 说明符可以修饰任何类型的自动变量, __block
修饰的变量其实也是block是一种结构体,初始化结构体的时候会把变量赋值给结构体的同名成员变量,若是变量是对象则会持有它,访问变量的时候则通过它的成员变量__forwarding来访问,结构如下:
struct __Block_byref_obj_0
{
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
__strong id obj;
};
static void __Block_byref_id_object_copy_131(void *dst, void *src)
{
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src)
{
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
/* __block variable declaration */
__Block_byref_obj_0 obj = { 0,
&obj,
0x2000000,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
};
当block从栈copy到堆时,
__block
变量也会一同拷贝到堆上并被该block持有;若干个block引用同一个__block
变量,会增加该block的引用计数,当block废弃时,_block会被释放,若它引用计数为0了就会被废弃,同oc的引用计数内存管理完全一样;无论是在block语法中,block语法外使用
__block
变量,还是__block
变量配置在栈上或堆上,都可以顺利访问到同一个__block
变量,访问形式均可转换为:++(val.__forwarding->val)
,原因是: 栈上的__block
用结构体实例在__block
变量复制到堆上,都会将结构体(__block
)的成员变量__forwarding
的值替换为复制到堆上__block
变量用结构体实例的地址,具体见下面例子:
- (IBAction)test_1:(id)sender {
__block int val = 0;
void (^block)(void) = [^{val++;} copy];
val++;
block();
NSLog(@"%d",val);
}
打印结果: 2
block的内存管理
- block截获对象
typedef void (^myBlock)(id obj);
myBlock block;
- (IBAction)test_6:(id)sender {
[self captureObject];
block([[NSObject alloc]init]);
block([[NSObject alloc]init]);
block([[NSObject alloc]init]);
}
//截获对象
- (void)captureObject {
id array = [[NSMutableArray alloc] init];
block = [^(id obj){
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
翻译代码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
id __strong array;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id __strong array = __cself->array;
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
}
static void __main_block_copy_0(struct __main_block_impl_0 *dst, __main_block_impl_0 *src) {
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
struct static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
/* Block literal and executing the Block */
blk_t blk;
{
id __strong array = [[NSMutableArray alloc] init];
blk = &__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA,
array,
0x22000000);
blk = [blk copy];
}
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
- 总结block的内存管理:
1.栈上的block copy到堆上时,通过copy方法,__main_block_copy_0
函数使用_Block_object_assign
函数将变量array对象赋值给block结构体成员变量并持有该对象;_Block_object_assign
调用相当于retain实例方法的函数,而__main_block_dispose_0
函数使用_Block_object_dispose
函数释放赋值在结构体成员变量array中的对象;
以下情形,block会从栈copy到堆:
- .当 block 调用 copy 方法时,如果 block 在栈上,会被拷贝到堆上;
2).当 block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
3).当 block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法(block用copy修饰的原因);
4).当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 进行拷贝;
2.栈区的block(_NSConcreteStackBlock)在作用域结束后就会被废弃;
3.全局的block(_NSConcreteGlobalBlock)的实例是在数据区域,全局都能访问到,不依赖执行时的状态,因此retain、copy、release操作都无效;
block的循环引用
block循环引用的例子:
例1:
typedef void (^blk_t)(void);
@interface MyObject : NSObject {
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^{NSLog(@"self = %@", self);};
return self;
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
例2:
@interface MyObject : NSObject {
blk_t blk_;
id obj_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^{ NSLog(@"obj_ = %@", obj_); };
return self;
}
...
...
@end
分析:
两个例子中都是block语法赋值给了成员变量中,因此block语法生成的block从栈复制到堆,例1持有使用的self,例2持有成员变量间接持有self;因此self持有block,block持有self,这就形成了循环引用;
解决循环引用可以通过三种方式:
__weak
,__unsafe_unretained
以及__block
来修饰; 因此例1中加入id __weak tmp = self;
例2中加入:id __weak obj = _obj;
并替换block中的self 和成员变量_obj即可;
而__block
修饰避免循环引用的前提是block必须要执行,并且要在block 内将对象置为 nil ,否则还是会造成循环引用,因此可以通过 __block 变量去控制对象的生命周期;
typedef void (^blk_t)(void);
@interface MyObject : NSObject {
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
__block id tmp = self;
blk_ = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
return self;
}
- (void)execBlock {
blk_();
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
- 三种方式解决循环引用的原因:
__weak
和__unsafe_unretained
修饰变量并不会持有对象,在不支持__weak
的情况下可以使用__unsafe_unretained
来修饰避免循环引用,而__block
在 MRC 下,使用__block
说明符也可以避免循环引用。因为当 block 从栈拷贝到堆时,__block
对象类型的变量不会被 retain,没有__block
说明符的对象类型的变量则会被 retian。正是由于__block
在 ARC 和 MRC 下的巨大差异,我们在写代码时一定要区分清楚到底是 ARC 还是 MRC,详细分析可以参考这里;