tzshlyt 2019-07-01
Block作为Objective-C中闭包的实现在iOS开发中占有非常重要的地位,尤其是作为回调(callback)使用。这篇文章主要记录Block的实现,关于Block的语法可以参考这里:How Do I Declare A Block in Objective-C
Block被称为带有自动变量(局部变量)的匿名函数,Block语法去和C语言的函数非常相似。实际上Block的底层就是作为C语言源代码来处理的,支持Block的编译器会将含有Block语法的源代码转换为C语言编译器能处理的源代码,当作C语言源码来编译。
通过LLVM编译器clang可以将含有Block的语法转换为C++源码:
clang -rewrite-objc fileName
比如一段非常简单的含有Block的代码:
#include <stdio.h> int main() { void (^blk)(void) = ^{ printf("Hello Block!\n"); }; blk(); return 0; }
使用clang将其转换为C++源码后,其核心内容如下:
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; // 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中的方法 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Hello Block!\n"); } // block的数据描述 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() { // 调用__main_block_impl_0的构造函数 void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); // blk()调用 ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0; }
这段源码主要包含了3个struct和两个函数,实际上就是C语言源码:
很容易看出mian()函数就是最初代码中的mian函数,__main_block_func_0函数就是最初代码中的Block语法:
^{ printf("Hello Block!\n"); };
由此得出:
接下来重点看看__main_block_impl_0结构体
__main_block_func_0函数的参数__cself类型声明为struct __main_block_impl_0。__main_block_impl_0就是该Block的数据结构定义,其中包含了成员变量为impl和Desc指针,impl的__block_impl结构体声明中包含了某些标志、今后版本升级所需的区域以及函数指针:
struct __block_impl { void *isa; int Flags; // 某些标志 int Reserved; // 今后版本升级所需的区域 void *FuncPtr; // 函数指针 };
Desc指针的中包含了Block的大小:
static struct __main_block_desc_0 { size_t reserved; // 今后版本升级所需的区域 size_t Block_size; // Block的大小 };
在__main_block_impl_0的构造函数中调用了impl和Desc的成员变量,这个构造函数在mian函数中被调用,为了便于阅读,将其中的转换去掉:
// 调用__main_block_impl_0的构造函数 void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); // 去掉转换之后 struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); struct __main_block_func_0 *blk = &tmp;
构造函数中使用的实参为函数指针__main_block_func_0和静态全局变量初始化的__main_block_desc_0结构体实例指针__main_block_desc_0_DATA:
static struct __main_block_desc_0 __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) };
通过这些调用可以总结出下面两条:
将__main_block_impl_0结构体展开:
struct __main_block_impl_0 { void *isa; int Flags; int Reserved; void *FuncPtr; struct __main_block_desc_0* Desc; };
在该程序中构造函数的初始化数据如下:
isa = &_NSConcreteStackBlock; Flags = 0; Reserved = 0; FuncPtr = __main_block_func_0; Desc = &__main_block_desc_0_DATA;
可以看出FuncPtr = __main_block_func_0就是简单的使用函数指针FuncPtr调用函数__main_block_func_0打印Hello Block!语句,这就是最初的源码中对于block调用的实现:
blk();
其对应的源码去掉转换之后就很清晰:
// blk()调用 ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); // 去掉转换之后: (*blk->impl.FuncPtr)(blk);
到此,对于Block的创建和使用就可以这样理解:
在__main_block_impl_0的构造函数中有一个_NSConcreteStackBlock:
impl.isa = &_NSConcreteStackBlock;
这个isa指针很容易想到Objective-C中的isa指针。在Objective-C类和对象中,每个对象都有一个isa指针,Objective中的类最终转换为struct,类中的成员变量会被声明为结构体成员,各类的结构体是基于objc_class结构体的class_t结构体。
typedef struct Objc_object { Class isa; } *id; typedef struct obje_class *Class; struct objc_class { Class isa; }; struct class_t { struct class_t *isa; strcut class_t *superclass; Cache cache; IMP *vtable; uintptr_t data_NEVER_USE; };
实际上Objective-C中由类生成对象就是像结构体这样生成该类生成的对象的结构体实例。生成的各个对象(即由该生成的对象的各个结构体实例),通过成员变量isa保持该类的结构体实例指针。
比如一个具有成员变量valueA和valueB的TestObject类:
@interface TestObject : NSObject { int valueA; int valueB; } @end
其类的对象的结构体如下:
struct TestObject{ Class isa; int valueA; int valueB; };
在Objective-C中,每个类(比如NSObject、NSMutableArray)均生成并保持各个类的class_t结构体实例。该实例持有声明的成员变量、方法名称、方法的实现(即函数指针)、属性以及父类的指针,并被Objective-C运行时库所使用。
再看__main_block_impl_0结构体就相当于基于Objc_object的结构体的Objective-C类对象的结构体,其中的成员变量isa初始化为isa = &_NSConcreteStackBlock;
,_NSConcreteStackBlock就相当于calss_t结构体实例,在将Block作为Objective-C对象处理时,关于该类的信息放置于_NSConcreteStackBlock中。
实际上Block的实质Block就是Objective-C对象。
Block作为传统回调函数的替代方法的其中一个原因是:block允许访问局部变量,能捕获所使用的变量的值,即保存该自动变量的瞬间值,比如下面这段代码,Block中保存了局部变量mul的瞬间值7,所以后面对于mul的更改不影响Block中保存的mul值:
int mul = 7; int (^blk)(int) = ^(int num) { return mul * num; }; // change mul mul = 10; int res = blk(3); NSLog(@"res:%d", res); // res:21 not 30
通过clang来看看Block捕获自动变量之后Block的结构有什么变化:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int mul; // Block语法表达式中使用的自动变量被当作成员变量追加到了__main_block_impl_0结构体中 // 初始化结构体实例时,根据传递构造函数的参数对由自动变变量追加的成员变量进行初始化 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _mul, int flags=0) : mul(_mul) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int mul = __cself->mul; // bound by copy printf("mul is:%d\n", mul); } 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 mul = 7; void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, mul)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0; }
分析__main_block_impl_0和其构造方法可以发现:Block中使用的自动变量mul被当作成员变量追加到了__main_block_impl_0结构体中,并根据传递构造函数的参数对该成员变量进行初始化。__main_block_impl_0中结构体实例的初始化如下:
impl.isa = &_NSConcreteStackBlock; impl.Flags = 0; impl.FuncPtr = __main_block_func_0; Desc = &__main_block_desc_0_DATA; mul = 7; // 追加的成员变量
由此可见,在__main_block_impl_0结构体实例(即Block)中,自动变量值被捕获。可以将Block捕获自动变量总结为如下:Block在执行语法时,Block中所使用的自动变量值被保存到Block的结构体实例(即Block自身)中。即向结构体__main_block_impl_0中追加成员变量。
虽然Block能捕获自动变量值,但是却不能对其进行修改,比如下面代码就会报错:
int main() { int val = 10; void(^blk)(void) = ^ { val = 1; // error Variable is not assignable (missing __block type specifier) }; blk(); printf("val:%d\n", val); return 0; }
需对val变量使用__block说明符:
int main() { __block int val = 10; void(^blk)(void) = ^ { val = 1; }; blk(); printf("val:%d\n", val); // val:1 return 0; }
将其用clang转换之后:
struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_val_0 *val; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref (val->__forwarding->val) = 1; } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->val, 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() { // __block类型的变量居然变成了结构体 __block int val = 10; __attribute__((__blocks__(byref))) __Block_byref_val_0 val = { (void*)0, (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10 }; void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); printf("val:%d\n", (val.__forwarding->val)); return 0; }
增加了__block变量之后源码急剧增多,最明显的是增加了一个结构体和4个函数:
首先比较一下使用__block和没有使用__block的__main_block_func_0函数对变化
// 没有使用__block static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int mul = __cself->mul; // bound by copy printf("mul is:%d\n", mul); } // 使用__block static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; // bound by ref (val->__forwarding->val) = 1; }
可以看出在没有使用__block时,Block仅仅是捕获自动变量的值,即int mul = __cself->mul;
。
再看刚才的源码,使用__block变量的val居然变成了结构体实例。
// __block int val = 10; 转换之后的源码: __attribute__((__blocks__(byref))) __Block_byref_val_0 val = { 0, &val, 0, sizeof(__Block_byref_val_0), 10 };
__block变量也同Block一样变成了__Block_byref_val_0结构体类型的自动变量(栈上生成的__Block_byref_val_0结构体实例),该变量初始化为10,且这个值也出现在结构体实例的初始化中,表示该结构体持有相当于原有自动变量的成员变量(下面__Block_byref_val_0结构体中的成员变量val就是相当于原自动变量的成员变量):
struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; // 相当于原自动变量的成员变量 };
回过头去看Block给val变量赋值的代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; // bound by ref (val->__forwarding->val) = 1; }
得出:__Block_byref_val_0结构体实例的成员变量__forwarding持有指向实例自身的指针。通过成员变量__forwarding访问成员变量val。
这里没有将__block变量的__Block_byref_val_0结构体直接写在Block的__main_block_impl_0结构体中是为了能在多个Block中使用同一个__block变量。 比如在两个Block中使用同一个__block变量:
__block int val = 10; void (^blk1)(void) = ^ { val = 1; }; void (^blk2)(void) = ^ { val = 2; };
转换之后:
__Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof( __Block_byref_val_0), 10}; blk1 = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 570425344); blk2 = &__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, &val, 570425344);
虽然到这里已经大致知道为什么Block能捕获自动变量了,但是这里还遗留几个问题:
综上可知:
结构体类型的自动变量即栈上说生成的该结构体的实例。
既然Block是Objective-C对象,那么它具体是哪种对象?在Block中的isa指针指向的就是该Block的Class,目前所见都是_NSConcreteStackBlock类型,而在block的runtime中实际定义了6中类型的Block,其中我们主要接触到的是这三种:
它们对应在程序中的内存分配:
那么Block在什么情况下时在堆上的?什么时候时栈上的?什么时候又是全局的?
_NSConcreteGlobalBlock很好理解,将Block当作全局变量使用的时候,生成的Block就是_NSConcreteGlobalBlock类对象。比如:
#include <stdio.h> void (^blk)(void) = ^{ printf("Gloabl Block\n"); }; int main() { return 0; }
用clang转换之后为该Block用结构体__block_impl的成员变量初始化为_NSConcreteGlobalBlock,即Block用结构体实例设置在程序内存的数据区:
isa = &_NSConcreteGlobalBlock;
将全局Block存放在数据区的原为:使用全局变量的地方不能使用自动变量,所以不存在对自动变量的捕获。因此Block用结构体实例的内容不依赖于执行时的状态,所以整个程序中只需要一个实例。只有在捕获自动变量时,Block用结构体实例捕获的值才会根据执行时的状态变化。因此总结Block为_NSConcreteGlobalBlock类对象的情况如下:
除了上述两中情况下Block配置在程序的数据区中以外,Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。
配置在栈上的Block,如果其所属的变量作用域结束,该Block就被自动废弃。
那么配置在堆上的_NSConcreteMallocBlock类在何时使用?
配置在全局变量上的Block,从变量作用域外也可以通过指针访问。但是设置在栈上的Block,如果其所属的作用域结束,该Block就被废弃;并且__block变量的也是配置在栈上的,如果其所属的变量作用域结束,则该__block变量也会被废弃。那么这时需要将Block和__block变量复制到堆上,才能让其不受变量域作用结束的影响。
Block提供了将Block和__block变量从栈上复制到堆上的方法。复制到堆上的Block将_NSConcreteMallocBlock类对象写入Block用结构体实例的成员变量isa:
isa = &_NSConcreteMallocBlock;
对于堆上的__block的访问,就是通过__forwarding实现的:__block变量用结构体成员变量__forwarding实现无论__block变量配置在栈还是在堆上都能正确的访问__block变量。当__block变量配置在堆上时,只要栈上的结构体成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上还是从堆上的__block变量都能正确访问。
并且在ARC时期,大多数情况下编译器知道在合适自动将Block从栈上复制到堆上,比如将Block作为返回值时。而当向方法或函数的参数中传递Block时,编译器不能判断,需要手动调用copy方法将栈上的Block复制到堆上,但是apple提供的一些方法已经在内部恰当的地方复制了传递过来的参数,这种情况就不需要再手动复制:
并且,不管Block配置在何存,用copy方法复制都不会出现问题。但是将Block从栈上复制到堆上时相当消耗CPU的。对于已经在堆上的Block调用copy方法,会增加其引用计数。
并且对使用__block变量的Block从栈复制到堆上时,__block变量也会收到影响:如果在1个Block中使用__block变量,当该Block从栈复制到堆时,这些__block变量也全部被从栈复制到堆上。并且此时Block持有__block变量。如果有个Block使用__block变量,在任何一个Block从栈复制到堆时,__block变量都会一并复制到堆上并被该Block持有;当剩下的Block从栈复制到堆时,被复制的Block持有__block变量,并增加其引用计数。如果配置在堆上的Block被废弃,它说使用的__block变量也就被释放。这种思考方式同Objective-C内存管理方式相同。即使用__block变量的Block持有该__block变量,当Block被废弃时,它所持有的__block变量也被废弃。
在这里Block捕获的是__block类型的变量val,如果捕获的是Objective-C对象会有什么区别?
int main() { id arr = [[NSMutableArray alloc] init]; void (^blk)(id) = [^(id obj) { [arr addObject:obj]; NSLog(@"arr count: %ld", [arr count]); } copy]; blk(@"Objective-C"); blk(@"Switf"); blk(@"C++"); return 0; }
值得注意的是:Block捕获的是objective-C对象,并且调用变更该对象的方法addObject:,所以这里不会产生编译错误。这是因为block捕获的变量值是一个NSMutableArray类的对象,用C语言描述就是捕获NSMutableArray类对象用的结构体实例指针。addObject方法是使用block截获的自动变量arr的值,所以不会有任何问题,但是如果在Block内部去给捕获的arr对象赋值就会出错:
int main() { id arr = [[NSMutableArray alloc] init]; void (^blk)(id) = [^(id obj) { arr = [NSMutableArray arrayWithObjects:obj, nil]; // error Variable is not assignable (missing __block type specifier) NSLog(@"arr count: %ld", [arr count]); } copy]; blk(@"Objective-C"); return 0; }
之前的代码转换之后的部分源码为:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->arr, (void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/); } 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 int val = 10;
转换之后的源码中的部分内容:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->val, 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*); }
OBjective-C对象和__block变量对比,发现在Block用的结构体部分基本相同,不同之处在于:Objective-C对象用BLOCK_FIELD_IS_OBJECT标识,__block变量是用BLOCK_FIELD_IS_BYREF标识。即通过BLOCK_FIELD_IS_OBJECT和BLOCK_FIELD_IS_BYREF参数区分copy函数和dispose函数的对象类型是对象还是__block变量。
该源码中在__main_block_desc_0 结构体中增加了成员变量copy和dispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和__main_block_dispose_0函数,这两个函数的作用:
这两个函数在Block从栈复制到堆和已经堆上的Block被废弃时调用:
当Block从栈复制到堆上时,Block会持有捕获的对象,这样就容易产生循环引用。比如在self中引用了Block,Block优捕获了self,就会引起循环引用,编译器通常能检测出这种循环引用:
@interface TestObject : NSObject @property(nonatomic, copy) void (^blk)(void); @end @implementation TestObject - (instancetype)init { self = [super init]; if (self) { self.blk = ^{ NSLog(@"%@", self); // warning:Capturing 'self' strongly in this block is likely to lead to a retain cycle }; } return self; }
同样,如果捕获到的是当前对象的成员变量对象,同样也会造成对self的引用,比如下面的代码,Block使用了self对象的的成员变量name,实际上就是捕获了self,对于编译器来说name只不过时对象用结构体的成员变量:
@interface TestObject : NSObject @property(nonatomic, copy) void (^blk)(void); @property(nonatomic, copy) NSString *name; @end @implementation TestObject - (instancetype)init { self = [super init]; if (self) { self.blk = ^{ NSLog(@"%@", self.name); }; } return self; } @end
解决循环引用的方法有两种:
代码:
- (instancetype)init { self = [super init]; if (self) { __weak typeof(self) weakSelf = self; self.blk = ^{ NSLog(@"%@", weakSelf.name); }; } return self; } // 或 - (instancetype)init { self = [super init]; if (self) { id tmp = self.name; self.blk = ^{ NSLog(@"%@", tmp); }; } return self; }
使用__weak修饰符修饰对象之后,在Block中对对象就是弱引用: