面试题引发的思考:
Q: __block的作用是什么?有什么使用注意点?
__block用于解决block内部无法修改auto变量值的问题;__block不能修饰全局变量、静态变量。
Q: block内部修改的NSMutableArray,是否需要添加__block?
- 因为block内部只是使用了
array的内存地址添加数据,并没有修改array的内存地址,所以array不需要__block修饰; - 添加
__block修饰符之后,系统会创建相应的结构体,占用一定的内存空间;所以要根据相应情况添加__block修饰符,避免内存浪费。
Q: 使用block有那些注意事项?
- 注意循环引用问题。
iOS底层原理 - 探寻block本质(二)中介绍到block对对象类型的变量捕获,以及对象的销毁时机。
下面介绍如何实现在block内部修改变量的值。
1. 修饰符__block
需要在block内部修改变量的值,代码如下:
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
Block block = ^{
age = 20; // error
};
block();
}
return 0;
}
以上代码会出现编译错误。

通过源码可知:
age是在main函数内部声明的变量,存在于main函数的栈空间内部;
block内部实现是__main_block_func_0函数,其内部捕获age,新增一个参数存储外部的age变量的值,这个age存在于block的栈空间内部;
所以block内部无法修改main函数内部的auto变量。
Q: 那么该如何在block内部修改变量的值呢?
1> 方法一:使用static变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
static int age = 10;
Block block = ^{
age = 20;
NSLog(@"------------ %d", age);
// 打印结果:------------ 20
};
block();
}
return 0;
}
由前文可知:局部变量都会被block捕获,auto变量值传递,static变量指针传递。
block内部会新增一个参数存储age的指针,通过指针访问age变量的内存地址,就可以修改age的值。
2> 方法二:使用全局变量
全局变量在哪里都可以访问,所以block不用捕获全局变量,直接进行访问。
以上两种方法会使变量一直存在于内存中,占用内存地址。我们可以使用__block修饰符来解决这个问题。
3> 方法三:使用__block修饰变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
Block block = ^{
age = 20;
NSLog(@"------------ %d", age);
// 打印结果:------------ 20
};
block();
}
return 0;
}
转化成C++源码:

由源码可知:
存值时:
被
__block修饰的age变量会在block内部转化成名为age的__Block_byref_age_0结构体,结构体包括:
__isa:说明__Block_byref_age_0本质也是对象__flags:赋值为0__forwarding:指向结构体自身的指针__size:占用的内存空间age:存储变量然后
__Block_byref_age_0结构体age存储在结构体__main_block_impl_0中。取值时:
- 通过
_cself->age将age赋值给__Block_byref_age_0结构体;age->__forwarding->age通过结构体指针访问成员变量来改变成员变量的值;- 通过
age->__forwarding->age进行取值。
4> Q: 以下代码是否可以正确执行?
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [NSMutableArray array];
Block block = ^{
[array addObject: @"1"];
[array addObject: @"2"];
NSLog(@"array - %@", array);
};
block();
}
return 0;
}
- 可以正确执行。
- 因为block内部只是使用了
array的内存地址添加数据,并没有修改array的内存地址; - 所以
array不需要__block修饰; - 添加
__block修饰符之后,系统会创建相应的结构体,占用一定的内存空间;所以要根据相应情况添加__block修饰符,避免内存浪费。
2. __block内存管理
(1) 分析一下代码:
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
__block int weight = 60;
NSObject *object = [[NSObject alloc] init];
__weak NSObject *weakObject = object;
__block NSObject *blockObject = object;
__block __weak NSObject *blockWeakObject = object;
Block block = ^{
NSLog(@"%d", age); // 局部变量
NSLog(@"%d", weight); // __block修饰的局部变量
NSLog(@"%p", object); // 对象类型的局部变量
NSLog(@"%p", weakObject); // __weak修饰的对象类型的局部变量
NSLog(@"%p", blockObject); // __block修饰的对象类型的局部变量
NSLog(@"%p", blockWeakObject); // __block、__weak修饰的对象类型的局部变量
};
block();
}
return 0;
}
使用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m将代码转化成C++语言:

由__main_block_impl_0函数可知:
- 未使用
__block修饰的变量(object、weakObject),根据被block捕获的指针类型进行强引用或弱引用; - 使用
__block修饰的变量(weight、blockObject、blockWeakObject),都是使用强指针引用生成的结构体。

由以上__block修饰的对象类型的变量生成的结构体可知:
- 其内部多了
__Block_byref_id_object_copy和__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理操作。 -
block捕获的对象类型决定结构体对象的引用类型:
a>blockObject是强指针,所以__Block_byref_ blockObject_1对blockObject就是强引用;
b>blockWeakObject是弱指针,所以__Block_byref_ blockWeakObject_1对blockWeakObject就是弱引用。

由以上C++代码可知:
-
__main_block_copy_0函数根据变量的强弱指针及是否被__block修饰做出不同处理:
a> 强指针在block内部产生强引用;
b> 弱指针在block内部产生弱引用;
c> 被__block修饰的变量最后的参数传入的是8;
d> 没有被__block修饰的变量最后的参数传入的是3。 __main_block_dispose_0函数会在block从堆中移除时释放这些变量。
(2) 总结:

由block复制到堆上的内存变化图可知:
- 将block复制到堆上时,block内部引用的
__block变量也被复制到堆上,此时block持有__block变量; - 若将block复制到堆上时,
__block变量已经在堆上,则不会再次将其复制到堆上。

由block从堆上移除的内存变化图可知:
- 将block从堆中移除时,若有别的block持有
__block变量,则不会将__block变量移除; - 将所有的block从堆中移除时,此时没有block持有
__block变量,__block变量被移除。
(3) __forwarding指针
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
Block block = ^{
age = 20;
};
block();
}
return 0;
}
将代码转化成C++语言:

由以上代码可知:
- 当block在栈上时,栈上的
__Block_byref_age_0结构体内部__forwarding指针指向结构体自己; - 当block复制到堆上时,栈上的
__Block_byref_age_0结构体也会被复制到堆上,此时栈上的__Block_byref_age_0结构体内部__forwarding指针指向的是堆中的__Block_byref_age_0结构体,堆中__Block_byref_age_0结构体内的__forwarding指针依然指向自己。
以上结论可由下图展示:

3. __block修饰的对象类型的内存管理
(1) __block修饰对象类型
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
// __block __weak Person *person = [[Person alloc] init];
Block block = ^ {
NSLog(@"%p", person);
};
block();
}
return 0;
}
将代码转化成C++语言:

由以上C++代码可知:
__Block_byref_person_0内部多了__Block_byref_id_object_copy和__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理操作:
对于__Block_byref_id_object_copy函数:
a> __Block_byref_id_object_copy函数赋值为__Block_byref_id_object_copy_131函数;
b> __Block_byref_id_object_copy_131函数调用_Block_object_assign函数;
c> _Block_object_assign函数内部拿到dst指针(block对象的地址值)加上40个字节,即为person指针。
也就是说__Block_byref_id_object_copy函数会将person地址传入_Block_object_assign函数,_Block_object_assign中对Person对象进行强引用或者弱引用。】
__Block_byref_id_object_copy函数同理。
(2) __block、__weak同时修饰对象类型
使用__block、__weak同时修饰变量同理。
即block内部对__block修饰变量生成的结构体都是强引用;
结构体内部对外部变量的引用取决于传入block内部的变量是强引用还是弱引用。
(3) 总结
当block在栈上时,不会对
__block变量产生强引用;当block被
copy到堆时:
a> 会调用block内部的copy函数;
b>copy函数内部会调用_Block_object_assign函数;
c>_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)。当block从堆中移除时:
a> 会调用block内部的dispose函数;
b>dispose函数内部会调用_Block_object_dispose函数;
c>_Block_object_dispose函数会自动释放指向的对象(release)。
4. 循环引用
(1) 循环引用原理
// TODO: ----------------- Person类 -----------------
typedef void (^Block)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) Block myBlock;
@end
@implementation Person
- (void)dealloc {
NSLog(@"------------ %s", __func__);
}
@end
// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 10;
person.myBlock = ^{
NSLog(@"%d", person.age);
};
}
NSLog(@"大括号已经结束");
return 0;
}
// 打印结果
Demo[1234:567890] 大括号已经结束
由打印结果可知:
大括号已经结束,person没有被释放,产生了循环引用。

循环引用原理如上图所示:
大括号结束后引用1被断开,引用2与引用3没有断开,形成循环引用,进而造成内存泄漏。
(2) 循环引用解决方法 - ARC
解决循环引用问题,还需要保证block在person销毁前不被销毁,解决方案是:
Person对block的引用(引用2)为强引用;block内部对Person的引用(引用3)为弱引用。
1> 使用__weak和__unsafe_unretained修饰符
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 10;
// __weak:不会产生强引用,指向对象销毁时,会自动让指针置为nil
// __unsafe_unretained:不会产生强引用,不安全,指向对象销毁时,指针存储的地址值不变
// __weak Person *weakPerson = person;
// __weak typeof(person) weakPerson = person;
__unsafe_unretained typeof(person) weakPerson = person;
person.myBlock = ^{
NSLog(@"age - %d", weakPerson.age);
};
}
NSLog(@"大括号已经结束");
return 0;
}
2> 使用__block修饰符
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
person.age = 10;
person.myBlock = ^{
NSLog(@"age - %d", person.age);
person = nil;
};
person.myBlock();
}
NSLog(@"大括号已经结束");
return 0;
}
使用__block修饰符打破循环引用原理如下:

由上文可知:
__block修饰person变量,会生成__Block_byref_person_0结构体,其内部包含的person对象才是block内部使用的变量。
那么将block内部的person置为nil,三角循环引用就会断开。
此方法要求执行block,并且在block内部将person对象置为nil。
(2) 循环引用解决方法 - MRC
1> 使用__unsafe_unretained修饰符
int main(int argc, const char * argv[]) {
@autoreleasepool {
__unsafe_unretained Person *person = [[Person alloc] init];
person.age = 10;
person.myBlock = [^{
NSLog(@"age - %d", person.age);
} copy];
[person release];
}
NSLog(@"大括号已经结束");
return 0;
}
MRC环境下不支持__weak修饰符,使用__unsafe_unretained修饰符原理同ARC环境下相同,不再赘述。
2> 使用__block修饰符
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
person.age = 10;
person.myBlock = [^{
NSLog(@"age - %d", person.age);
} copy];
[person release];
}
NSLog(@"大括号已经结束");
return 0;
}
由上文可知:
MRC环境下,当block被copy到堆时,__block结构体不会对person产生强引用,所以也可以解决循环引用问题。