title: 理解Block
block是开发中经常用到的一个对象,或者说一个方法。在很多编程语言中,都有闭包的概念,block就是OC对闭包的实现。
定义block
局部变量:
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {…};
属性:
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);
方法参数:
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
方法调用:
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];
typedef定义:
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...}
block究竟是什么
新建一个工程,我们在main函数里面写一个最简单的block
int main(int argc, char * argv[]) {
@autoreleasepool {
^{
int i = 0;
i++;
};
return 0;
}
}
然后用命令行将main.m这个文件转换成C++代码:clang -rewrite -objc main.m,我们会在目录下会得到一个main.cpp的文件,打开我们会发现其实block是这样的:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int i = 0;
i++;
}
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)};
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
return 0;
}
}
- 因为有isa指针,所以我们可以把block看做一个类对象,isa表明该block的类型,下面会讲到;
- Flags是标志变量;
- Reserved是保留变量;
- FuncPtr是block执行时候调用的函数指针。
__block_impl是block的主体数据结构,__main_block_impl_0可以看作是block的构造函数。在__main_block_impl_0中,有__block_impl类型的impl,用于存储block的数据结构,还有个__main_block_desc_0类型的指针Desc,通过下面__main_block_desc_0的结构体可以看出,这是用于存储block的保留字段和空间大小,另外还有个同名的初始化方法,方法含有三个参数,在main函数内的调用过这个方法,对比一下我们可以看到,第一个参数是__main_block_func_0函数,正好是我们所写block中的代码块,第二个参数是__main_block_desc_0_DATA,即__main_block_desc_0的结构体,同时赋值为{ 0, sizeof(struct __main_block_impl_0)},即reserved为0,Block_size字段为__main_block_impl_0结构体的大小,第三个参数是一个缺省参数,在该函数内部,可以看到确定了该block的类型,缺省参数flags赋值给了impl.Flags,要执行的代码块也赋值给了impl.FuncPtr,后期block在执行的时候可以直接根据该指针调用。
block的类型
上文将main.m文件编译成main.cpp中,有这样一句代码,在__main_block_impl_0结构体的__main_block_impl_0函数中:
impl.isa = &_NSConcreteStackBlock;
block的类型总共有三种:
- _NSConcreteStackBlock 栈block
- _NSConcreteMallocBlock 堆block
- _NSConcreteGlobalBlock 全局block
block的类型根据isa指针来确定的,在实际开发中如何写这三种block呢?如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int xxx = 100;
NSLog(@"globalBlock:%@",^(){
});
NSLog(@"stackBlock:%@",^(){
xxx;
});
void (^mallocBlock)(void) = ^(){
xxx;
};
NSLog(@"mallocBlock:%@", mallocBlock);
}
return 0;
}
打印结果:
globalBlock:<__NSGlobalBlock__: 0x100001060>
stackBlock:<__NSStackBlock__: 0x7fff5fbff728>
mallocBlock:<__NSMallocBlock__: 0x100400110>
可以发现,globalBlock和stackBlock差别只是stackBlock的大括号之中一段引用了一个上下文变量a,stackBlock和mallocBlock的差别只是mallocBlock在stackBlock的基础上赋值给了一个变量。
所以,一个不引用外部变量的block是_NSConcreteGlobalBlock,对应的,引用了外部变量的block则会分配在栈上成为_NSConcreteStackBlock,最后,将栈block赋值给一个变量,系统会自动拷贝到堆上,成为_NSConcreteMallocBlock。
关于__block
在实际开发中,开发者会通过block做回调传值,也因此会修改外部的局部变量。实例代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int xxx = 100;
void (^aBlock)(void) = ^(){
xxx = 101;
};
}
return 0;
}
实际上这段代码是编译不过的,原因刚才提到过,因为block不允许修改外部的局部变量。解决办法很简单,在局部变量的声明前加上__block关键字
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int xxx = 100;
void (^aBlock)(void) = ^(){
xxx = 101;
};
}
return 0;
}
为何加上__block就可以在block内部修改局部变量a的值?我们做如下尝试,在多个位置打印a的地址。
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int xxx = 100;
NSLog(@"before block xxx:%p", &xxx);
void (^aBlock)(void) = ^(){
xxx = 101;
NSLog(@"in block xxx:%p", &xxx);
};
aBlock();
NSLog(@"after block xxx:%p", &xxx);
NSLog(@"block:%@", aBlock);
}
return 0;
}
打印结果如下:
before block xxx:0x7fff5fbff748
in block xxx:0x100300388
after block xxx:0x100300388
block:<__NSMallocBlock__: 0x1003008b0>
此时将其编译成.cpp文件:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __Block_byref_xxx_0 {
void *__isa;
__Block_byref_xxx_0 *__forwarding;
int __flags;
int __size;
int xxx;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_xxx_0 *xxx; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_xxx_0 *_xxx, int flags=0) : xxx(_xxx->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_xxx_0 *xxx = __cself->xxx; // bound by ref
(xxx->__forwarding->xxx) = 101;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->xxx, (void*)src->xxx, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->xxx, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t 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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_xxx_0 xxx = {(void*)0,(__Block_byref_xxx_0 *)&xxx, 0, sizeof(__Block_byref_xxx_0), 100};
void (*aBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_xxx_0 *)&xxx, 570425344));
((void (*)(__block_impl *))((__block_impl *)aBlock)->FuncPtr)((__block_impl *)aBlock);
}
return 0;
}
我们会发现,用__block修饰的变量,会被转化成一个结构体:__Block_byref_xxx_0,同时在第47行,声明了一个该结构体类型的变量,重新开辟空间以及赋值。那么该位置是在哪,是在栈空间还是堆空间?
对比刚才的打印结果,before block ****xxx****:****0x7fff5fbff748,拷贝以后in block ****xxx****:****0x100300388,二者相差太多,反而拷贝后xxx的地址与存在堆内存的block相差1M不到,由此可以推断出,block在拷贝的过程中,将以__block修饰的局部变量也拷贝到了堆中。**
**
简单地数值类型比较简单,下面我们尝试一下对象类型。
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSMutableString *a = [NSMutableString stringWithString:@"Tom"];
NSLog(@"before block:a:%p , %p", a, &a);
void (^foo)(void) = ^{
NSLog(@"in block:a:%p , %p", a, &a);
};
foo();
NSLog(@"after block:a:%p , %p", a, &a);
}
return 0;
}
打印结果:
before block:a:0x1003064f0 , 0x7fff5fbff748
in block: a:0x1003064f0 , 0x1003067a8
after block: a:0x1003064f0 , 0x1003067a8
可以发现,a所指向堆中内容的地址不变,变的是a本身的地址。所以在block拷贝过程中,将原本在栈中的a拷贝到了堆上,a的内容仍然是原本字符串的堆地址,因为由栈到堆,所以a本身的地址发生了变化,再结合刚才cpp文件中,__Block_byref_xxx_0结构体中的__forwarding变量,无论是一开始还在栈的block,还是拷贝后在堆上的block,其__forwarding都是指向了堆上的block,所以才能指向同一段字符串内容。
__block在ARC和MRC下的区别
同样是上面一段代码,在MRC环境下,打印出来的结果是什么呢?
before block:a:0x1003064a0 , 0x7fff5fbff748
in block: a:0x1003064a0 , 0x7fff5fbff748
after block: a:0x1003064a0 , 0x7fff5fbff748
和ARC环境下的不一样,a本身的地址并没有因为__block的修饰而变化,也就是说,a仍然在栈上,并没有拷贝到堆上。同样在MRC环境下,我们再稍作修改。
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSMutableString *a = [NSMutableString stringWithString:@"Tom"];
NSLog(@"before block:a:%p , %p", a, &a);
void (^foo)(void) = [^{
NSLog(@"in block:a:%p , %p", a, &a);
} copy];
foo();
NSLog(@"after block:a:%p , %p", a, &a);
}
return 0;
}
打印结果:
before block:a:0x100302f30 , 0x7fff5fbff748
in block: a:0x100302f30 , 0x100400308
after block: a:0x100302f30 , 0x100400308
在block赋值之前先拷贝一下,这时的效果跟ARC下就差不多了。
总结:MRC下block并没有对__block修饰的局部变量进行拷贝,只是一个弱引用;相反,ARC下block会拷贝该变量,并对其retain。
block的函数性
如果从数据结构的角度上说,因为block是结构体指针,因为其有isa指针,那么在实际使用过程中,我们更注重的是如何利用block进行传值和处理,其作用更像一个函数。
^(){
NSLog(@"block is kind of function too.");
}();
该段代码可以直接运行,加上其可以作为参数和返回值,完全可以实现类似于swift下真正意义上的block,此时,它就是一个函数。