面试题引发的思考:
Q: block的属性修饰词为什么是copy
?
- 如果block是在栈上,将不会对
auto
变量产生强引用; -
如果block被
copy
到堆上:
1> 会调用block内部的copy
函数;
2>copy
函数内部会调用_Block_object_assign
函数;
3>_Block_object_assign
函数会根据auto
变量的修饰符(__strong
,_weak
)做出相应的操作,形成强引用、弱引用(仅限于ARC时会retain
,MRC时不会)。 -
如果block从堆上移除:
1> 会调用block内部的dispose
函数;
2>dispose
函数内部会调用_Block_object_dispose
函数;
3>_Block_object_dispose
函数会自动释放引用的auto
变量,类似于release
。
对象类型的auto
变量
iOS底层原理 - 探寻block本质(一)中介绍到block底层原理以及block的变量捕获,那么block对对象类型变量的捕获同对基本数据类型变量的捕获是否相同?
(1) 栈空间、堆空间的强引用、弱引用
Q: 首先查看一下代码,block访问对象类型变量时,对象何时销毁?
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
- (void)dealloc {
NSLog(@"------------ Person - dealloc");
}
@end
// TODO: ----------------- main -----------------
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
Person *person = [[Person alloc] init];
person.age = 10;
block = ^{
NSLog(@"------------ 内部 %d", person.age);
};
} // 执行完毕,person没有被释放
NSLog(@"------------ 外部");
}
return 0;
}
// 打印结果
Demo[1234:567890] ------------ 外部
Demo[1234:567890] ------------ Person - dealloc
由打印结果可知:
大括号执行完毕,person
没有被释放。
而person
对象是在大括号内声明的局部变量,它的生命周期仅限于这个大括号内,打印结果是什么原因造成的?
上篇文章介绍到:person
是auto
变量,person
会被捕获到block内部,即block对person
进行强引用; 那么block销毁之前,person
是不会被销毁的。
查看源码,符合我们的结论。
下面我们进入MRC环境:
// MRC环境
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
Person *person = [[Person alloc] init];
person.age = 10;
block = ^{
NSLog(@"------------ 内部 %d", person.age);
};
[person release];
} // 执行完毕,person被释放
NSLog(@"------------ 外部");
}
return 0;
}
// 打印结果
Demo[1234:567890] ------------ Person - dealloc
Demo[1234:567890] ------------ 外部
由打印结果可知:
大括号执行完毕,person
被释放。
原因是:在MRC环境下,block访问auto
变量,会在栈空间,不会对person
进行强引用。
对block进行copy
操作,person
没有被释放
// MRC环境
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
Person *person = [[Person alloc] init];
person.age = 10;
block = [^{
NSLog(@"------------ 内部 %d", person.age);
} copy];
[person release];
}
// 执行完毕,person没有被释放
NSLog(@"------------ 外部");
// 需要block销毁掉,person才会被释放
[block release];
}
return 0;
}
// 打印结果
Demo[1234:567890] ------------ 外部
Demo[1234:567890] ------------ Person - dealloc
由打印结果可知:
大括号执行完毕,person
没有被释放。
原因是:对栈空间的block进行copy
操作,将栈空间的block拷贝到堆中,会对person
进行强引用;
堆空间的block可能会对person
进行一次retain
操作,保证person
不被销毁,堆空间的block销毁之后会对person
进行release
操作。
总结可知:堆区的block对person
对象有强引用作用,栈空间的block对person
对象没有强引用作用。
(2) 关键字__weak
切回ARC环境,执行下列代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
Person *person = [[Person alloc] init];
person.age = 10;
__weak Person *weakPerson = person;
block = ^{
NSLog(@"------------ 内部 %d", weakPerson.age);
};
}
// 执行完毕,person没有被释放
NSLog(@"------------ 外部");
}
return 0;
}
// 打印结果
Demo[1234:567890] ------------ Person - dealloc
Demo[1234:567890] ------------ 外部
将代码转化成C++,_weak
修饰变量,需要告知编译器使用ARC环境及版本号:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
由源码可知:_weak
修饰的变量,在生成的__main_block_impl_0
中也是使用_weak
修饰。
所以对person
对象是弱引用,不能改变person
对象的生命周期。
(3) _main_block_copy_0
函数和 __main_block_dispose_0
函数
block捕获对象类型的变量时__main_block_impl_0
内部结构体__main_block_desc_0
中多了copy
函数和dispose
函数两个参数:
copy
函数和dispose
函数中传入的都是__main_block_impl_0
结构体本身。
a> copy
函数本质是__main_block_copy_0
函数,其内部调用_Block_object_assign
函数;
_Block_object_assign
中传入的是person
对象的地址,person
对象,以及8
。
b> dispose
函数本质是__main_block_dispose_0
函数,其内部调用_Block_object_dispose
函数;
_Block_object_dispose
函数传入的参数是person
对象,以及8
。
1> _Block_object_assign
函数
block进行copy
操作的时候就会自动调用__main_block_desc_0
内部的__main_block_copy_0
函数;
__main_block_copy_0
函数内部会调用_Block_object_assign
函数;
_Block_object_assign
函数会自动根据__main_block_impl_0
结构体内部的person
的指针类型,对person
对象产生强引用或者弱引用。
2> _Block_object_dispose
函数
当block从堆中移除时就会自动调用__main_block_desc_0
中的__main_block_dispose_0
函数;
__main_block_dispose_0
函数内部会调用_Block_object_dispose
函数;
_Block_object_dispose
会对person
对象做释放操作,类似于release
,即断开对person
对象的引用,而person
究竟是否被释放还是取决于person
对象自己的引用计数。
总结可知:
当block内部访问了对象类型的
auto
变量时:
- 如果block是在栈上,将不会对
auto
变量产生强引用;- 如果block被拷贝到堆上:
1> 会调用block内部的copy
函数;
2>copy
函数内部会调用_Block_object_assign
函数;
3>_Block_object_assign
函数会根据auto
变量的修饰符(__strong
,_weak
)做出相应的操作,形成强引用,弱引用。- 如果block从堆上移除:
1> 会调用block内部的dispose
函数;
2>dispose
函数内部会调用_Block_object_dispose
函数;
3>_Block_object_dispose
函数会自动释放引用的auto
变量,类似于release
。
(4) 以下几个示例中person
都在何时销毁?
1> 示例一:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
Person *person = [[Person alloc] init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"------------ %@", person);
});
NSLog(@"------------ touchesBegan");
}
打印结果显示:block执行完毕后person
对象销毁。
由iOS底层原理 - 探寻block本质(一)可知:
ARC环境中,block作为GCD API的方法参数时会自动进行copy
操作,将block复制到堆上,所以block内部copy
函数会对person
进行强引用。
当block执行完毕需要销毁时,调用dispose
函数释放对person
对象的引用,person
没有强指针引用时会被销毁。
2> 示例二:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
Person *person = [[Person alloc] init];
__weak Person *weakPerson = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"------------ %@", weakPerson);
});
NSLog(@"------------ touchesBegan");
}
打印结果显示:person
对象先销毁,然后执行block,打印为null
。
block对weakPerson
为__weak
弱引用,所以block内部copy
函数会对person
进行弱引用。
当touchesBegan: withEvent:
执行完毕时,person
没有强指针引用时会被销毁,所以block执行时打印null
。
3> 示例三:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
Person *person = [[Person alloc] init];
__weak Person *weakPerson = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"------------1 %@", person);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"------------2 %@", weakPerson);
});
});
NSLog(@"------------ touchesBegan");
}
打印结果显示:外层block执行完毕后,即1秒后person
对象销毁,然后执行内层block,即3秒后打印为null
。
外层block对person
进行强引用,内层block对person
进行弱引用。
当外层block执行完毕时,person
有强指针引用;然后内层block执行时person
没有强指针会被销毁,所以内层block执行时打印null
。
4> 示例四:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
Person *person = [[Person alloc] init];
__weak Person *weakPerson = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"------------1 %@", weakPerson);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"------------2 %@", person);
});
});
NSLog(@"------------ touchesBegan");
}
打印结果显示:外层block执行完毕后,然后执行内层block,3秒后person
对象销毁。
外层block对person
进行弱引用,内层block对person
进行强引用。
而person
的强引用何时结束,person
何时dealloc
,所以3秒后person
对象才销毁。