Block内存管理
Block根据在内存的位置可以分为全局的Block,栈上的Block,还有堆上的Block:
- __NSConcretStackBlock
- __NSConcretGlobalBlock
- __NSConcretMallocBlock
ARC一般只有全局的Block和堆上的Block,栈上的Block在ARC下一般都会默认拷贝到堆上。 以下情况除外:
- Block作为方法或函数的参数传入时,Block还是在栈上的;
- (void)viewDidLoad {
[super viewDidLoad];
__block NSString *str = @"I am Jan!";
NSLog(@"Before the block string value is '%@' and address is %p",str,str);
[self testWithBlock:^{
str = @"My name is Jack!";
}];
NSLog(@"After the block string value is '%@' and address is %p",str,str);
}
- (void)testWithBlock:(void(^)())bl{
bl();
NSLog(@"Block invoked %@",bl);
}
输出结果如下:
Test[43208:1418384] Before the block string value is 'I am Jan!' and address is 0x100dd80e0
Test[43208:1418384] Block invoked <__NSStackBlock__: 0x7fff5ee27a68>
Test[43208:1418384] After the block string value is 'My name is Jack!' and address is 0x100dd8120
如果将block当作参数传入容器类的初始化函数中,这个block也会拷贝到了堆上,即该block的是__NSMallocBlock
NSMutableArray *arr = [[NSMutableArray alloc]initWithObjects:^{str = @"My name is Jack!";}, nil];
NSLog(@"blk is %@",[arr objectAtIndex:0]);
输出结果如下:
Test[43476:1427221] blk is <__NSMallocBlock__: 0x608000047170>
Block循环引用
1.内存
程序中栈内存是由系统负责管理申请和释放,堆内存是由程序负责申请和释放。不同语言内存管理是不一样的,objective-c内存管理是用引用计数来管理对象内存的。使用alloc/new/copy/mutableCopy方法返回的对象引用计数都会加1,在MRC下需要调用release方法释放这些方法返回的对象,在ARC下由ARC负责管理释放的。
NSMutableArray *array = [[NSMutableArray alloc]initWithObjects:@"a", nil];
NSMutableArray *arr1 = array;
NSMutableArray *arr2 = array;
NSLog(@"%p,%p,%p",&array,&arr1,&arr2);
NSLog(@"%p,%p,%p",array,arr1,arr2);
NSLog(@"%@,%@,%@",array,arr1,arr2);
该段代码输出如下:
Test[17150:2347822] 0x7fff525dfac8,0x7fff525dfac0,0x7fff525dfab8
Test[17150:2347822] 0x608000057c70,0x608000057c70,0x608000057c70
Test[17150:2347822] (
a
),(
a
),(
a
)
可见,array,arr1和arr2都是在栈上分配的内存,栈上的内存地址是由高地址向低地址,所以这三个变量的地址0x7fff525dfac8,0x7fff525dfac0,0x7fff525dfab8都是由高向低扩展。由于array指针指向了alloc方法申请的对象,然后赋值给了arr1和arr2,所以他们三个变量指向的地址都是0x608000057c70,在ARC下,该对象由ARC负责管理释放。
2.block循环引用
使用block一般都会遇到循环引用问题导致内存泄漏。如果block声明为对象obj的copy或者strong类型property变量,该block里面又使用到了obj对象,那就会造成循环引用内存泄漏问题。因为设置obj的block属性的时候,由于block里面使用到了obj对象,block会捕捉obj对象,并对obj进行retain操作,而block又是obj的copy或者strong(ARC下都一样)属性,即obj拥有block,block拥有obj导致循环引用,无法释放。
如:
typedef void (^proBlock)(void);
@interface SecViewController : UIViewController
@property (copy,nonatomic) proBlock block;
- (void)secTestWithBlock:(void(^)())blk;
@end
在SecViewController的viewdidload方法中执行该block:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.block();
}
当前界面中通过按钮跳转到第二个界面,代码如下:
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
secVC.block = ^{
[secVC secTestWithBlock:^{
NSLog(@"Print within block");
}];
};
[self presentViewController:secVC animated:YES completion:nil];
该代码就会造成block循环引用问题。编译器会给出⚠️capturing ‘secVC’ strongly in this block is likely to lead to a retain cycle.运行并从SecViewController返回当前界面的时候,控制台输出如下:
Test[17486:2361465] Print within block
可见SecViewController的
- (void)dealloc {
NSLog(@"Dealloc SecVC");
}
方法并没有执行,这是因为循环引用问题导致了内存无法释放。但是可以通过__weak声明变量来打破循环,代码修改如下:
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
__weak __typeof(secVC) weakSecVC = secVC;
secVC.block = ^{
[weakSecVC secTestWithBlock:^{
NSLog(@"Print within block");
}];
};
[self presentViewController:secVC animated:YES completion:nil];
控制台输出结果如下:
Test[17558:2364346] Print within block
Test[17558:2364346] Dealloc SecVC
由于block的执行时机并不确定,因为block捕获的变量是weak修饰的,当block执行的时候,有可能weakSecVC已经被释放了,所以会导致向nil对象发送消息,即block执行的时候[weakSecVC secTestWithBlock:]这个方法中weakSecVC已经被外面释放掉了。
如代码修改如下:
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
__weak __typeof(secVC) weakSecVC = secVC;
secVC.block = ^{
double delayInSeconds = 2.0;
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
[weakSecVC secTestWithBlock:^{
NSLog(@"Print within block");
}];
});
};
[self presentViewController:secVC animated:YES completion:nil];
控制台输出结果如下:
Test[17723:2370401] Dealloc SecVC
将block中的[weakSecVC secTestWithBlock:]这个方法延迟2秒执行,并立刻返回第一个页面,这个时候会发现dealloc方法在返回的时候就会执行了,而[weakSecVC secTestWithBlock:]这个方法并不会被执行,因为weakSecVC已经被释放掉了。
接下来将代码修改如下:
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
__weak __typeof(secVC) weakSecVC = secVC;
secVC.block = ^{
double delayInSeconds = 2.0;
SecViewController *strongSecVC = weakSecVC;
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
[strongSecVC secTestWithBlock:^{
NSLog(@"Print within block");
}];
});
};
[self presentViewController:secVC animated:YES completion:nil];
控制台输出结果如下:
Test[17654:2368051] Print within block
Test[17654:2368051] Dealloc SecVC
block代码中将捕获的weak变量weakSecVC进行了强引用,这个时候SecViewController在返回的时候并不会被释放,只有等block执行完成返回的时候才进行释放。注意的是block在声明赋值的时候(拷贝到堆上),不管block执行还是没执行,block就会对block使用的变量进行了捕获,如果变量是强引用的,block也就强引用了该变量,如果该变量也强引用了block,那就会导致循环引用问题。但是如果在block里面声明了一个变量强引用捕获的weak变量,那么该强引用只有在block执行的时候才会发生,并且在block执行完成返回的时候会进行释放,即该强引用只会在block执行期间生效,block执行完成就无效了,所以并不会导致循环引用。
在实际项目中由于项目的复杂性,在block中这句代码SecViewController *strongSecVC = weakSecVC;中,strongSecVC也有可能是nil的,所以代码应该优化成以下代码会比较好:
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
__weak __typeof(secVC) weakSecVC = secVC;
secVC.block = ^{
SecViewController *strongSecVC = weakSecVC;
if (strongSecVC) {
double delayInSeconds = 2.0;
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
[strongSecVC secTestWithBlock:^{
NSLog(@"Print within block");
}];
});
} else {
//block capture object is nil;
}
};
[self presentViewController:secVC animated:YES completion:nil];
其他情况说明:
- 在Block的外面将weak变量赋值给strong变量,block里面使用strong变量一样会导致循环引用问题,如:
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"]; __weak __typeof(secVC) weakSecVC = secVC; SecViewController *strSecVC = weakSecVC; secVC.block = ^{ [strSecVC secTestWithBlock:^{ NSLog(@"Print within block"); }]; }; [self presentViewController:secVC animated:YES completion:nil];因为经过以上赋值之后,strSecVC,weakSecVC和secVC变量都是指向了同一个在堆上申请的SecViewController对象地址,而strSecVC对该申请返回对象进行了强引用,所以导致循环引用。
- block里面通过赋值nil解决循环引用
如果将之前代码改成这样:UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"]; __weak __typeof(secVC) weakSecVC = secVC; __block SecViewController *strSecVC = weakSecVC; secVC.block = ^{ [strSecVC secTestWithBlock:^{ NSLog(@"Print within block"); }]; strSecVC = nil; }; [self presentViewController:secVC animated:YES completion:nil];输出结果如下:
Test[18929:2406918] Print within block Test[18929:2406918] Dealloc SecVC可见并没有发生循环引用,因为在block执行的时候,将strSecVC设置了nil,但是如果将block执行的代码注释掉,block不执行,就会发生循环引用问题,因为‘strSecVC = nil;’得不到执行,像之前说的那样不管block执行还是没执行,block就会对block使用的变量进行了捕获,如果变量是强引用的,block也就强引用了该变量,如果该变量也强引用了block,那就会导致循环引用问题。