前两天被问到NSTimer的使用场景以及循环引用问题,回过头来做一些研究,记录一下。
注意:NSTimer使用不当会造成循环引用以致内存泄露
场景:
页面1跳转页面2
#import "ViewController.h"
#import "OneViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor redColor];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//页面1跳转页面2
OneViewController *vc = [[OneViewController alloc] init];
[self presentViewController:vc animated:true completion:nil];
}
页面2中有个定时器,通过scheduledTimerWithTimeInterval初始化,实现timer的方法,并在页面2的dealloc方法中释放timer
#import "OneViewController.h"
@interface OneViewController ()
@property (nonatomic,strong) NSTimer *timer;//页面2中有一个定时器
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(timerAction)
userInfo:nil repeats:YES];
}
- (void)timerAction {
NSLog(@"正在计时...");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self dismissViewControllerAnimated:true completion:nil];
}
- (void)dealloc {
NSLog(@"dealloc");
[self.timer invalidate];
self.timer = nil;
}
结果:
页面2触发dismiss方法后dealloc方法并没有被调用,说明页面没有被释放,timer也没有被释放,虽然页面已经关闭,定时器方法timerAction仍不断被调用
思考:
页面2没有走dealloc方法说明没有被释放,不断调用timerAction方法说明timer也没有被释放,考虑瑟吉是因为页面2对timer有强引用,timer也对页面2有强引用,将timer的属性设为weak,尝试过后结果仍然发生着循环引用; 或者设置__weak typeof(self) weakSelf = self,结果还是一样
关于NSTimer中target文档描述:
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated
可知timer对target会有一个强引用,直到timer失效(invalidate)
timer要想解除对target的强引用,需要先invalidate,这就造成页面2的dealloc方法不被调用,进而无法invalidate,所以循环引用一直保持
解决办法:
大概有以下几种办法解除这个问题
方法1:定义一个weakTarget代替原有target控制器self,改变controller和timer相互强引用关系
@interface WeakTarget : NSObject
@property (nonatomic,assign) SEL selector;
@property (nonatomic,weak) NSTimer *timer;
@property (nonatomic,weak) id target;
+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(nonnull id)aTarget
selector:(nonnull SEL)aSelector
userInfo:(nullable id)userInfo
repeats:(BOOL)yesOrNo;
@end
@implementation WeakTarget
+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)yesOrNo
{
WeakTarget *weakTarget = [[WeakTarget alloc] init];
weakTarget.target = aTarget; // aTarget = OneViewController
weakTarget.selector = aSelector; // aSelector = timerAction方法的SEL包装
// weakTarget.timer对weakTarget有一个强引用
weakTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:weakTarget
selector:@selector(fire:)
userInfo:userInfo
repeats:yesOrNo];
return weakTarget.timer;
}
-(void)fire:(NSTimer *)timer {
if (self.target) {
//调用了外界传来的selector,即timerAction方法
//由OneViewController调用自己的timerAction方法
//这里会产生一个警告:PerformSelector may cause a leak because its selector is unknown.
[self.target performSelector:self.selector withObject:timer.userInfo];
}else {
[self.timer invalidate];
}
}
@end
- 关于PerformSelector may cause a leak because its selector is unknown警告,这里
外部调用:
#import "OneViewController.h"
#import "WeakTarget.h"
@interface OneViewController ()
@property (nonatomic,strong) NSTimer *timer;//页面2
@end
@implementation OneViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor greenColor];
self.timer = [WeakTarget scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
}
方法2:定义一个中间件target,通过消息转发实现(未传SEL)
@interface WeakTarget1 : NSObject
+(instancetype)initWithTarget:(id)target;
@end
@interface WeakTarget1()
@property (nonatomic,weak) id target;
@end
@implementation WeakTarget1
+(instancetype)initWithTarget:(id)target {
WeakTarget1 *weakTarget = [[WeakTarget1 alloc] init];
weakTarget.target = target;
return weakTarget;
}
//为了保证中间件能响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部的self
// 消息转发,简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
外部调用:
#import "OneViewController.h"
#import "WeakTarget1.h"
@interface OneViewController ()
@property (nonatomic,strong) NSTimer *timer;//页面2
@end
@implementation OneViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor greenColor];
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:[WeakTarget1 initWithTarget:self]
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
}
方法3:block解决循环引用(iOS10系统提供了timer的block初始化方法)
@interface OneViewController ()
@property (nonatomic,strong) NSTimer *timer;//页面2
@end
@implementation OneViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor greenColor];
/*
为了避免在 block 的执行过程中,突然出现 self 被释放的情况,先定义了一个弱引用,令其指向self,
然后使block捕获这个引用,而不直接去捕获普通的self变量。也就是说,self不会为计时器所保留。
当block开始执行时,立刻生成strong引用,以保证实例在执行期间持续存活。
*/
__weak typeof(self)weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(self)strongSelf = weakSelf;
[strongSelf timerAction];
}];
方法4:给NSTimer添加category,增加一个带block参数的初始化方法(类似方法3)
@interface NSTimer (block)
+(NSTimer *)zq_scheduledTimerIntervl:(NSTimeInterval)interval
block:(void(^)(void))block
repeats:(BOOL)repeats;
@end
#import "NSTimer+block.h"
@implementation NSTimer (block)
+ (NSTimer *)zq_scheduledTimerIntervl:(NSTimeInterval)interval block:(void (^)(void))block repeats:(BOOL)repeats {
/*userInfo文档描述:The user info for the timer.
The timer maintains a strong reference to this object until it (the timer) is invalidated.
This parameter may be nil.
计时器的用户信息。计时器保持对该对象的强引用,直到它(计时器)失效。此参数可以为nil。
因为现在是假定iOS10之前的系统版本,timer没有自带block参数的实例化方法,手动实现,将block作为timer的信息赋值给userInfo
*/
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(zq_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+(void)zq_blockInvoke:(NSTimer *)timer {
//在timer方法中执行block
void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}
外部调用:
#import "OneViewController.h"
#import "NSTimer+block.h"
@interface OneViewController ()
@property (nonatomic,strong) NSTimer *timer;//页面2
@end
@implementation OneViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor greenColor];
__weak typeof(self)weakSelf = self;
self.timer = [NSTimer zq_scheduledTimerIntervl:2.0 block:^{
__strong typeof(self)strongSelf = weakSelf;
[strongSelf timerAction];
} repeats:YES];
}
打印:
2019-08-31 15:18:08.924239+0800 NSTimer相关[65510:4082744] 正在计时...
2019-08-31 15:18:11.376765+0800 NSTimer相关[65510:4082744] dealloc
2019-08-31 15:18:11.376948+0800 NSTimer相关[65510:4082744] 已经失效
另外:
当timer设置为repeats = NO时候,不会存在上述问题
repeats参数文档描述:
If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
如果设置为YES,计时器将不断循环,直到无效。如果设置为NO,计时器将在触发后失效。(相当于设置为NO时自动调用invalidate方法)
-(void)invalidate方法文档解释:
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point. If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
此方法是从NSRunLoop对象中删除计时器的唯一方法。在invalidate方法返回之前或稍后某个时间点,NSRunLoop对象将删除对计时器的强引用 如果有的话,定时器的target和userInfo对象的强引用也会被一起删除