一、Block 的本质
block 的本质是一个OC对象。
看下面一段代码:
int main(int argc, const char *argv[])
{
@autoreleasepool {
void (^ aBlock)(void) = ^{
NSLog(@"this is a block");
};
aBlock();
}
return 0;
}
复制代码
- 定义一个
block,在block中执行一句代码,之后调用block - 我们通过
clang编译器来查看一下编译后block的样子:
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;
}
};
// 封装了 block 执行的代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5k_slnd0jd17fsb0qlp4fp4w09m0000gn_T_main_91fc10_mi_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) };
// main 函数
int main(int argc, const char *argv[])
{
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (* aBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)aBlock)->FuncPtr)((__block_impl *)aBlock);
}
return 0;
}
复制代码
-
编译后的
main函数,首先是调用了__main_block_impl_0函数,并传入了2个参数,分别为__main_block_func_0和&__main_block_desc_0_DATA -
__main_block_impl_0函数是__main_block_impl_0结构体的构造函数,第一个参数接收的是一个指针,指针指向的是__main_block_func_0函数,该函数中封装了block中的代码。第二个参数传入的是block的一些描述包含block占用的内存大小信息 -
__main_block_impl_0函数因为是一个构造函数,返回结果就是__main_block_impl_0结构体的变量,在构造函数中给__block_impl结构体成员进行赋值,将函数地址复制给了FuncPtr指针。 -
__main_block_impl_0结构体的就是block的本质,它有一个结构体成员__block_impl,在__block_impl结构体里面又包含了isa指针,从这方面说明了block的本质是一个OC对象,isa指针指向了该block的类,__block_impl结构体里还包含了FuncPtr指针,在给结构体赋值的时候,该指针指向了__main_block_func_0函数,在调用block的时候,通过该指针进行函数调用。
上面情况中的 block 没有参数也没有返回值,如果在 block 中传递参数,那么 block 的结构又是什么样的呢?
void (^ aBlock)(int, int) = ^(int a, int b){
NSLog(@"a is %d, b is %d", a, b);
};
aBlock(10, 20);
复制代码
继续通过 clang 编译器查看一下生产的cpp代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5k_slnd0jd17fsb0qlp4fp4w09m0000gn_T_main_c68cfa_mi_0, a, b);
}
复制代码
- 只是在
__main_block_func_0函数中传入了2个参数,block的结构体并没有发生任何变化。
二、Block 的变量捕获
2.1 局部变量的捕获(auto变量)
请问,下面的代码打印结果是什么?
int main(int argc, const char *argv[])
{
@autoreleasepool {
int num = 6;
void (^ aBlock)(void) = ^{
NSLog(@"num is %d", num);
};
num = 66;
aBlock();
}
return 0;
}
复制代码
- 答案是 num is 6
- 通过上面的打印结果会产生一个疑问,为啥我先修改了
num的值再调用block而打印结果是 num is 6。接下来就来研究一下:
还是通过 clang 编译器生成 cpp 代码来寻找答案
首先观察 main 函数
int main(int argc, const char *argv[])
{
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int num = 6;
void (* aBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
num = 66;
((void (*)(__block_impl *))((__block_impl *)aBlock)->FuncPtr)((__block_impl *)aBlock);
}
return 0;
}
复制代码
__main_block_impl_0函数除了和之前一样传递了2个参数外,还传递了一个参数就是num。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
- 在
block的结构体中多出了一个成员变量num,刚才传递的参数num的值6被赋值给了该结构体。
接下来我们修改 num 的值,改成了66。我们只是改变了局部变量 num 的值并没有修改 block 结构体中 num 的值
调用 block,本质就是通过 FuncPtr 指针来调用 __main_block_func_0 函数:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5k_slnd0jd17fsb0qlp4fp4w09m0000gn_T_main_dbf0b3_mi_0, num);
}
复制代码
- 该函数中通过
struct __main_block_impl_0 *__cself函数指针取出里面的成员num的值,最后进行打印,所以打印的结果是6。
综合以上分析,可以得出一个结论:block 会将局部变量捕获到自己的内部,捕获的是局部变量的值。
2.2 局部变量的捕获(static变量)
继续看下面的代码的打印结果是什么?
int main(int argc, const char *argv[])
{
@autoreleasepool {
static int num = 10;
void (^ aBlock)(void) = ^{
NSLog(@"num is %d", num);
};
num = 20;
aBlock();
}
return 0;
}
复制代码
答案是 num is 20。
继续查看编译后的 cpp 代码,首先看 main 函数:
int main(int argc, const char *argv[])
{
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static int num = 10;
void (* aBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));
num = 20;
((void (*)(__block_impl *))((__block_impl *)aBlock)->FuncPtr)((__block_impl *)aBlock);
}
return 0;
}
复制代码
- 这次给
__main_block_impl_0函数传递的第三个参数传递的是num的地址。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
__main_block_impl_0结构体成员中多的是num的指针。
再观察一下 block 的调用:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5k_slnd0jd17fsb0qlp4fp4w09m0000gn_T_main_a3338a_mi_0, (*num));
}
复制代码
- 获取到
num的指针之后,在调用阶段通过*num获取指针所指向内存地址中的值。
通过以上分析可知 num 的值发生改变是理所应当的了。
结论:无论是被 static 修饰的局部变量还是默认被 auto 修饰的局部变量都会被 block 捕获,只不过 static 修饰的局部变量是地址传递,auto 修饰的局部变量是值传递。
2.3 全局变量
运行下面代码的打印结果是什么?
int num = 10;
static int num2 = 10;
int main(int argc, const char *argv[])
{
@autoreleasepool {
void (^ aBlock)(void) = ^{
NSLog(@"num is %d , num2 is %d", num, num2);
};
num = 20;
num2 = 20;
aBlock();
}
return 0;
}
复制代码
- 这次,我们将
block中用到的变量换成了全局变量,打印结果都是20
继续查看一下本质:
int num = 10;
static int num2 = 10;
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) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5k_slnd0jd17fsb0qlp4fp4w09m0000gn_T_main_9bca35_mi_0, num, num2);
}
复制代码
- 在
block的结构体中并没有捕获全局变量,在block执行的代码中直接访问全局变量,因为全局变量的内存会一直存在,直接获取全局变量的值直接访问就可以了。
三、Block的类型
我们已经知道 block 的本质是一个OC对象,那么一个OC对象是一定有他所属的类型的。block 也不例外,看下面的代码打印结果是什么?
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^aBlock)(void) = ^{
NSLog(@"this is a block");
};
NSLog(@"%@", [aBlock class]);
NSLog(@"%@", [[aBlock class] superclass]);
NSLog(@"%@", [[[aBlock class] superclass] superclass]);
NSLog(@"%@", [[[[aBlock class] superclass] superclass] superclass]);
}
return 0;
}
复制代码
- 打印结果依次为:
__NSGlobalBlock__=>__NSGlobalBlock=>NSBlock=>NSObject - 通过一层层的调用,我们最终发现一个
block最终是继承NSObject的,所以从侧面也可以证明block本质是一个 OC 对象。
block是有3种类型的,除去上面我们打印的 __NSGlobalBlock__类型,还有2种类型,分别为 __NSStackBlock__ 和 __NSMallocBlock__。
在研究 block 的类型之前,我们需要先将项目调整为 MRC 环境,这样才可以真正研究出 block 的类型,因为 ARC 环境下,编译器会默默为我们做一些事情。
3.1 _NSGlobalBlock_
MRC环境下,代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^ aBlock) (void) = ^{
NSLog(@"this is a block");
};
NSLog(@"%@", [aBlock class]);
}
return 0;
}
复制代码
- 在
block中没有捕获auto修饰的局部变量,那么它的类型就是__NSGlobalBlock__ - 该
block是存储在内存中的数据段的。
3.2 _NSStackBlock_
MRC环境下,代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int num = 10;
void (^aBlock)(void) = ^{
NSLog(@"num is %d", num);
};
NSLog(@"%@", [aBlock class]);
}
return 0;
}
复制代码
block捕获auto修饰的局部变量,此时它的类型就是__NSStackBlock__。- 该
block是存储在内存中的栈区的,既然存储在栈区,说明它的内存创建和销毁不是程序员控制的,所以如果我们在它的作用域外再去使用它就会出现问题
void (^aBlock)(void);
void test() {
int num = 10;
aBlock = ^{
NSLog(@"num is %d", num);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
aBlock();
}
return 0;
}
复制代码
- 在
main函数中先调用test函数创建了block并且block捕获了局部变量,之后调用block来查看一下打印结果:num is -272632744,这并不是我们想要的结果。这是什么原因呢? - 因为该
block是在栈区的,一开始确实捕获了num的值存在了block里,随着test函数执行完毕block也在栈区被销毁,里面的成员num已经被赋值给垃圾数据了。所以当我们再通过全局的变量aBlock调用该block的时候打印的就是垃圾数据,没有任何意义了。
如何来保住 block 的命呢?那就需要把 block 移动到堆区,由程序员来控制其什么时候销毁。
3.3 _NSMallocBlock_
MRC环境下,代码如下:
void (^aBlock)(void);
void test() {
int num = 10;
aBlock = [^{
NSLog(@"num is %d", num);
} copy];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
aBlock();
}
return 0;
}
复制代码
- 此时打印的结果就是:num is 10
- 在
test函数中对block进行copy操作,那么此时的block类型就是__NSMallocBlock__ __NSMallocBlock__在内存中处于堆区。- 如果我们对
__NSMallocBlock__再进行copy操作呢?它还是在堆区,只不过引用计数会进行+1的操作
我们在 MRC 的环境下,探究了 block 的类型。我们平时的开发都在 ARC 环境下,ARC 环境下编译器会在某些时刻自动为 block 进行 copy 操作。
block作为返回值并且block内部捕获了auto修饰的变量。
typedef void(^LLBlock)(void);
LLBlock testBlock() {
int num = 10;
return ^{
NSLog(@"num is %d", num);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
LLBlock aBlock = testBlock();
NSLog(@"%@", [aBlock class]);
}
return 0;
}
// __NSGlobalBlock__
复制代码
block被强指针引用并且block内部捕获了auto修饰的变量
typedef void(^LLBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int num = 20;
LLBlock aBlock = ^{
NSLog(@"num is %d", num);
};
NSLog(@"%@", [aBlock class]); // __NSGlobalBlock__
// 没有被强指针引用的 block
NSLog(@"%@", [^{
NSLog(@"num is %d", num);
} class]); // __NSStackBlock__
}
return 0;
}
复制代码
GCDAPI中使用到的block和CocoaAPI中带有UsingBlock字样的blockenumerateObjectsUsingBlockvoid dispatch_async(dispatch_queue_t queue, dispatch_block_t block);等
四、对象类型的auto变量内存管理
在之前我们探究过 block 针对 auto 修饰的局部变量的捕获问题,那时我们定义变量使用的是基本数据类型,接下来我们研究一下定义对象类型 block 是如何进行内存管理的。
首先看下面的代码:
// Person.m
@implementation Person
- (void)dealloc {
NSLog(@"person - dealloc");
}
@end
// main.m
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myBlock;
{
Person *person = [[Person alloc] init];
myBlock = ^{
NSLog(@"person is %@", person);
};
}
myBlock();
}
return 0;
}
// 打印结果:
person is <Person: 0x10288f260>
person - dealloc
复制代码
- 创建一个
Person类,并重写它的dealloc方法,目的是监控Person对象什么时候销毁。 - 在
main函数中,使用{}代码块生成局部作用域,当{}的代码执行完毕后,来监控person对象是否会销毁。 - 因为使用
myBlock强引用着block对象,在ARC环境下编译器会对block自动进行copy操作,所以此时block的处于堆区,在block中同时捕获了auto修饰的person对象,所以会对person对象的引用计数+1,保证person对象在{}执行完毕后不会销毁。
通过 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m 命令查看一下编译器生成的 c++ 代码。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
block内部捕获了person对象而且是一个强引用
再观察一下 desc 这个结构体
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 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};
复制代码
- 该结构体多出了2个函数指针。分别调用是
__main_block_copy_0和__main_block_dispose_0两个函数 - 在
__main_block_copy_0函数中,调用了_Block_object_assign函数处理对象引用计数的增加 - 在
__main_block_dispose_0函数中,调用了_Block_object_dispose函数处理对象引用计数的减少
如果我们对上面的代码进行一下改造:
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myBlock;
{
Person *person = [[Person alloc] init];
// 将 person 对象修改为弱引用
__weak Person *weakPerson = person;
myBlock = ^{
NSLog(@"person is %@", weakPerson);
};
}
myBlock();
}
return 0;
}
// 打印结果:
person - dealloc
person is (null)
复制代码
- 使用弱指针指向
person对象并在block中调用。 - 此时,
block虽然捕获了person对象,但是并没有使person对象的引用计数+1,所以当{}代码执行完毕后,person对象就先销毁了,之后调用block打印结果person就是空了。 - 此时,
__main_block_impl_0中person指针是弱引用的。
总结来说:在 ARC 环境下,在堆区存在的 block 会对 auto 修饰的对象类型默认进行一个强引用,目的是在 block 内部用到 person 对象时保证 person 对象不被销毁。如果我们不希望 block 内部对对象类型进行强应用,可以用弱指针指向该对象。
如果 block 在栈区呢?
如果 block 在栈区,block 的内存就不再受到我们程序员的控制,此时我们就需要对 block 进行 copy 操作,把它复制到堆区,这样 block 内部捕获的对象类型变量才不会被销毁。如果不对 block 进行 copy 操作,不会对 block 内部捕获的对象类型进行引用计数+1的操作。
五、__block
提问:是否可以修改 block 中使用到变量的值?如果可以,都有哪几种方式?
-
将变量定义为
static类型 -
定义一个全局变量
-
使用
__block的方式
由于前2种方式会导致变量的内存一直不会销毁,所以通常开发中使用 __block 修饰变量,在 block 中对变量进行修改
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int num = 10;
void (^block)(void) = ^{
num = 20;
NSLog(@"num is %d", num);
};
block();
}
return 0;
}
// 打印结果
num is 20
复制代码
还是通过 clang 编译器来查看一下编译后的 C++ 代码
首先看 main 函数:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
复制代码
- 变量加上
__block之后,定义的变量的类型不是简单的int类型了,而是__Block_byref_num_0类型。在__Block_byref_num_0中的传值过程中,第2个参数将num的地址传递了进去,第4个参数才是num的值。
struct __Block_byref_num_0 {
void *__isa;
__Block_byref_num_0 *__forwarding;
int __flags;
int __size;
int num;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_num_0 *num; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
__Block_byref_num_0成员中包含了isa和它同样类型的__forwarding指针,最后1个参数才是用来存储num变量的成员,有isa指针说明它是一个对象,__forwarding指针用来指向它自己。- 再看
block的结构,不再直接保存num而是 保存了__Block_byref_num_0指针,指向__Block_byref_num_0结构体。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_num_0 *num = __cself->num; // bound by ref
(num->__forwarding->num) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5k_slnd0jd17fsb0qlp4fp4w09m0000gn_T_main_f080da_mi_0, (num->__forwarding->num));
}
复制代码
block的调用本质是调用该函数,我们可以看到block通过__Block_byref_num_0指针找到__forwarding找到num最终修改num的值。
通过上面的代码分析,我们发现在 auto 修饰的局部变量上加上了 __block 本质是把该变量包装成了一个对象,通过该对象来修改 num 的值。
在 ARC 的环境下,block 捕获auto修饰的局部变量是通过 copy 复制到了堆区。因为 __block 会将变量包装成 __Block_byref_num_0 对象,__Block_byref_num_0 也会被拷贝到堆区,那么肯定要进行内存管理,内存管理就用到了 __main_block_desc_0 结构体中的 __main_block_copy_0 函数 和 __main_block_dispose_0 函数。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
复制代码
__main_block_copy_0函数内部调用了_Block_object_assign函数来对num进行强引用- 当
block被销毁的时候又会调用__main_block_dispose_0函数中的_Block_object_dispose函数,对num进行销毁操作
接下来,我们研究一下更复杂的情况,__block 修饰对象类型。先看下面的代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSObject *obj = [[NSObject alloc] init];
void (^block) (void) = ^{
NSLog(@"%@", obj);
};
block();
}
return 0;
}
复制代码
使用 clang 编译器来生成对应的 C++ 代码:
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 *__strong obj;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_obj_0 *obj; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_obj_0 *_obj, int flags=0) : obj(_obj->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
- 先看
block的结构体,被__block修饰的对象类型也会被包装成一个对象类型即__Block_byref_obj_0 __Block_byref_obj_0的obj指针就指向了__Block_byref_obj_0结构体,在结构体中有一个强引用的obj指针指向NSObject对象
由于被包装成了一个对象,那么一定会涉及内存的管理:
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};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}
复制代码
__Block_byref_obj_0对象的内存通过__main_block_desc_0结构体中的__main_block_impl_0函数和__main_block_impl_0进行管理- 当
block被拷贝到堆区的时候,调用__main_block_copy_0函数中的_Block_object_assign函数进行强引用 - 当
block被销毁的时候,调用__main_block_dispose_0函数中的_Block_object_dispose函数进行对象的销毁
我们再看 __Block_byref_obj_0 结构体中 也存在2个函数,分别为 __Block_byref_id_object_copy 和 __Block_byref_id_object_dispose
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_id_object_copy_131函数会根据__Block_byref_obj_0中obj的指针是强指针还是弱指针来对obj对象强引用和弱引用。__Block_byref_id_object_dispose_131函数会在__Block_byref_obj_0对象销毁时来销毁obj对象。
注意一种情况,我们上面研究的都是 ARC 环境下的情况,如果在 MRC 环境下,即使我们主动对 block 对象进行copy操作,使用 __block 修饰的对象类型,在 block 内部也不会被强引用
六、循环引用
在使用 block 的时候经常遇到的问题就是循环引用问题从而导致内存泄漏。看下面的代码:
// Person.h
typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, copy) MyBlock block;
@end
// Person.m
@implementation Person
- (void)dealloc {
NSLog(@"Person 对象销毁");
}
@end
// main 函数
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.block = ^{
NSLog(@"%p", person);
};
person.block();
}
return 0;
}
复制代码
- 定义一个
Person类,并定义一个block属性,在person.m文件中通过dealloc方法来观察person对象是否销毁 - 在
main函数中,让person的block属性强引用着一个block,在block内部调用person对象,block就会捕获person对象并强引用着它。 - 当
main函数执行完毕,person对象不会被销毁,就因为block和person的相互引用导致的循环引用发生了内存泄漏,person对象没有被销毁。
该如何解决循环引用呢?下面使用3种方式来解决
__weak
循环引用是因为 block 和 person 对象之间的强引用导致的,可以使用 __weak 来把其中一个引用换成弱引用即可
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
__weak typeof(person) weakPerson = person;
person.block = ^{
NSLog(@"%p", weakPerson);
};
person.block();
}
return 0;
}
复制代码
- 当
main函数执行完毕,person对象也会被销毁 - 当
person对象被销毁后,使用__weak修饰的对象会指向nil
_unsafe_unretained
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
__unsafe_unretained typeof(person) weakPerson = person;
person.block = ^{
NSLog(@"%p", weakPerson);
};
person.block();
}
return 0;
}
复制代码
- 使用
_unsafe_unretained和__weak一样不会强引用对象,但是当对象销毁后,它仍然保存着该对象的地址,如果我们再次访问该对象会产生野指针。
__block
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
person.block = ^{
NSLog(@"%p", person);
person = nil;
};
person.block();
}
return 0;
}
复制代码
- 通过
__block解决循环引用必须要调用block,并且在block的内部必须将person对象置为nil。 - 由于
__block本质会重新包装一个对象,该对象强引用着person对象,person对象强引用着block,block又强引用着该对象,他们三个形成互相引用的状态。想要解决循环引用就主动把person对象置为nil来破坏之间的引用关系。





















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)