内存管理之NSTimer定时器
CADisplayLink
它一个定时器 是用于同步屏幕刷新频率的计时器
CADisplayLink其实也是一个定时器,只不过这个定时器不用你来设置时间,它是要保证调用频率和屏幕的刷帧频率一致,通常来说大概是60FPS(一秒钟会调用60次),当然如果你主线程要是做了很多耗时操作的话也可能就不到60了
- (void)viewDidLoad {
[super viewDidLoad];
// 保证调用频率和屏幕的刷帧频率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)click {
NSLog(@"1");
}
- (void)dealloc {
[self.link invalidate];
}它会造成循环引用 导致无法释放 从而导致dealloc不会执行
NSTimer
首先我们先来看一段代码
@interface SecondViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
//使用方法1执行
self.timer= [NSTimer timerWithTimeInterval:1 target:self selector:@selector(click) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
// [[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate date]];
//使用方法2执行
NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector: @selector(click) userInfo:nil repeats:YES];
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
- (void)click {
NSLog(@"123");
}
@end不卖关子了 直接说结论 两者执行效果一致
我们再来看下官方的文档对**timerWithTimeInterval: target: selector: userInfo: repeats:**的解释
Initializes a timer object with the specified object and selector.谷歌翻译:
使用指定的对象和选择器初始化计时器对象。再看下
scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:
Creates a timer and schedules it on the current run loop in the default mode.谷歌翻译:
创建一个计时器并在默认模式下将其安排在当前运行循环上。一个是创建一个定时器 一个是创建定时器并加入到runloop 所以 scheduledTimerWithTimeInterval 只是对**timerWithTimeInterval**的基本封装 因为**scheduledTimerWithTimeInterval**只是把time加入到runloop的默认模式 如果需要加入其他模式 还是需要自己处理下
再来看一下这段代码
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}很多时候我们发现 就算是我们放到dealloc里面 发现timer根本没有被销毁 会继续在后台执行
因为在初始化NSTimer的时候,传入的target(self)会被NSTimer强引用,并且控制器又强引用NSTimer,所以产生循环引用
- (void)viewDidDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}年少无知的我 为了搞定这个问题直接采用了一个暴力的方法 在**viewWillDisappear**里面操作了一波 效果是达到了 但是总感觉不那么优雅 而且而且 这个处理方式有一个弊端 就是如果有多级push跳转 就会导致timer被提前释放 无法再使用
于是有了下面的操作
1. 使用block方式 该方案是将计时器所应执行的任务封装成"Block",在调用计时器函数时,把block作为userInfo参数传进去
2. 给self添加中间件proxy
这两种方式都可以解决 也是目前网上比较多的解决方案 具体的实现代码我就不贴了 网上很多 搜索关键词
NSTimer 循环引用和解决方案
其实通过苹果的API也可以解决这个问题
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakself = self;
self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakself click];
}];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}API
+ (NSTimer )timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));官方api注释文档
Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
- parameter: timeInterval The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
- parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
- parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references谷歌翻译
创建并返回一个用指定块对象初始化的新 NSTimer 对象。 这个计时器需要在运行循环(通过 -[NSRunLoop addTimer:])之前被调度,然后才会触发。
- 参数:timeInterval 定时器触发之间的秒数。 如果秒小于或等于 0.0,则此方法选择非负值 0.1 毫秒
- 参数:repeats 如果是,定时器将重复重新调度自己直到失效。 如果否,定时器将在触发后失效。
- 参数:block 定时器的执行体; 计时器本身在执行时作为参数传递给此块以帮助避免循环引用参数block就是为了解决这个循环引用的问题 当然也要稍稍注意强引用问题 还有就是此方法是从ios10开始兼容
NSTimer的时间其实是不准的 因为有runloop的存在 很可能runloop循环几圈(runloop每循环一圈的时间都是不固定的) timer才走一秒 或者runloop循环一次需要的时间远大于timer执行一次的时间 也有可能导致错过某个需要回调的点 从而导致runloop不会很精准的去触发timer 持续时间长 就会有较大的影响
其实NSTimer自身是有一个属性可以允许有一定的时间误差存在 感兴趣的可以自己去搜索一下
GCD
GCD的定时器相比前面两个就要精确很多 因为他的执行是直接跟CPU内核挂钩 而且也不依赖runloop 可控性比较强
首先我们来创建一个GCD定时器
- (void)viewDidLoad {
[super viewDidLoad];
//创建队列
dispatch_queue_t quene = dispatch_get_main_queue();
//创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, quene);
dispatch_source_set_timer(timer,DISPATCH_TIME_NOW, 1 NSEC_PER_SEC, 0 NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"123");
});
dispatch_resume(timer);
}当你满心欢喜的创建出定时器 发现他压根都不会执行打印 也就是handler回调没有执行
所以我们来改进下方法
- (void)viewDidLoad {
[super viewDidLoad];
//创建队列
dispatch_queue_t quene = dispatch_get_main_queue();
//创建定时器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, quene);
dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, 1 NSEC_PER_SEC), 1 NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"123");
});
dispatch_resume(self.timer);
}因为viewDidLoad在执行完成后timer就被释放掉了 导致不会走回调里面的方法 所以必须用self强引用保住timer不被释放 这个时候再次运行定时器就正常的执行打印了 同时当前界面被销毁时dealloc正常的执行
- (void)viewDidLoad {
[super viewDidLoad];
//创建队列
dispatch_queue_t quene = dispatch_get_main_queue();
//创建定时器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, quene);
dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, 1 NSEC_PER_SEC), 1 NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"123");
self.string = @"123";
});
dispatch_resume(self.timer);
}如果在dispatch_source_set_event_handler 回调的block里面用self强引用 就会出现循环引用的问题 所以需要注意**__weak**的使用问题
dispatch_suspend 和 dispatch_resume
dispatch_suspend 是将定时器暂停
dispatch_resume 是恢复定时器
在方法
dispatch_suspend(dispatch_object_t object) 里面有一句说明
Calls to dispatch_suspend() must be balanced with calls* to dispatch_resume().谷歌翻译
对 dispatch_suspend() 的调用必须与调用平衡到 dispatch_resume()。suspend 暂停 resume 恢复
你调用了suspend几次, 你想resume的话,就必须要remuse几次,才能继续运行。
但remuse的状态下,如果再进行一次resume就会crash,所以要注册一个BOOL值的状态进行记录,防止多次suspend和resume引起闪退。并且在suspend的状态下,如果你设置_timer = nil也会crash