block是在iOS开发中经常使用到的技术,我们可以通过block传值或回调,响应适当时候该响应的事件。那block具体是怎么实现的,原理是什么,了解这些,能够帮助我们更好、更合适的使用block,可以规避使用block可能带来的循环引用,了解变量捕获机制,正确使用__block!
01
block本质上也是一个OC对象,它内部也有个isa指针,block是封装了函数调用以及函数调用环境的OC对象。
了解block的底层结构

block的变量捕获(capture)
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
| 变量类型 | 是否捕获到block内部 | 访问方式 |
| auto修饰符(局部变量) | 会捕获 | 值传递 |
| static修饰符(局部变量) | 会捕获 | 指针传递 |
| 全局变量 | 不会捕获 | 直接访问,变量的地址 |
局部变量auto(自动变量)
平时写的局部变量,默认就有auto(自动变量,离开作用域 { } 就会自动销毁),该类型变量一般存储在Stack栈上。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 实际上是auto int age = 10, 表示自动释放的局部变量,离开作用域就会被释放。
int age = 10;
void(^block)(void) = ^{
NSLog(@"age is %d", age);
};
block();
}
return 0;
}
// 将代码转为编译后代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
// 找到block最后生成的结构体,这里可以看到age成为该结构体的一个变量。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
对象类型的auto变量
02
block内部调用对象时,会自动生成相应的内存管理方法。并在适当的时候retain或relese
当block内部访问了对象类型的auto变量时,如果block是在栈上,将不会对auto变量产生强引用。
调用copy方法将会调用block内部的_Block_object_assign函数,_Block_object_assign会根据auto变量的修饰符(__strong, __weak, __unsafe_unretained)做出相应的操作。形成强引用(retain)或者弱引用。
block从堆上移除,会调用blcok内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose会自动释放引用的auto变量。
局部变量static(静态局部变量)
修饰符 extern/staic/const/UIKIT_EXTERN(OC extern)的使用
静态变量(static 修饰的变量)都在全局数据区分配内存,包括静态全局变量和静态局部变量。直到程序结束运行,它才会被释放。
static int height = 20;
// static 局部变量也捕获,但和auto变量不同,static存储的是指针,指向->height内存地址
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
block的类型
block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
NSGlobalBlock ( _NSConcreteGlobalBlock )
NSStackBlock ( _NSConcreteStackBlock )
NSMallocBlock ( _NSConcreteMallocBlock )

| block | 环境 | 内存区、释放机制 |
| NSGlobalBlock | 没有访问auto变量 | 全局数据区 程序运行结束时才会被释放 |
| NSStackBlock | 访问了auto变量 | 栈区 变量在作用域结束后就会被释放 |
| NSMallocBlock | _NSStackBlock_ 调用了copy | 堆区 需要程序员手动管理内存 |
block的copy
| block类型 | 副本源的配置存储域 | 复制效果 |
| _NSConcreteStackBlock | 栈 | 从栈复制到堆 |
| _NSConcreteGlobalBlock | 程序的数据区 | 什么也不做 |
| _NSConcreteMallocBlock | 堆 | 引用计数器增加 |
03
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况
block作为函数返回值时
typedef void (^Block)(void);
- (Block)doSomething {
Block b = ^{
};
return b;
}
将block赋值给__strong指针时
int a = 10;
// b访问了auto变量,理论上是__NSStackBlock__, 但是b 默认被__strong修饰
// 所以ARC中会被copy操作上堆。
// 实际上是 __strong Block b = ...
Block b = ^{
NSLog(@"a is %d", a);
};
// weakb 访问了auto变量,所以在栈上__NSStackBlock__
__weak Block weakb = ^{
NSLog(@"a is %d", a);
};
// ^{} 没有访问auto变量,所以为:__NSGlobalBlock__
NSLog(@"b class is %@, and block2 class is %@, weakb class is %@",
[b class], [^{} class], [weakb class]);
// 打印输出
b class is __NSMallocBlock__,
and block2 class is __NSGlobalBlock__,
block3 class is __NSStackBlock__
block作为Cocoa API中方法名含有usingBlock的方法参数时
[[[NSArray alloc] init] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
block作为GCD API的方法参数时
// dispatch_async(dispatch_queue_t _Nonnull queue, <#^(void)block#>)
dispatch_async(dispatch_get_main_queue(), ^{
});
MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);
ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
__block的本质是什么?
__block会将修饰对象封装成__Block_byref_a_0结构体,将该对象作为自己的成员变量,如果是对象类型,同时会生成__Block_byref_id_object_copy方法和__Block_byref_id_object_dispose,对其内存进行管理。
// 修饰基本数据类型
__block int a = 10;
NSLog(@"a is %d", a);
// 转为源码
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
// 修饰对象
__block NSObject *obj = [[NSObject alloc] init];
NSLog(@"str is %@", obj);
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj;
};

__block的使用
04
编译器会将__block变量包装成一个对象
__block可以用于解决block内部无法修改auto变量值的问题
int age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d", age);
};
age = 20;
block();
// 打印输出
2021-05-30 21:44:34.484323+0800 Strong&Weak[7070:466915] age is 10
这种情况,block捕获age,并将age的值赋值给了block对象中的age。所以之后改变age,也不改变block中的age。通过__block修饰,则会生成一个__Block_byref_age_1结构体
__block int age = 10;
struct __Block_byref_age_1 {
void *__isa;
__Block_byref_age_1 *__forwarding; // 指向自己的指针
int __flags;
int __size;
int age;
};
// 此时age = 20,在源码中是,第一个age(__Block_byref_age_1)
// 相当于age.age,将值赋值给age对象中的age。
// age.age指向的地址没有改变
(age.__forwarding->age) = 20;
// 所以打印输出自然是20
2021-05-30 22:03:02.218831+0800 Strong&Weak[7264:480507] age is 20
__block不能修饰全局变量、静态变量(static)


__block的内存管理
当block在栈上时,并不会对__block变量产生强引用
当block被copy到堆时
- 会调用
block内部的copy函数 copy函数内部会调用_Block_object_assign函数_Block_object_assign函数会对__block变量形成强引用(retain)
当block从堆中移除时
- 会调用
block内部的dispose函数 dispose函数内部会调用_Block_object_dispose函数_Block_object_dispose函数会自动释放引用的__block变量(release)
__block的__forwarding指针

循环引用
循环引用的产生
对象持有block,而block持有对象就会造成,双方都无法释放,导致循环应用。



对象持有block,block持有__block变量,__block持有对象也会造成循环引用


循环引用的解决
用__weak、__unsafe_unretained解决



用__block解决(必须要调用block)


解决循环引用问题 – MRC

