所谓循环引用,即互相持有对方,导致双方的引用计数都不能降为0,也就不能最终被释放,引起内存泄漏。在ARC环境下,程序员就省去了亲自管理内存的烦恼。但是,由于引用计数式的内存管理机制不能够解决环状引用也即是循环引用的问题,即使ARC,某些情况下也会引起循环引用的问题。本文将探讨在ARC环境下出现循环引用的场景以及解决办法。
一 代理
最常见的循环引用场景就是代理模式下delegate跟委托方互相持有强引用。当委托中的delegate 属性用strong修饰的时候,便会引起循环引用。示例代码
//ClassA 委托方
@protocol TestDelegate <NSObject>
-(void)dx_testMethod;
@end
@interface ClassA : NSObject
@property(nonatomic,strong)id<TestDelegate>delegate; //这里用strong修饰是不对的,会造成循环引用的问题,应该用weak修饰
@end
//ClassB 代理方
@interface ClassB: NSObject
@property(nonatomic,strong)ClassA * classA;
@end
@implementation ClassB
-(instancetype)init
{
if(self = [super init])
{
self.classA.delegate = self;
}
return self;
}
classB对属性ClassA强引用,如果ClassA 对代理对象强引用,则会造成互相持有,从而造成循环引用。正确的做法是用weak来修饰delegate。
二 block
block 造成循环引用的情况很是常见。面试中面试官问的最多的也是block,问道block必会问其造成循环引用的情况和规避办法。
我想,研究这个问题,先来了解下block。
什么是block跟block基本用法这里就不再赘述,我想谈的是:block的种类。
我很久前面试的时候曾折戟于此:种类?block分种类的?(小白都有此一问吧)。
没错,block是分种类的。
block的三种类型:
struct objc_class _NSConcreteGlobalBlock; //全局block
struct objc_class _NSConcreteStackBlock; //栈block
struct objc_class _NSConcreteMallocBlock; //堆block
block类型 | 存储区域 |
_NSConcreteGlobalBlock | 全局区 |
_NSConcreteStackBlock | 栈区 |
_NSConcreteMallocBlock | 堆区 |
1 全局Block(NSConcreteGlobalBlock) :存储在内存的全局区,没有引用任何局部变量。
static NSString * globalStr = @"globalStr";
@implementation TestClassA
-(void)globalBlock{
void (^globalBlock)(NSString *) = ^void(NSString * str){
NSLog(@"%@",str);
};
NSLog(@"%@",globalBlock);
globalBlock(@"test");
void (^globalBlock2)(NSString *) =^void(NSString *str){
NSLog(@"%@",globalStr);
};
NSLog(@"%@",globalBlock2);
globalBlock2(@"test2");
}
控制台打印情况:
019-03-06 14:20:07.441489+0800 TestUntilPro[26043:3175490] <__NSGlobalBlock__: 0x107ed5d00>
2019-03-06 14:20:45.705464+0800 TestUntilPro[26043:3175490] test
2019-03-06 14:26:25.302211+0800 TestUntilPro[26108:3185021] <__NSGlobalBlock__: 0x101b97d40>
2019-03-06 14:27:03.926597+0800 TestUntilPro[26108:3185021] globalStr
可以看到,globalBlock确实是一个NSGlobalBlock类型的block,没有引用任何的局部变量。只要没有引用任何堆或者栈内存上的数据,就是NSConcreteGlobalBlock.
2 栈区block(NSConcreteStackBlock):存储在堆区,引用局部变量
-(void)globalBlock{
NSString * localTestStr =@"localTestStr";
^void(NSString * str){
NSLog(@"%@",localTestStr);
};
NSLog(@"%@",^(NSString * str){
NSLog(@"%@",localTestStr);
});
}
控制台打印情况:
2019-03-06 14:38:00.642140+0800 TestUntilPro[26219:3204053] <__NSStackBlock__: 0x7ffee0512418>
从打印结果可以看出,引用了局部变量,这个block是一个NSStackBlock。是存储在内存栈区。
3 堆区block (NSConcreteMallocBlock):栈blockcopy到堆
-(void)globalBlock{
NSString * localTestStr =@"localTestStr";
void (^mallocBlock)(NSString * str) = ^void(NSString * str){
NSLog(@"%@",localTestStr);
};
NSLog(@"%@",mallocBlock);
}
控制台打印结果:
2019-03-06 14:47:06.404086+0800 TestUntilPro[26353:3220402] <__NSMallocBlock__: 0x600000c75290>
从打印结果可以看出,这是个NSConcreteMallocBlock类型的block。我们把栈block赋值给一个变量,就把这个block从栈区copy到堆区成了NSConcreteMallocBlock类型
把BLock赋值给变量就一定是堆block吗?
看代码:
-(void)globalBlock{
NSString * localTestStr =@"localTestStr";
void (^ __weak mallocBlock)(NSString * str) = ^void(NSString * str){
NSLog(@"%@",localTestStr);
};
NSLog(@"%@",mallocBlock);
}
上面的堆block改成__weak修饰,会发生什么改变呢?
2019-03-06 14:57:14.071639+0800 TestUntilPro[26423:3235598] <__NSStackBlock__: 0x7ffee7832438>
可以看到mallocBlock成了__NSStackBlock即栈block。这是强行用__weak把block分配在栈上。MRC环境下,但我们声明一个block属性的时候用copy修饰,是因为block默认是在栈上创建的,把他赋值给一个变量如果不是用copy修饰或者在赋值的时候没有被copy,就会发生crash。ARC下block默认就创建在堆上,不用copy 修饰就不会发生崩溃。虽然如此,还是需要用copy修饰的。如上,我们上可以强行把block分配在栈上的。
以上是block的三种类型的探讨(水平有限,若有错误路过大神指出)。
再来看看什么时候block造成了循环引用。
根据循环引用的定义,对象间互相持有导致引用计数无法归0,那么可以猜测,当block对象被当前对象所持有并且强引用,当前对象又强引用block对象时候回造成循环引用。
示例代码:
//ClassA.h
typedef void (^TestBlock)(NSString *);
@interface ClassA :NSObject
@property(nonatomic,copy) TestBlock block;//一个block属性,并且用copy从栈copy到堆中
@end
//ClassA.h.m
@implementation SecViewController
-(instancetype)init
{
if(self = [super init])
{
self.block = ^(NSString * str){
[self.view addSubview:[UILabel new]]; //此处会报“Capturing 'self' strongly in this block is likely to lead to a retain cycle”警告
};
}
return self;
}
跟之前想的一样,block被当前对象强引用,block 块中直接引用self ,编译器发出警告“Capturing 'self' strongly in this block is likely to lead to a retain cycle”,会造成循环引用。,那么可知,如果block没有直接或者间接被self存储,就不会产生循环引用。就不需要用weak self
解决办法是创建当前对象的弱引用weakSelf
//ClassA.h 这是一个直接互相持有
typedef void (^TestBlock)(NSString *);
@interface ClassA :NSObject
@property(nonatomic,copy) TestBlock block;//一个block属性,并且用copy从栈copy到堆中
@end
//ClassA.h.m
@implementation SecViewController
-(instancetype)init
{
if(self = [super init])
{
__weak typeof(self)weakself = self;
self.block = ^(NSString * str){
[weakself.view addSubview:[UILabel new]];
};
}
return self;
}
间接持有的情况这里忽略知道就可以了(原谅我很懒)。
再来看一种情况:(节选自简书:iOS开发笔记(二):block循环引用)
- (void)viewDidLoad {
[super viewDidLoad];
MitPerson*person = [[MitPerson alloc]init];
__weak MitPerson * weakPerson = person;
person.mitBlock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakPerson test];
});
};
person.mitBlock();
}
这种情况下,person.mitBlock作用域结束之后,weakPerson会被释放,test方法不会执行。
解决的办法是在person.mitBlock里再次强引用person;
- (void)viewDidLoad {
[super viewDidLoad];
MitPerson*person = [[MitPerson alloc]init];
__weak MitPerson * weakPerson = person;
person.mitBlock = ^{
__strong MitPerson * strongPerson = weakPerson;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[strongPerson test];
});
};
person.mitBlock();
}
这样当person.mitBlock 作用域结束之后,dispatch_afater的block块还在强引用person,因此不会被释放。
还有,当使用系统API
[[NSNotificationCenter defaultCenter]addObserverForName:@"" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
}];
在这个block中使用self 会发生循环引用,规避办法也是weakself;
三 定时器
NSTimer ,定时器。在开发中我们经常会用到。虽然NSTimer有些弊端,例如并不是完全准确,会造成内存不释放的问题,但是依然不影响开发中的应用。现在,我们就来探讨下NSTimer的是放问题
我们新建一个工程,并且在已经创建好的控制器ViewController中来写一个跳转,跳转到SecViewController中,他们都是基于导航栏管理。在SecViewController中写一个定时器跟定时器方法,然后执行pop操作跳转回到Viewcontroller.
示例代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(tiemAction:) userInfo:nil repeats:YES];
[self.timer fire];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.navigationController popViewControllerAnimated:YES];
}
-(void)tiemAction:(NSTimer *)timer
{
NSLog(@"do something");
}
-(void)dealloc
{
NSLog(@"SecViewController dealloc");
}
运行跳转到SecViewController之后发现定时器已经开启,控制台打印:
3-07 13:27:19.424685+0800 TestUntilPro[5406:360073] do something
2019-03-07 13:27:21.423575+0800 TestUntilPro[5406:360073] do something
2019-03-07 13:27:23.423859+0800 TestUntilPro[5406:360073] do something
2019-03-07 13:27:25.423642+0800 TestUntilPro[5406:360073] do something
此时点击空白处触发pop方法会跳到ViewController页面之后,我们看到控制台还在打印“do something”,而且,dealloc方法中的打印代码也没有执行。这也就是说SecViewController实例并没有销毁,仍然在内存中没有释放,如果频繁的进出或者有大量的这些页面,就会造成一些性能已经内存方面的问题。
怎么解决呢??
朱子曰:“捉得病根,方能对症下药”。(以下部分内容节选自《iOS快速开发进阶与实战》)
事实上,timer会对直接传进来的target强引用,即使是用weak修饰的target,在这个例子中,控制器Self是对timer强引用的,而由于self没有被释放,导致很多人认为是timer强引用了self导致了循环引用。如果真是这样的话,那self 对timer弱引用即可打破这个循环,但是我们在用weak修饰了timer之后仍然没有解决问题(这个可以自己试一下,这里不贴代码测试)。说明病根不再此处。
定时器的启用还涉及一个领域,就是RunLoop。timer必须加到RunLoop中才能有效。在repeats为YES的情况下是通过RunLoop 来控制timer的。有时候RunLoop处理事务可能会消耗一些时间导致某一次的循环时间可能稍长,这就导致我们说的定时器有时候不准。所以runLoop 对timer强引用。当前runLoop 是不会被销毁的,强引用了timer,而timer通过参数强引用了target,也就是当前控制器self,所以不管self对timer强引用还是弱引用都不能打破这种循环,如果传递的target是一个self 的弱引用,也是不可以打破循环,因为timer内部为了防止target提前释放会对targer强引用,多以还是无法释放。
解决方案:创建中间对像,该对象弱引用原本的timer应该持有的targer,将自己传递给timer,通过selector方法来判断target是否为空。若为空,则将timer置为失效。
示例代码:
//WeakTimerTarget.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface WeakTimerTarget : NSObject
@property(nonatomic,weak)id target;
@property(nonatomic,assign)SEL selector;
@property(nonatomic,strong)NSTimer * timer;
+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
NS_ASSUME_NONNULL_END
//WeakTimerTarget.m
#import "WeakTimerTarget.h"
@implementation WeakTimerTarget
+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats
{
WeakTimerTarget * timerTarget = [[WeakTimerTarget alloc]init];
timerTarget.target = target;
timerTarget.selector = selector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:timerTarget selector:@selector(fire:) userInfo:userInfo repeats:repeats];
return timerTarget.timer;
}
-(void)fire:(NSTimer *)timer
{
if(self.target)
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "- Warc - performSelector - leaks"
[self.target performSelector:self.selector withObject:timer.userInfo];
#pragma clang diagnostic pop
}
else
{
[self.timer invalidate];
}
}
-(void)dealloc
{
NSLog(@"weakTimer dealloc");
}
@end
这时候我们再次运行,跳转到SecViewController,定时器运行,点击空白处执行pop方法,控制台打印:
019-03-07 14:23:12.614822+0800 TestUntilPro[5845:413502] SecViewController dealloc
2019-03-07 14:23:13.182169+0800 TestUntilPro[5845:413502] weakTimer dealloc
这就很好的解决了上面的无法释放的问题。
此外,还有必要探讨一下令人恶心的定时器的其他的创建方法的不同之处:
以scheduled开头的方法是不需要手动添加Timer到runlopp 中去 的,其他的方法是需要手动添加。
另外NSTimer 跟线程 RunLoop之间的眉来眼去会在下一篇博客中探讨。