一.block的底层结构及调用
我们简单实现一个block
void(^block)(void) = ^{ NSLog(@"hello world"); }; block();转C++代码后
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)); ((**void** (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);可以看到block是一个__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; } }; struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; static struct __main_block_desc_0 { size_t reserved; size_t Block_size; }可以看到__main_block_impl_0的结构体包含了__block_impl的结构体和__main_block_desc_0结构体,__block_impl又包含了isa指针和方法地址FuncPtr,__main_block_desc_0主要包含了__main_block_impl_0的结构体大小Block_size
从底层结构可以看出,block其实是一个OC对象,那block中的代码是如何执行的呢
((__block_impl *)block)->FuncPtr
从这句代码我们可以看到是把block强转成了(__block_impl *)类型,这里因为block底层结构体的第一个元素为struct __block_impl impl,因此__main_block_impl_0的地址就是impl的地址,所以可以强转,然后通过impl找到方法地址FuncPtr进行调用
总结: block本质上是一个对象,有自己的isa指针,block封装了函数调用及函数调用环境)
二.block的三种类型
从上面我们可以得知block有自己的isa指针,那接下来我们打印下面这几种情况的block的class
int main(int argc, const char * argv[]) { @autoreleasepool { int age = 10; void(^block)(void) = ^{ }; NSLog(@"%@",[block class]); NSLog(@"%@",[^{ NSLog(@"%d",age); } class]); void(^block3)(void) = ^{ NSLog(@"%d",age); }; NSLog(@"%@",[block3 class]); } return 0; }得到打印信息为 NSGlobalBlock,NSStackBlock,NSMallocBlock
总结:
没有访问auto变量的为 NSGlobalBlock,存储在全局区,访问了auto变量的为 NSStackBlock,存储在栈区,对 NSStackBlock 进行copy操作的为 NSMallocBlock,存储在堆区
在ARC环境下,有些情况会自动对 NSStackBlock 进行copy操作,都有哪些情况呢?
block作为函数返回值时 block强引用时 block作为Cocoa API中方法名含有usingBlock的方法参数时 block作为GCD API的方法参数时
三.block的捕获
测试auto临时变量的捕获
int age = 10; void(^block)(void) = ^{ NSLog(@"age:%d",age); }; block();我们发现可以打印age:10,通过上面的分析我们知道此时age作为临时变量存储在栈区,block在ARC下因为被block指针强应用,此时会copy到堆上,那我们的block是如何捕捉到临时变量age的呢?接下来我们转成C++代码进行分析
int age = 10; void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; } static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_ry_hyx4zvkj45s9nd1fvr1pw_3r0000gn_T_main_4a1515_mi_0,age); }这时我们可以看到__main_block_impl_0结构体新增一个age成员变量,初始化的时候把临时变量int age = 10的值赋值给了内部的成员变量age,在执行block内部代码的时候是从__main_block_impl_0里取出了age的值进行打印
测试static变量的捕获
static int age = 10; void(^block)(void) = ^{ NSLog(@"age:%d",age); }; block();经过C++转换
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *age; } static int age = 10; void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age)); static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int *age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_ry_hyx4zvkj45s9nd1fvr1pw_3r0000gn_T_main_203c2d_mi_0,(*age)); }我们可以看到如果是static变量,block捕获的是age的指针变量
测试全局变量的捕获
int age = 10; int main(int argc, const char * argv[]) { @autoreleasepool { void(^block)(void) = ^{ NSLog(@"age:%d",age); }; block(); } return 0; }转换C++后
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; }发现如果是全局变量,不会进行任何捕获,直接访问
总结:
对auto变量是值传递,static变量是指针传递,全局变量不进行捕获
四.__block
运行如下的代码
int main(int argc, const char * argv[]) { @autoreleasepool { int age = 10; void(^block)(void) = ^{ age = 20; }; block(); } return 0; }我们发现报错missing __block,接下来我们分析一下为什么会报错,此时age作为临时变量作用域是main函数内,block里的代码是作为另一个函数封装在block对象内,临时变量不能跨函数调用,所以报错了,接下来我们在int age=10前面加一个__block的修饰词 __block int age = 10;我们发现神奇的不报错了,而且我们发现外面的局部变量age的值被修改为20,接下来我们通过转成C++代码来看一下底层做了什么
//main函数执行流程 __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10}; void(*block)(void) = (&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344)); ((__block_impl *)block)->FuncPtr)((__block_impl *)block); //block结构体 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_age_0 *age; // by ref } //__Block_byref_age_0类型结构体 struct __Block_byref_age_0 { void *__isa; __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; }; //block块执行的方法 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_age_0 *age = __cself->age; // bound by ref (age->__forwarding->age) = 20; }我们发现底层把age封装成了一个__Block_byref_age_0的对象,age的地址传给了__forwarding指针,age的值传给了__Block_byref_age_0结构体里的成员变量age,然后把这个对象传入了block结构体里的age指针
当age的值进行修改的时候是从当前结构体拿到__Block_byref_age_0类型的age指针,然后再通过__forwarding指针拿到age,然后进行值的修改
总结: __block可以用于解决block内部无法修改auto变量值的问题 __block不能修饰全局变量,静态变量 编译器会将__block包装成一个对象
五.block的内存管理
__block的内存管理
转换下面的代码
int main(int argc, const char * argv[]) { @autoreleasepool { __block int age = 10; void(^block)(void) = ^{ age = 20; }; block(); } return 0; }我们发现__main_block_desc_0结构体多了2个函数__main_block_copy_0和__main_block_dispose_0,里面分别调用了_Block_object_assign和_Block_object_dispose函数,我们知道在ARC下,当前的block因为被强指针指向,会执行copy操作,此时会调用这个__main_block_copy_0函数,执行_Block_object_assign对__Block_byref_age_0对象进行强引用
auto对象的内存管理
转换下面的代码
int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; void(^block)(void) = ^{ NSLog(@"%@",p); }; block(); } return 0; }我们发现结构和上面差不多,只是__main_block_copy_0和__main_block_dispose_0函数的参数略有不同,还有就是此时执行_Block_object_assign函数会根据所指向的对象的修饰符(__strong,__weak,__unsafe_unretained)进行强引用或弱引用
__block修饰的auto对象的内存管理
转换下面的代码
__block Person *p = [[Person alloc] init]; void(^block)(void) = ^{ NSLog(@"%@",p); }; block();我们发现Person对象被封装成了__Block_byref_p_0结构体
struct __Block_byref_p_0 { void *__isa; __Block_byref_p_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); Person *__strong p; };当block从栈copy到堆的时候,会调用__main_block_desc_0里的__main_block_copy_0->_Block_object_assign函数对__Block_byref_p_0 *p进行强引用,然后__Block_byref_p_0会调用内部的__Block_byref_id_object_copy函数根据所指向的对象的修饰符(__strong,__weak,__unsafe_unretained)进行强引用或弱引用
总结:
__block修饰基础类型变量的内存管理:
在栈上时不会产生强引用
当block拷贝到堆时,会调用内部的_Block_object_assign函数进行强引用
当block从堆移除时,会调用内部的_Block_object_dispose函数进行释放
auto对象类型的内存管理:
在栈上时不会产生强引用
当block拷贝到堆时,会调用内部的_Block_object_assign函数根据所指向的对象的修饰符(__strong,__weak,__unsafe_unretained)进行强引用或弱引用
当block从堆移除时,会调用内部的_Block_object_dispose函数进行释放
__block修饰的auto对象的内存管理:
会把对象类型包装成__Block_byref_xxx的结构体
在栈上时不会产生强引用
当block拷贝到堆上时,会调用内部的_Block_object_assign函数对封装的__Block_byref_xxx进行强引用,然后__Block_byref_xxx会调用内部的_Block_object_assign函数根据所指向的对象的修饰符(__strong,__weak,__unsafe_unretained)进行强引用或弱引用
当block从堆移除时,会调用内部的_Block_object_dispose函数进行释放,并且__Block_byref_xxx也会调用内部的_Block_object_dispose函数进行释放
六.block造成的循环引用
执行下面的代码
@interface BlockController ()
@property (nonatomic, assign) int age;
@property (nonatomic, copy) void(^block)(void);
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.block = ^{
self.age = 10;
};
self.block();
}
xcode会给出警告Capturing 'self' strongly in this block is likely to lead to a retain cycle,并且监听delloc方法发现不会执行,这时,就因为我们的不当操作造成了循环引用,从而导致内存泄漏,那我们来分析一下上面的这段代码
首先控制器强引用block属性,block属性捕获了self,这时block从栈copy到堆时,执行_Block_object_assign函数根据self进行引用,因为当前self是__strong类型,所以会强引用当前控制器,控制器强引用block,block又强引用控制器,所以造成了循环引用,那如何解决此类问题呢,只需要把当前对象进行弱引用即可,例如当前代码可以改为
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self)weakSelf = self;
self.block = ^{
weakSelf.age = 10;
};
self.block();
}