block是C语言的扩充,既像函数那样能够执行又像参数那样进行传递,是可以带自动变量的匿名函数。在OC中,block常常会造成会造成循环引用,我们要用__weak修饰符来打破循引用,达到正常释放的目的。一个小的知识点,block为什么会对其块内的对象进行强引用?因为系统或我们对block进行了copy操作。这些都不是本篇博客的中心,这篇博客主要来看下block的底层实现。
先来看下最简单block(没有参数、没有返回值、block不做任何操作)通过Clang编译后的C++ 代码:
//声明
void (^block)(void) = ^() {
};
//执行
block();
编译后=>
//声明:可以看到这个block是一个没有返回值、没有参数的函数指针
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//执行
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
//C函数:
void func();
void (*funcP)() = &func;
(*funcP)();
下面来看下__main_block_impl_0、__main_block_func_0、__main_block_desc_0_DATA
是什么?
//block结构体
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结构体
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//copy的备份,也就是block块内的内容
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
//block的描述:block的已用空间和size
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块内的局部变量进行值引用:因为block的执行可能已经超出了局部变量的作用域。
int a = 0;
NSMutableArray *array = [[NSMutableArray alloc] init];
void (^block)(void) = ^() {
a = 1; //报错!不能修改
array = [[NSMutableArray alloc] init]; //报错!不能修改
[array addObject:[NSObject new]]; //可以正常进行操作
};
block();
NSLog(@"---%d", a); //a=0;
}
在block块中a其实就是0相当于一个常量,array是一个地址,这个地址也是不可更改的,当能通过地址进行相关操作。
如果有需求对a和array要进行修改,应该用__block进行修饰。
{
__block int a = 0;
__block NSMutableArray *array = [[NSMutableArray alloc] init];
void (^block)(void) = ^() {
a = 1; //可以修改
array = [[NSMutableArray alloc] init]; //可以修改
[array addObject:[NSObject new]]; //可以正常进行操作
};
block();
NSLog(@"---%d", a); //a=1;
}
另外一些C语言变量也允许block修改:静态变量,静态全局变量,全局变量。那么这是为什么呢?
先来看下允许block修改C语言变量:静态变量,静态全局变量,全局变量。
int global_int = 1;
static int static_global_int = 2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
static int static_int = 3;
void (^block)(void) = ^() {
global_int = 10;
static_global_int = 20;
static_int = 30;
};
block();
}
return 0;
}
编译转换后 =>
int global_int = 1;
static int static_global_int = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_int;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_int, int flags=0) : static_int(_static_int) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_int = __cself->static_int; // bound by copy
global_int = 10;
static_global_int = 20;
(*static_int) = 30;
}
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;
static int static_int = 3;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_int));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
可以看到全局变量和静态全局变量的使用和转换前一样,正常访问,因为他们不用担心作用域的问题,而静态变量的指针被copy到了__main_block_func_0
结构体中,利用指针是解决超出作用域使用变量的最简单的方法。
来看下一点:__block说明符
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 100;
void (^block)(void) = ^() {
a = 1000;
};
block();
}
return 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), 100};
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
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
(a->__forwarding->a) = 1000;
}
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};
和上面最简单的block相比,我们仅仅是加了__block int a = 100;
,但是block的代码量却增加了很多。通过代码看到__block的变量a竟然变成了一个结构体变量:该结构体的初始值为100,这表明结构体a和int a是等效的。上面可以看到__Block_byref_a_0
结构体的定义,发最后一个成员变量是int a,也能证明我们的猜想。
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
100
};
再来看__main_block_impl_0
结构体,它持有的是__Block_byref_a_0
结构体a
的指针;在__main_block_func_0
结构体中,通过__Block_byref_a_0
结构体a
的指针访问到成员变量__forwarding,__forwarding是一个指向自身的指针,通过__forwarding找到自己的成员变量a,并进行修改为1000即(a->__forwarding->a) = 1000
。
这里留个疑问:为什么要通过__forwarding指针呢?是不是有点多余?这个我们后面再讲!
通过上面的转化代码可以看到__Block_byref_a_0
结构体是定义在block的外面的,这么做是为了多个block使用时处理的。
block按照其存储的区域可分为:__NSStackBlock__、__NSMallocBlock__、__NSGlobalBlock__
。
stackBlock 通过copy到堆区或者全局区来解决block和其使用的__block临时变量在超出作用域被释放的问题。笔者声明了一个global block来看下他的部分转换后的代码
struct __block_block_impl_0 {
struct __block_impl impl;
struct __block_block_desc_0* Desc;
__block_block_impl_0(void *fp, struct __block_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
看到这句代码impl.isa = &_NSConcreteGlobalBlock;
这个isa是global block变量的地址,可以看到它已经变成一个全局block了。__block的变量也从栈中复制到了堆上。
如果多个block同时持有一个__block的变量,当一个block从栈中复制到堆中之后会对同时复制到堆上的__block的变量进行强引用,剩余的block再从栈中复制到堆中的时候也会对之前复制到堆中的__block的变量进行强引用,增加引用计数。这种模式和OC的ARC模式一样。
最后我们来回答之前留下的一个问题:为什么要通过__forwarding指针呢?是不是有点多余?
回答当然不是多余的,之所以有这个__forwarding是为了一个目的:无论访问的是堆中的__block变量还是栈中的__block变量都能正确的访问。怎么达到这个目的,就是利用我们说的__forwarding指针。在copy到堆中之后,栈中的__forwarding指针会从指向自己变成指向堆中,这样就能达到目的了。
至此我们的block大致就讲完了,我们的讲解是简单的block,没有返回值没有参数,不过这已经足够我们对block进行详细的了解了,当你懂得了这些,更复杂的block也就不在话下!
参考:
《Objective-C高级编程》