面试题引发的思考:
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
产生强引用,所以也可以解决循环引用问题。