一直使用block,总感觉用的云里雾里的,索性进行一次深入的研究,各位看官如果发现文章有什么理解不到位的情况,可以指出来,我们一起研究,如有转载,请注明出处。
block的实质
一.block是什么
- block是带有自动变量(局部变量)值的匿名函数。
顾名思义,所谓匿名函数就是不带有名称的函数。那么带有自动变量是什么意思呢,是怎么来的呢,我的理解就是,截获来的,那么也就是说,block是能够截获自动变量的匿名函数。如何截获的,下面会进行说明。
1.先从一个最简单的不含有变量的block开始看:
int main(int argc, const char * argv[])
{
@autoreleasepool
{
void (^simpleBlock)() = ^{
NSLog(@"Hello,World!");
};
simpleBlock();
}
return 0;
}
使用clang进行反编译(clang -rewrite-objc XXX.m),可以得到C++的实现代码,在这里面,我们可以看到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) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_77zrry7j3b570h0tq36z405c0000gn_T_BlockTest_985af6_mi_0);
}
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, const char * argv[])
{
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
}
return 0;
}
我们可以看到:
void (^simpleBlock)() = ^{
NSLog(@"Hello,World!");
};
被转化成了:
void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
以上代码我们看到我们写的simpleBlock被转化成了指向__main_block_impl_0
结构体的指针。下面我们来看看这个结构体:
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;
}
};
我们可以看到这个结构体包含 __block_impl 类型的 impl,和 __main_block_desc_0 类型的Desc两个变量,且这两个类型都是结构体,以及一个构造方法__main_block_impl_0()。
我个人理解__block_impl
像是 block的一个基类,__main_block_desc_0
就是block的描述。
下面就分析一下包含的这两个结构体是干嘛的。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
- isa 指向所属类的指针 也就是保存了block的类型
- Flags 标识
- Reserved为保留字段
- FuncPtr指针 block执行时调用的函数指针 也就是block内的函数的实现
可以看出,它包含了isa指针(包含isa指针的皆为对象),也就是说block也是一个对象(runtime里面,对象和类都是用结构体表示)。
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)};
- reserved为保留字段默认为0
- Block_size为sizeof(struct __main_block_impl_0),用来表示block所占内存大小。因为没有持有变量,block大小为impl的大小加上Desc指针大小
- __main_block_desc_0_DATA为main_block_desc_0的一个结构体实例,
这个结构体,用来描述block的大小等信息。如果持有可修改的捕获变量时(即加block),会增加两个函数(copy和dispose)
下面我们再来看一下构造函数
__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;
}
构造函数里面初始化了imp和Desc,我们看到impl.isa为&_NSConcreteStackBlock 说明我们的simpleBlock是一个栈类型的block。标识Flags初始化默认为0。再来看看这个构造函数的调用:
void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
impl.FuncPtr赋值为__main_block_func_0
,Desc赋值为&__main_block_desc_0_DATA
以上我们可以看出我们写的simpleBlock是一个对象,这个对象是结构体类型的,我们会给这个结构体里面的实例进行赋值,如描述,类型,标识,方法等。
2.含有变量的block又是什么样子呢?
我们写如下代码:
int main(int argc, const char * argv[])
{
@autoreleasepool
{
int a = 10;
void (^simpleBlock)() = ^{
NSLog(@"a is %d",a);
};
a = 20;
simpleBlock();
}
return 0;
}
我们运行后,会发现打印结果为:
a is 10
为什么后面我们改了a = 20没有起作用呢?
同样的,使用clang命令转化下上述代码
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;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_77zrry7j3b570h0tq36z405c0000gn_T_BlockTest_287a6e_mi_0,a);
}
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, const char * argv[])
{
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
int a = 10;
void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
}
return 0;
}
我们说这个block持有了变量,那么这个变量在哪里呢?看一下代码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我们会发现__main_block_impl_0结构体里面多了一个 int a
; 也就是这个block持有了一个变量a,那么这个a和外面我们在main方法里面写的那个int a = 10
的a有什么关系呢?我们继续看一下__main_block_impl_0的构造函数:
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我们看到这个构造函数后面跟了一个: 其实这个C++的语法,构造函数后面加:相当于赋值,
冒号后面跟的是赋值,这种写法是C++的特性。
A( int aa, int bb ):a(aa),b(bb){
}
相当于
A( int aa, int bb ){
a=aa;
b=bb;
}
也就是上面的代码等价于:
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0){
a = _a;
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我们再来看看__main_block_impl_0的引用:
int a = 10;
void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
我们可以看到参数传入值为10,仅仅是一个值传递,里面a和外面的a没有什么关系,所以后续更改a = 20,并不能影响block内部的变量值。所以外面改变,依然打印:
a is 10
总得来说,所谓"截获自动变量值"意味着在执行block语法的时候,block语法表达式所使用的自动变量值被保存到block的结构体实例(也就是block自身)中。
我们都知道,当我们在block方法里面,改变a的值的时候,编译器会报错,也就是写以下代码的时候,
void (^simpleBlock)() = ^{
a = 30;
NSLog(@"a is %d",a);
};
编译器会报错:
为什么这里会报错呢,为什么这个时候不能给a进行赋值呢?
因为main函数中的局部变量a和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。在不同的作用域的时候,改变值的方法就是通过指针传递,所以,在上面代码中,我们可以通过指针来实现局部变量的修改。如下:
int main(int argc, const char * argv[])
{
@autoreleasepool
{
int *pointer1;
int a = 10;
pointer1=&a; //把变量a的地址付给pointer1
void (^simpleBlock)() = ^{
*pointer1 = 30;
};
simpleBlock();
NSLog(@"a is %d",a);
}
return 0;
}
这样就会打印:
a is 30
这样就可以改了,那么我们是不是每个都可以通过指针来改变局部变量呢?显然不是的,这个例子是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量a还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了(block此时在哪里?),再用指针访问就……所以,对于auto类型的局部变量,不允许block进行修改是合理的。
3.如果我对a加__block修饰,为什么就行改变打印值了呢?
__block修饰符是什么呢? __block修饰符类似于static,auto,register等说明符,他们是用于指定讲变量值设置到哪个存储域中,例如auto表示作为自动变量存储在栈种,static表示作为静态变量存储在数据区中。那么__block是存储到哪里呢?
我们写以下代码:
int main(int argc, const char * argv[])
{
@autoreleasepool
{
__block int a = 10;
void (^simpleBlock)() = ^{
NSLog(@"a is %d",a);
};
a = 20;
simpleBlock();
}
return 0;
}
我们查看打印结果,如下
a is 20
我们发现这次我们修改a = 20后,竟然响应了block里面的打印内容,也就是说__block的修饰起了某些作用,到底起了什么作用呢,我们使用clang进行反编译后,得到以下代码:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_77zrry7j3b570h0tq36z405c0000gn_T_BlockTest_43d9d1_mi_0,(a->__forwarding->a));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
(a.__forwarding->a) = 20;
((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
}
return 0;
}
看到以上代码,我们发现加了__block修饰后,产生了一些之前没有的代码,如新增了一个结构体__Block_byref_a_0
代码如下:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
- __isa, 指向变量Class
- __forwarding,指向自己的指针,当从栈copy到堆时,指向堆上的block
- __flags,当block被copy时,标识被捕获的对象,该执行的操作
- __size,结构体大小
- i,持有的变量
注意到这个结构体中包含了该实例本身的引用 __forwarding。
还增加了两个实例:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
这两个实例是干嘛的,copy和dispose分别有什么作用呢?从表面意思看是执行了copy操作,那么是把什么东西copy了呢,又是从哪里copy到哪里呢?
我们看一个代码例子:
- (void)viewDidLoad {
[super viewDidLoad];
[self testBlockFunc];
}
- (void)testBlockFunc
{
__block int a = 50;
void (^simpleBlock)() = ^{
a = 70;
};
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
simpleBlock();
NSLog(@"a is %d",a);
});
}
我们写一个controller,在viewDidLoad里面定义了一个int a = 50;和一个simpleBlock,我们都知道这个a和 simpleBlock都存储在栈上,我们进入这个controller后,5秒之后执行一个simpleBlock,修改这个变量a的值为70.但是我们在不到5s的时间内就离开这个界面,对应的就是存储在栈上的这个变量a和simpleBlock已经被销毁了,到了5s的时候,这个simpleBlock执行,我们看一下结果:
a is 70
发现虽然已经离开了这个界面,栈上的变量a和simpleBlock已经被销毁了,但是simpleBlock依然执行且依然改了a的值,为什么呢。原来,Blocks提供了将block和__block变量从栈上复制到堆上的功能,将配置在栈上的block复制到堆上,这样即使block语法记述的变量作用域结束,堆上的block还可以继续存在。当block被copy到堆中时,__main_block_impl_0的拷贝辅助函数__main_block_copy_0会将__Block_byref_a_0拷贝至堆中,所以即使局部变量所在栈被销毁,block依然能对堆中的局部变量进行操作
我们来继续之前的话题,来看看是如何copy的。我们看看__main_block_copy_0和__main_block_dispose_0这两个实例:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
我们看到__main_block_copy_0里面用到了_Block_object_assign函数
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
我们再想_Block_object_assign这个函数到底是如何工作的呢,
我们先看一下main函数里面加入__block后,a的变化:
由
__block int a = 10
变成了:
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
上述代码看出加入__block的修饰后,竟然把简单变量int a转变成了一个对象,这个对象是一个结构体__Block_byref_a_0
类型的。
我们继续查看_Block_object_assign的源代码:
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
...
else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF) {
// copying a __block reference from the stack Block to the heap
// flags will indicate if it holds a __weak reference and needs a special isa
_Block_byref_assign_copy(destAddr, object, flags);
}
...
}
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
struct Block_byref **destp = (struct Block_byref **)dest;
struct Block_byref *src = (struct Block_byref *)arg;
...
else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
//printf("making copy\n");
// src points to stack
bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
// if its weak ask for an object (only matters under GC)
struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack
copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
if (src->flags & BLOCK_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
copy->byref_keep = src->byref_keep;
copy->byref_destroy = src->byref_destroy;
(*src->byref_keep)(copy, src);
}
...
}
...
}
这里出现了Block_byref的类型,我理解的Block_byref就是__Block_byref_a_0的基类。
看到有这样一段代码:
copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
src->forwarding = copy; // patch stack to point to heap copy
堆里的Block_byref变量copy的成员变量forwarding指向了它自己,栈里的Block_byref变量src的成员变量forwarding指向了堆里的copy,这就保证了操作的值始终是堆中的拷贝,而不是栈中的值。
我们看一下_Block_object_dispose
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
当堆上的block被废弃时,会使用_Block_object_dispose函数释放该变量(相当于release)。
综上可以得出结论:
当block被copy到堆中时,__main_block_impl_0
的拷贝辅助函数 __main_block_copy_0
会将__Block_byref_a_0拷贝至堆中,所以即使局部变量所在堆被销毁,block依然能对堆中的局部变量进行操作。其中__Block_byref_a_0
成员指针__forwarding
用来指向它在堆中的拷贝,
simpleBlock由:
void (^simpleBlock)() = ^{
NSLog(@"a is %d",a);
}
a = 20;
变成了:
void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
(a.__forwarding->a) = 20;
之前说__forwarding指向了堆里的变量,也就是堆里的变量a赋值为20,所以block打印出来的a就是修改后的值。
从以上例子看出,block是一个匿名函数,本质上block也是一个OC对象,结构体类型的对象,它可以截获自动变量,如果自动变量加了__block修饰符,可以将自动变量从栈拷贝到堆里面,从而保证了超出作用域的时候,block的执行。