软件设计 2017-07-19
Block
是iOS
开发中一种比较特殊的数据结构,它可以保存一段代码,在合适的地方再调用,具有语法简介、回调方便、编程思路清晰、执行效率高等优点,受到众多猿猿的喜爱。但是Block
在使用过程中,如果对Block
理解不深刻,容易出现Cycle Retain
的问题。本文主要从ARC
模式下解析一下Block
的底层实现,以及Block
的三种类型(栈、堆、全局)的区别。
返回值类型 (^block变量名)(形参列表) = ^(形参列表) { }; // 调用Block保存的代码 block变量名(实参);
在项目中,通常会重新定义Block
的类型的别名,然后用别名来定义Block
的类型
// 定义block类型 typedef void (^Block)(int); // 定义block Block block = ^(int a){}; // 调用block block(3);
Block
的底层实现是结构体,和类的底层实现类似,都有isa
指针,可以把Block
当成是一个对象。下面通过创建一个控制台程序,来窥探Block
的底层实现
Block
的内存结构图
Block_layout
结构体成员含义如下:
Block_descriptor
结构体成员含义如下:
具体实现代码如下(代码来自Block_private.h
):
enum { BLOCK_REFCOUNT_MASK = (0xffff), BLOCK_NEEDS_FREE = (1 << 24), BLOCK_HAS_COPY_DISPOSE = (1 << 25), BLOCK_HAS_CTOR = (1 << 26), /* Helpers have C++ code. */ BLOCK_IS_GC = (1 << 27), BLOCK_IS_GLOBAL = (1 << 28), BLOCK_HAS_DESCRIPTOR = (1 << 29) }; /* Revised new layout. */ struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); }; struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */ };
int main(int argc, const char * argv[]) { @autoreleasepool { // 最简block ^{ }; } return 0; }
利用 clang
把 *.m
的文件转换为 *.cpp
文件,就可以看到 Block
的底层实现了
$ clang -rewrite-objc main.m
转换后的代码:
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) { } 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 (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA); } return 0; }
从代码中可以看出,__main_block_impl_0
就是Block
的C++
实现(最后面的_0
代表是main
中的第几个Block
),__main_block_func_0
是Block
的代码块,__main_block_desc_0
是Block
的描述,__block_impl
是Block
的定义。
__block_impl
成员含义如下:
__main_block_impl_0
解释如下:
其中,__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
这是显式构造函数,flags的默认值为0
,函数体如下:
__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; }
可以看出:
__main_block_desc_0
成员含义如下:
Block
有三种类型:
APUE的进程虚拟内存段分布图如下:
其中,_NSConcreteGlobalBlock
和 _NSConcreteStackBlock
可以由程序创建,而 _NSConcreteMallocBlock
则无法由程序创建,只能由 _NSConcreteStackBlock
通过拷贝生成。
测试代码如下:
void (^globalBlock)() = ^{ }; int main(int argc, const char * argv[]) { @autoreleasepool { void (^stackBlock1)() = ^{ }; } return 0; }
clang转换后的代码如下:
// globalBlock struct __globalBlock_block_impl_0 { ... __globalBlock_block_impl_0(void *fp, struct __globalBlock_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteGlobalBlock; ... } }; ... // stackBlock struct __main_block_impl_0 { ... __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; ... } }; ... int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; void (*stackBlock)() = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA); } return 0; }
可以看出, globalBlock
的 isa
指向了 _NSConcreteGlobalBlock
,即在全局区域创建,编译时其具体代码在代码段中,Block
变量则存储在全局数据区;而stackBlock
的 isa
则指向了 _NSConcreteStackBlock
,表明在栈区创建。
由于堆区 Block
是由栈区 Block
转化而成, 所以下面主要分析栈区 Block
如何转化为堆区 Block
。
捕获局部非静态变量
代码:
int a = 0; ^{a;};
转化后:
struct __Person__test_block_impl_0 { ... int a; // a(_a)是构造函数的参数列表初始化形式,相当于a = _a。从_I_Person_test看,传入的就是a __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, int _a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; ... } }; static void _I_Person_test(Person * self, SEL _cmd) { int a; (void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, a); }
可以看到,Block
对于栈区变量的引用只是值传递,由于 Block
内部变量 a
和 外部变量 a
不在同一个作用域,所以在 Block
内部不能把变量 a
作为左值(left-value),因为赋值没有意义。所以,如果出现如下代码,编译器会提示错误:
a = 10;
捕获局部静态变量
代码:
static int a; ^{ a = 10; };
转换后:
struct __Person__test_block_impl_0 { ... int *a; __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, int *_a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; ... } }; static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) { int *a = __cself->a; // bound by copy // 这里通过局部静态变量a的地址来对其进行修改 (*a) = 10; } static void _I_Person_test(Person * self, SEL _cmd) { static int a; // 传入a的地址 (void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, &a); }
由于局部静态变量也存储在静态数据区,和程序拥有一样的生命周期
,但是其作用范围局限在定义它的函数中,所有在Block
里是通过地址
来访问。
捕获全局变量
代码:
// 全局静态 static int a; // 全局 int b; - (void)test { ^{ a = 10; b = 10; }; }
转换后:
static int a; int b; static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) { a = 10; b = 10; } static void _I_Person_test(Person * self, SEL _cmd) { (void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA); }
可以看出,因为全局变量都在静态数据区,在程序结束前不会被销毁,所以block直接访问了对应的变量,而没有在Persontest_block_impl_0结构体中给变量预留位置。
捕获对象的变量
代码:
@interface Person() { int _a; } @end @implementation Person - (void)test { void (^block)() = ^{ _a; }; } @end
转换后:
struct __Person__test_block_impl_0 { ... Person *self; __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) { impl.isa = &_NSConcreteStackBlock; ... } }; static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) { Person *self = __cself->self; // bound by copy (*(int *)((char *)self + OBJC_IVAR_$_Person$_a)); } static void _I_Person_test(Person * self, SEL _cmd) { void (*block)() = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344)); }
可以看到,即使 Block
只是引用对象的变量,但是底层依然引用的是对象本身 self
,这和直接使用 self.a
产生的循环引用的问题是一样的。所以,要在 Block
内使用对象的弱引用,即可解决循环引用的问题。并且,对变量a
的访问也是通过 self
的地址加 a
的偏移量的形式。
**捕获__block修饰的基本变量**
代码:
__block int a; ^{ a = 10; };
转换后:
struct __Block_byref_a_0 { void *__isa; __Block_byref_a_0 *__forwarding; int __flags; int __size; int a; }; struct __main_block_impl_0 { ... __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; ... } }; 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) = 10; } 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 { ... 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), 1}; ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344)); } return 0; }
可以看到,被__block
修饰的变量被封装成了一个对象,类型为__Block_byref_a_0
,然后把&a
作为参数传给了Block
。
__Block_byref_a_0
成员含义如下:
其中,isa
、__flags
和 __size
的含义和之前类似,而 __forwarding
是用来指向对象在堆中的拷贝,runtime.c
里有源码说明:
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) { ... 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 // 堆中拷贝的forwarding指向它自己 copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier) // 栈中的forwarding指向堆中的拷贝 src->forwarding = copy; // patch stack to point to heap copy copy->size = src->size; ... }
这样做是为了保证在 Block
内 或 Block
变量后面对变量a
的访问,都是直接访问堆内的对象,而不上栈上的变量。同时,在 Block
拷贝到堆内时,它所捕获的由 __block
修饰的局部基本类型也会被拷贝到堆内(拷贝的是封装后的对象),从而会有 copy
和 dispose
处理函数。
Block_byref
的结构定义在 Block_private.h
文件里有介绍:
struct Block_byref { void *isa; struct Block_byref *forwarding; int flags; /* refcount; */ int size; void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src); void (*byref_destroy)(struct Block_byref *); /* long shared[0]; */ }; // flags/_flags类型 enum { /* See function implementation for a more complete description of these fields and combinations */ // 是一个对象 BLOCK_FIELD_IS_OBJECT = 3, /* id, NSObject, __attribute__((NSObject)), block, ... */ // 是一个block BLOCK_FIELD_IS_BLOCK = 7, /* a block variable */ // 被__block修饰的变量 BLOCK_FIELD_IS_BYREF = 8, /* the on stack structure holding the __block variable */ // 被__weak修饰的变量,只能被辅助copy函数使用 BLOCK_FIELD_IS_WEAK = 16, /* declared __weak, only used in byref copy helpers */ // block辅助函数调用(告诉内部实现不要进行retain或者copy) BLOCK_BYREF_CALLER = 128 /* called from __block (byref) copy/dispose support routines. */ };
可以看到,Block_byref
和 __Block_byref_a_0
的前4
个成员类型相同,可以互相转化。
** copy
函数
copy
的实现函数是 _Block_object_assign
,它根据对象的 flags
来判断是否需要拷贝,或者只是赋值,函数实现在 runtime.c
里:
// _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); } ... } // _Block_byref_assign_copy源码 static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) { // 这里因为前面4个成员的内存分布一样,所以直接转换后,使用Block_byref的成员变量名,能访问到__Block_byref_a_0的前面4个成员 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) { // 从main函数对__Block_byref_a_0的初始化,可以看到初始化时将flags赋值为0 // 这里表示第一次拷贝,会进行复制操作,并修改原来flags的值 // static int _Byref_flag_initial_value = BLOCK_NEEDS_FREE | 2; // 可以看出,复制后,会并入BLOCK_NEEDS_FREE,后面的2是block的初始引用计数 ... copy->flags = src->flags | _Byref_flag_initial_value; ... } // 已经拷贝到堆了,只增加引用计数 else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) { latching_incr_int(&src->forwarding->flags); } // 普通的赋值,里面最底层就*destptr = value;这句表达式 _Block_assign(src->forwarding, (void **)destp); }
主要操作都在代码注释中了,总体来说,__block修饰的基本类型会被包装为对象,并且只在最初block拷贝时复制一次,后面的拷贝只会增加这个捕获变量的引用计数。
** dispose
函数
dispose
的实现函数是 _Block_object_dispose
,代码依然可以在 runtime.c
里:
void _Block_object_dispose(const void *object, const int flags) { //printf("_Block_object_dispose(%p, %x)\n", object, flags); if (flags & BLOCK_FIELD_IS_BYREF) { // get rid of the __block data structure held in a Block _Block_byref_release(object); } ... } // Old compiler SPI static void _Block_byref_release(const void *arg) { struct Block_byref *shared_struct = (struct Block_byref *)arg; int refcount; // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?) shared_struct = shared_struct->forwarding; ... refcount = shared_struct->flags & BLOCK_REFCOUNT_MASK; ... else if ((latching_decr_int(&shared_struct->flags) & BLOCK_REFCOUNT_MASK) == 0) { ... } }
可以看到,被__block
修改的变量,释放时要 latching_decr_int
减引用计数,直到计数为0
,就释放改对象;而普通的对象、Block
,就直接释放销毁。
**捕获没有__block修饰的对象**
代码:
NSObject *a = [[NSObject alloc] init]; Block block = ^ { a; };
转换后部分结果如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSObject *a = __cself->a; // bound by copy 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, 3/*BLOCK_FIELD_IS_OBJECT*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 3/*BLOCK_FIELD_IS_OBJECT*/);} int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; ... Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, 570425344)); } return 0; }
对象在没有__block
修饰时,并没有产生__Block_byref_a_0
结构体,只是将标志位修改为BLOCK_FIELD_IS_OBJECT
。而在_Block_object_assign
中对应的判断分支代码如下:
... else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) { _Block_retain_object(object); _Block_assign((void *)object, destAddr); } ...
可以看到,block复制时,会retain捕捉对象,以增加其引用计数。
**捕获有__block修饰的对象**
代码:
__block NSObject *a = [[NSObject alloc] init]; Block block = ^ { a; };
转化后部分结果如下:
struct __Block_byref_a_0 { void *__isa; __Block_byref_a_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); NSObject *a; }; 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, 33554432, sizeof(__Block_byref_a_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131,....}; Block block = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344); } // 以下的40表示__Block_byref_a_0对象a的位移(4个指针(32字节)+2个int变量(8字节)=40字节) 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_byref_a_0
外,还另外增加了两个辅助函数__Block_byref_id_object_copy
、__Block_byref_id_object_dispose
,以实现对对象内存的管理。其中两者的最后一个参数131表示BLOCK_BYREF_CALLER|BLOCK_FIELD_IS_OBJECT
,BLOCK_BYREF_CALLER
表示在内部实现中不对a对象进行retain
或copy
;以下为_Block_object_assign
函数的部分代码:
if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) { ... else { // do *not* retain or *copy* __block variables whatever they are _Block_assign((void *)object, destAddr); } }
_Block_byref_assign_copy
函数的以下代码会对上面的辅助函数__Block_byref_id_object_copy_131
进行调用;570425344
表示BLOCK_HAS_COPY_DISPOSE | BLOCK_HAS_DESCRIPTOR
,即(1<<25 | 1<<29),_Block_byref_assign_copy
函数的部分代码:
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); }
在ARC
模式下,在栈间传递Block
时,不需要手动copy
栈中的Block
,即可让Block
正常工作。主要原因是ARC
对栈中的Block
自动执行了copy
,将_NSConcreteStackBlock
类型的Block
转换成了_NSConcreteMallocBlock
类型的Block
。
代码:
int i = 10; void (^block)() = ^{i;}; __unsafe_unretained void (^weakBlock)() = ^{i;}; void (^stackBlock)() = ^{}; NSLog(@"%@", ^{i;}); NSLog(@"%@", block); NSLog(@"%@", weakBlock); NSLog(@"%@", stackBlock); /* ARC <__NSStackBlock__: 0x7fff5fbff708> <__NSMallocBlock__: 0x100300000> <__NSStackBlock__: 0x7fff5fbff738> <__NSGlobalBlock__: 0x100001110> */ /* MRC <__NSStackBlock__: 0x7fff5fbff6e0> <__NSStackBlock__: 0x7fff5fbff740> <__NSStackBlock__: 0x7fff5fbff710> <__NSGlobalBlock__: 0x1000010e0>*/
从打印结果可以看出,ARC
模式下,Block
只有引用了外部的变量,并且被强引用,才会被拷贝到堆上;只引用了外部的变量,或者被弱引用都只在栈上创建;如果没有引用外部变量,无论是否被强引用,都会被转换为全局 Block
,也就是说,在编译时,这个Block
的所有内容已经在代码段
中生成了。
代码
typedef void (^Block)(); NSMutableArray *arrays; void testBlock() { int a = 5; [arrays addObject:^{ NSLog(@"%d", a); }]; } int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here.. arrays = @[].mutableCopy; testBlock(); Block block = [arrays firstObject]; NSLog(@"%@", block); /* ARC * <__NSMallocBlock__: 0x1006034c0> */ /* MRC * 崩溃,野指针 */ } return 0; }
可以看出,ARC
模式下,栈区的Block
被拷贝到了堆区,在 testBlock
函数结束后依然可以访问;而 MRC
模式下,由于我们没有手动执行[block copy]
来将Block
拷贝到堆区,随着函数生命周期结束,Block
被销毁,访问时出现野指针错误,但是如果把testBlock
函数中的Block
打印语句删掉:
NSLog(@"%d", a);
那么,Block
就变为全局的,在MRC
模式下,再次访问不会出错。
http://www.jianshu.com/p/51d04b7639f1
http://www.jianshu.com/p/aff2cad778c0
http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/
runtime.c
Block.private.h