Block是C语言的扩充功能,是带有自动变量的匿名函数。block 将同一逻辑的代码放在一个块,使代码更简洁紧凑,易于阅读,比函数使用更方便,代码更美观,开发中受到广泛的使用。
block 的底层实现
将main.m中的代码通过clang编译成main.cpp代码:
int main(int argc, const char * argv[]) {
// insert code here...
void (^blk)(void) = ^{ printf("FlyElephant---Block\n"); };
blk();
return 0;
}
main.cpp代码:
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) {
printf("FlyElephant---Block\n"); }
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[]) {
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
block通过__main_block_impl_0初始化,后序工作通过 __block_impl ,__main_block_desc_0实现。
__block_impl
__block_impl结构体代码如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
- isa 指向实例对象,block 本身也是一个 Objective-C 对象。block 的三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,即当代码执行时,isa 有三种取值 :
impl.isa = &_NSConcreteStackBlock;
impl.isa = &_NSConcreteMallocBlock;
impl.isa = &_NSConcreteGlobalBlock;
- Flags 按位承载 block 的附加信息;
- Reserved 保留变量;
- FuncPtr 函数指针,指向 Block 要执行的函数
以上的四个字段均在初始化的时候完成:
__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;
}
其中FuncPtr指针指向的block函数:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("FlyElephant---Block\n"); }
__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;
}
};
- impl block 实现的结构体变量;
- Desc 描述 block 的结构体变量;
- __main_block_impl_0 结构体的构造函数,初始化结构体变量 impl、Desc;
__main_block_desc_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)};
- reserved 结构体信息保留字段
- Block_size 结构体大小
整个实现过程就是初始化__main_block_impl_0,返回impl,执行impl->FuncPtr.
block 捕获外部变量
int main(int argc, const char * argv[]) {
// insert code here...
int localValue = 1;
void (^blk)(void) = ^{ printf("FlyElephant = %d\n", localValue); };
blk();
return 0;
}
block捕获外部变量编译之后的cpp代码如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int localValue;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localValue, int flags=0) : localValue(_localValue) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int localValue = __cself->localValue; // bound by copy
printf("FlyElephant = %d\n", localValue); }
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[]) {
int localValue = 1;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localValue));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
__block_impl定义不变:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
block通过参数传递获取到了localValue,保存到结构体中的同名变量中。赋值的时候通过__cself来赋值:
int localValue = __cself->localValue; // bound by copy
不过暂时还不能还不能修改值,如果修改localValue会报错:
Variable is not assignable (missing __block type specifier)
内存区域
讨论block的存储区域我们先了解一下C/C++编译的程序在内存中的分布情况 :
1.栈区(stack)— 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。程序结束时由编译器自动释放。
2.堆区(heap) — 在内存开辟另一块存储区域。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是不一样,分配方式类似于链表。用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。
3.全局区(静态区)(static)—编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的。对于C语言初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。而C++则没有这个区别 - 程序结束后由系统释放。
4.文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5.程序代码区—存放函数体的二进制代码。
block访问变量有两种方式一种是静态变量,全局变量和__block形式。
静态变量,全局变量
block使用静态变量,全局变量,全局静态变量代码:
int global_val = 1;
static int static_global_val = 2;
int main(int argc, const char * argv[]) {
// insert code here...
static int static_val = 3;
void(^blk)(void) = ^ {
global_val = 2;
static_global_val = 3;
static_val = 4;
};
return 0;
}
编译之后的代码:
int global_val = 1;
static int static_global_val = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
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_val = __cself->static_val; // bound by copy
global_val = 2;
static_global_val = 3;
(*static_val) = 4;
}
对静态全局变量和全局变量访问和转换之前一样,对静态变量的方式是将指针保存了起来。block对于其自动变量而言没有将指针保存起来,是因为自动变量(局部变量)超出其作用域之后就会被废弃。
__block
通过__block看下代码:
__block int localValue = 0;
void (^blk)(void) = ^{
localValue = 1;
};
编译之后代码:
struct __Block_byref_localValue_0 {
void *__isa;
__Block_byref_localValue_0 *__forwarding;
int __flags;
int __size;
int localValue;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_localValue_0 *localValue; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_localValue_0 *_localValue, int flags=0) : localValue(_localValue->__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_localValue_0 *localValue = __cself->localValue; // bound by ref
(localValue->__forwarding->localValue) = 1;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->localValue, (void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->localValue, 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[]) {
__attribute__((__blocks__(byref))) __Block_byref_localValue_0 localValue = {(void*)0,(__Block_byref_localValue_0 *)&localValue, 0, sizeof(__Block_byref_localValue_0), 0};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_localValue_0 *)&localValue, 570425344));
return 0;
}
相对之前的代码多了不少代码,主要新增代码:
- __Block_byref_localValue_0 结构体:用于封装 __block 修饰的外部变量。
- _Block_object_assign 函数:当 block 从栈拷贝到堆时,调用此函数。
- _Block_object_dispose 函数:当 block 从堆内存释放时,调用此函数。
OC源码中的 __block localValue 翻译后变成了 __Block_byref_intValue_0 结构体指针变量 intValue,通过指针传递到 block 内,与静态变量的指针传递是一致的。__Block_byref_intValue_0 这个结构体的字段需要注意:
struct __Block_byref_localValue_0 {
void *__isa;
__Block_byref_localValue_0 *__forwarding;
int __flags;
int __size;
int localValue;
};
结构体里面还多了个 __forwarding 指向自己的指针变量,这与block的类型有关系。
block类型
block 有三种类型 NSConcreteGlobalBlock,NSConcreteStackBlock和NSConcreteMallocBlock。
NSConcreteGlobalBlock
_NSConcreteGlobalBlock 类型的 block 处于内存的 ROData 段,不捕获局部变量,运行不依赖上下文,内存管理比较简单。
*block 字面量写在全局作用域时,编译之后也是_NSConcreteGlobalBlock类型。
void (^blk)(void) = ^{ printf("Global Block\n"); };
int main(int argc, const char * argv[]) {
blk();
return 0;
}
struct __blk_block_impl_0 {
struct __block_impl impl;
struct __blk_block_desc_0* Desc;
__blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __blk_block_func_0(struct __blk_block_impl_0 *__cself) {
printf("Global Block\n"); }
NSConcreteStackBlock
NSConcreteStackBlock 类型的 block 处于内存的栈区。global block 由于处在 data 段,可以通过指针安全访问,但 stack block 处在内存栈区,如果其变量作用域结束,这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。
为了解决这个问题,block 提供了 copy 的功能,将 block 和 __block 变量从栈拷贝到堆,也就是 _NSConcreteMallocBlock。
_NSConcreteMallocBlock
当 block 从栈拷贝到堆后,当栈上变量作用域结束时,仍然可以继续使用 block,在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,例如:
- block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
- block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
- block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 进行拷贝;
编译器不会自动调用 copy 方法:
- block 作为方法或函数的参数传递时;
typedef int (^blk_t)(int);
blk_t func(int rate)
{
return ^(int count){return rate * count;};
}
上面的 block 获取了外部变量,所以是创建在栈上,当 func 函数返回给调用者时,脱离了局部变量 rate 的作用范围,如果调用者使用这个 block 就会出问题。那 ARC 开启的情况呢?运行这个 block 一切正常,编译器编译之后的代码:
blk_t func(int rate)
{
blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp);
}
objc_retainBlock本质上调用的是_Block_copy函数:
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
objc_autoreleaseReturnValue 本质上调用的是objc_autorelease函数:
id
objc_autoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
return objc_autorelease(obj);
}
block类型拷贝:
Block类型 | 源拷贝区域 | Copy结果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 程序的数据区域 | 什么都不做 |
_NSConcreteMallocBlock | 堆 | 引用计数加1 |
block 内存管理
当 block 从栈内存被拷贝到堆内存时,__block 变量的变化如下图。需要说明的是,当栈上的 block 被拷贝到堆上,堆上的 block 再次被拷贝时,对 __block 变量已经没有影响了。
__forwarding
block 从栈被拷贝到堆时,__forwarding 指针变量也会指向堆区的结构体。
block 获取局部变量,当要在其他地方(超出局部变量作用范围)使用这个 block 的时候,由于访问局部变量异常,导致程序崩溃,因此需要将block从栈区拷贝到堆区。
将 block 拷贝到堆上的同时,将 __forwarding 指针指向堆上结构体。后面如果要想使用 __block 变量,只要通过 __forwarding 访问堆上变量,就不会出现程序崩溃了。
简单讲就是“不管__block变量配置在栈上还是堆上,都能正确的访问该变量。”
__block int val = 0;
void (^blk)(void) = [^{++val;} copy];
++val;
blk();
NSLog(@"%d", val);
👆代码中 ^{++val;} 和 ++val; 都会被转换成 ++(val.__forwarding->val);,堆上的 val 被加了两次,最后打印堆上的 val 为 2。
block 循环引用
如果在Block使用附有__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆上时,该对象为Block所持有。
经典的循环引用是self与block之间的相互引用:
typedef void (^blk_t)(void);
@interface User()
{
blk_t blk_;
}
@end
@implementation User
- (id)init
{
self = [super init];
blk_ = ^{NSLog(@"self = %@", self);};
return self;
}
- (void)dealloc
{
NSLog(@"dealloc");
}
@end
编译器会提示警告:
Capturing 'self' strongly in this block is likely to lead to a retain cycle
如果block中使用self中的属性或者成员变量:
@interface User()
{
blk_t blk_;
}
@property (assign, nonatomic) NSInteger age;
@end
@implementation User
- (id)init
{
self = [super init];
blk_ = ^{
NSLog(@"FlyElephant--%ld", (long)self.age);
};
return self;
}
- (void)dealloc
{
NSLog(@"dealloc");
}
@end
编译会警告提示循环引用:
Capturing 'self' strongly in this block is likely to lead to a retain cycle
__weak修饰符解决循环引用问题:
__weak typeof(User) *weakself = self;
blk_ = ^{
NSLog(@"FlyElephant--%ld", (long)weakself.age);
};
return self;
__block避免循环引用:
typedef void (^blk_t)(void);
@interface User : NSObject
{
blk_t blk_;
}
@end
@implementation User
- (id)init
{
self = [super init];
__block id tmp = self;
blk_ = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
return self;
}
- (void)execBlock
{
blk_();
}
- (void)dealloc
{
NSLog(@"dealloc");
}
@end
int main()
{
id object = [[User alloc] init];
[object execBlock];
return 0;
}
该代码没有引起循环引用。但是如果不执行execBlock实例方式,即不执行复试给成员变量blk_的Block,会循环引用并引起内存泄漏。
参考链接
http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-1/
http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-2/