1.NSTimer
iOS中最基本的定時(shí)器。其通過(guò)RunLoop來(lái)實(shí)現(xiàn),一般情況下較為準(zhǔn)確,但當(dāng)當(dāng)前循環(huán)耗時(shí)操作較多時(shí),會(huì)出現(xiàn)延遲問(wèn)題。同時(shí),也受所加入的RunLoop的RunLoopMode影響。
NSTimer的類方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
//iOS10.0之后可以用的方法
+ (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));
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
NSTimer的對(duì)象方法
這兩個(gè)方法多了一個(gè)參數(shù)date,這個(gè)參數(shù)可以指定定時(shí)器什么時(shí)候開(kāi)啟,創(chuàng)建完之后需要手動(dòng)加入Runloop
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
- (void)fire;
//iOS10.0之后可以使用
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
可以看到定時(shí)器創(chuàng)建的類方法都分為invocation和selector兩種方式。
1.1Timer定時(shí)器的創(chuàng)建
1.1.1傳NSInvocation方法創(chuàng)建定時(shí)器
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(timered)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = @selector(timered);
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 invocation:invocation repeats:YES];
- (void)timered{
NSLog(@"定時(shí)器被調(diào)用");
}
1.1.2傳SEL方式創(chuàng)建定時(shí)器
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timered) userInfo:nil repeats:YES];
1.1.3block方式創(chuàng)建定時(shí)器
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"定時(shí)器被調(diào)用");
}];
1.1.4NSTimer的fire方法
調(diào)用了fire方法之后會(huì)立即執(zhí)行定時(shí)器的方法,fire方法不會(huì)改變預(yù)定周期性調(diào)度。即調(diào)用完fire方法之后不會(huì)從當(dāng)前時(shí)間重新開(kāi)始計(jì)算時(shí)間間隔,還是會(huì)從上一次計(jì)算時(shí)間間隔。
定時(shí)器如果不循環(huán)調(diào)用,提前調(diào)用了fire方法,不會(huì)在時(shí)間到了之后在調(diào)用一次,因?yàn)閳?zhí)行一次之后任務(wù)就結(jié)束了。
NSLog(@"當(dāng)前時(shí)間");
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(timered) userInfo:nil repeats:YES];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[timer fire];
});
運(yùn)行結(jié)果:
2020-03-02 17:54:02.436750+0800 Test[46183:3046157] 當(dāng)前時(shí)間
2020-03-02 17:54:07.438237+0800 Test[46183:3046157] 定時(shí)器被調(diào)用
2020-03-02 17:54:12.439445+0800 Test[46183:3046157] 定時(shí)器被調(diào)用
定時(shí)器時(shí)間間隔設(shè)置了10秒,第五秒的時(shí)候調(diào)用了一次fire方法,定時(shí)器第二次調(diào)用是在第10秒的時(shí)候,而不是15秒的時(shí)候。
定時(shí)器如果不循環(huán)調(diào)用,提前調(diào)用了fire方法,不會(huì)在時(shí)間到了之后在調(diào)用一次,因?yàn)閳?zhí)行一次之后任務(wù)就結(jié)束了。
NSLog(@"當(dāng)前時(shí)間");
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(timered) userInfo:nil repeats:NO];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[timer fire];
});
運(yùn)行結(jié)果:
2020-03-02 17:56:39.859278+0800 Test[46250:3050494] 當(dāng)前時(shí)間
2020-03-02 17:56:44.860940+0800 Test[46250:3050494] 定時(shí)器被調(diào)用
定時(shí)器在5秒之后調(diào)用一次就不再調(diào)用。
1.1.5timerWithTimeInterval和scheduledTimerWithTimeInterval的區(qū)別
scheduledTimerWithTimeInterval創(chuàng)建的時(shí)候就已經(jīng)添加到runloop,
通過(guò)timerWithTimeInterval創(chuàng)建定時(shí)器之后需要手動(dòng)添加到runloop才能開(kāi)始運(yùn)行,因?yàn)槎〞r(shí)器的運(yùn)行是依賴runloop的,xcode中對(duì)方法的解釋
scheduledTimerWithTimeInterval
Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
timerWithTimeInterval
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.
1.2定時(shí)器循環(huán)引用出現(xiàn)的原因
把定時(shí)器在一個(gè)控制器中創(chuàng)建,在控制器的dealloc方法中銷毀定時(shí)器。
通過(guò)NSInvocation、selector和block三種方式創(chuàng)建定時(shí)器,分別在控制器的dealloc中銷毀定時(shí)器
- (void)dealloc{
NSLog(@"控制器被銷毀");
[self.timer invalidate];
self.timer = nil;
}
運(yùn)行發(fā)現(xiàn),當(dāng)控制器被pop時(shí),只有通過(guò)block方式創(chuàng)建的定時(shí)器會(huì)調(diào)用“控制器被銷毀”。說(shuō)明其他兩種方式都會(huì)造成控制器無(wú)法釋放,造成內(nèi)存泄漏。
如果把是否循環(huán)的參數(shù)改成NO,表示定時(shí)器只執(zhí)行一次,則控制器也能被銷毀。
1.2.1定時(shí)器造成循環(huán)引用的原因
因?yàn)槎〞r(shí)器要被加到runloop中才能運(yùn)行,所以定時(shí)器被runloop強(qiáng)引用,因?yàn)槎〞r(shí)器在運(yùn)行時(shí)需要調(diào)用傳入的target的方法,target一般都是控制器,所以定時(shí)器強(qiáng)引用了控制器,定時(shí)器的銷毀又放在控制器的dealloc方法中,所以一直無(wú)法釋放。
1.2.2 不會(huì)造成循環(huán)引用的情況
- 1.非重復(fù)計(jì)時(shí)器,即repeat參數(shù)傳NO的,因?yàn)閳?zhí)行完一次之后會(huì)直接失效。相當(dāng)于調(diào)用了invalidate方法,runloop會(huì)把定時(shí)器移除。所以控制器就可以被銷毀了。蘋(píng)果文檔描述如下
//非重復(fù)計(jì)時(shí)器在觸發(fā)后立即使自身失效。
A nonrepeating timer invalidates itself immediately after it fires.
- 2.通過(guò)iOS10.0之后的新方法,定時(shí)器調(diào)用block,方法的介紹中描述如下
//阻塞定時(shí)器執(zhí)行體;在執(zhí)行時(shí),計(jì)時(shí)器本身作為參數(shù)傳遞給這個(gè)塊,以幫助避免循環(huán)引用
- 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
1.3循環(huán)引用的解決
由于造成循環(huán)引用的原因是runloop強(qiáng)引用NSTimer,NSTimer強(qiáng)引用控制器,所以我們可以讓NSTimer不再?gòu)?qiáng)引用控制器,即在創(chuàng)建NSTimer的時(shí)候傳入的target為另一個(gè)對(duì)象,用來(lái)相應(yīng)定時(shí)器。
具體步驟:
1.創(chuàng)建類YYTimerResponse類,在類中時(shí)間NSTimer的調(diào)用方法"- (void)timered"
#import "YYTimerResponse.h"
@implementation YYTimerResponse
- (void)timered{
NSLog(@"在YYTimerResponse中定時(shí)器被調(diào)用");
}
- (void)dealloc{
NSLog(@"YYTimerResponse被銷毀");
}
@end
2.在控制器中添加定時(shí)器,并且在dealloc中銷毀定時(shí)器
- (void)viewDidLoad {
[super viewDidLoad];
YYTimerResponse *timeResponse = [[YYTimerResponse alloc] init];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:timeResponse selector:@selector(timered) userInfo:nil repeats:YES];
}
- (void)dealloc{
NSLog(@"控制器被銷毀");
[self.timer invalidate];
self.timer = nil;
}
運(yùn)行之后定時(shí)器開(kāi)始調(diào)用,控制器返回之后打印如下
2020-03-02 18:47:46.879168+0800 Test[47229:3134132] 在YYTimerResponse中定時(shí)器被調(diào)用
2020-03-02 18:47:47.879918+0800 Test[47229:3134132] 在YYTimerResponse中定時(shí)器被調(diào)用
2020-03-02 18:47:48.116563+0800 Test[47229:3134132] 控制器被銷毀
2020-03-02 18:47:48.116796+0800 Test[47229:3134132] YYTimerResponse被銷毀
因?yàn)榇藭r(shí)不再?gòu)?qiáng)引用控制器,所以當(dāng)控制器返回時(shí),控制器的dealloc方法被調(diào)用,在dealloc方法中調(diào)用了[self.timer invalidate];,所以NSTimer被移除runloop,控制器被銷毀所以控制器也不強(qiáng)引用NSTimer了,所以NSTimer也要被釋放,所以YYTimerResponse也被銷毀。
1.4在子線程啟動(dòng)定時(shí)器
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"當(dāng)前線程%@", [NSThread currentThread]);
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timered) userInfo:nil repeats:YES];
});
- (void)timered{
NSLog(@"定時(shí)器被調(diào)用,當(dāng)前線程:%@", [NSThread currentThread]);
}
運(yùn)行結(jié)果:
2020-03-02 19:04:50.483133+0800 Test[47650:3162147] 當(dāng)前線程<NSThread: 0x6000013c1a00>{number = 3, name = (null)}
運(yùn)行之后發(fā)現(xiàn)定時(shí)器沒(méi)有執(zhí)行
因?yàn)樽泳€程的runloop默認(rèn)沒(méi)有開(kāi)啟
所以需要在創(chuàng)建完定時(shí)器之后調(diào)用“[[NSRunLoop currentRunLoop] run];”,開(kāi)啟定時(shí)器。
因?yàn)樽泳€程也可以強(qiáng)引用NSTimer,所以此時(shí)控制器也不會(huì)被銷毀,所以還是需要像1.3中一樣解決循環(huán)引用的問(wèn)題
所以代碼修改如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"當(dāng)前線程%@", [NSThread currentThread]);
YYTimerResponse *timeResponse = [[YYTimerResponse alloc] init];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:timeResponse selector:@selector(timered) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
});
運(yùn)行結(jié)果:發(fā)現(xiàn)控制器pop之后,定時(shí)器還是無(wú)法停止
把target設(shè)置為其他對(duì)象后,控制器的dealloc方法仍然無(wú)法調(diào)用的原因:因?yàn)樵陂_(kāi)啟異步操作的block中強(qiáng)引用了self,即子線程的runloop強(qiáng)引用了控制器,所以控制器無(wú)法被銷毀,把self弱引用。
所以最終代碼如下:
-(void)viewDidLoad{
[super viewDidLoad];
__weak __typeof__(self) weakSelf = self;
self.timeResponse = [[YYTimerResponse alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"當(dāng)前線程%@", [NSThread currentThread]);
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf.timeResponse selector:@selector(timered) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
weakSelf.thread = [NSThread currentThread];
});
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
self.timeResponse = nil;
NSLog(@"控制器被銷毀");
}
打印結(jié)果:
2020-03-03 16:03:48.477618+0800 Test[55881:3549309] 當(dāng)前線程<NSThread: 0x600001e4c240>{number = 3, name = (null)}
2020-03-03 16:03:49.479146+0800 Test[55881:3549309] 在YYTimerResponse中定時(shí)器被調(diào)用,當(dāng)前線程<NSThread: 0x600001e4c240>{number = 3, name = (null)}
2020-03-03 16:03:50.480857+0800 Test[55881:3549309] 在YYTimerResponse中定時(shí)器被調(diào)用,當(dāng)前線程<NSThread: 0x600001e4c240>{number = 3, name = (null)}
2020-03-03 16:03:51.483406+0800 Test[55881:3549309] 在YYTimerResponse中定時(shí)器被調(diào)用,當(dāng)前線程<NSThread: 0x600001e4c240>{number = 3, name = (null)}
2020-03-03 16:03:53.939806+0800 Test[55881:3549256] YYTimerResponse被銷毀
2020-03-03 16:03:53.939990+0800 Test[55881:3549256] 控制器被銷毀
在上面的代碼中,除了把self弱引用之外,還手動(dòng)把YYTimerResponse對(duì)象設(shè)置為nil,因?yàn)閅YTimerResponse對(duì)象也被強(qiáng)引用了,無(wú)法銷毀。
1.5定時(shí)器加入Runloop的模式(有時(shí)無(wú)法響應(yīng))
在進(jìn)行UI交互的時(shí)候(如tableView的滑動(dòng)時(shí)),runloop所在的模式是UITrackingRunLoopMode,而在把定時(shí)器默認(rèn)加入runloop的時(shí)候會(huì)加入"NSDefaultRunLoopMode"
runloop執(zhí)行任務(wù)時(shí)會(huì)在Mode間切換,所以在UI交互時(shí)無(wú)法響應(yīng)定時(shí)器。
1.5.1解決方法1: 再加入runloop時(shí)把模式設(shè)置為NSRunLoopCommonModes
把定時(shí)器添加到runloop時(shí)模式設(shè)置為"NSRunLoopCommonModes",這個(gè)模式并不是一種Mode,而是一種特殊的標(biāo)記,關(guān)聯(lián)的有一個(gè)set(默認(rèn)包含NSDefaultRunLoopMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode) 。
代碼:
YYTimerResponse *timeResponse = [[YYTimerResponse alloc] init];
self.timer = [NSTimer timerWithTimeInterval:1 target:timeResponse selector:@selector(timered) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
1.5.3解決方法2:把定時(shí)器加入子線程
由于UI交互是在主線程,所以把定時(shí)器加入子線程的Runloop,就不用管加入的模式,因?yàn)槭莾蓚€(gè)runloop,沒(méi)有關(guān)聯(lián)。
2.GCD中的定時(shí)器
GCD中的Dispatch Source其中的一種類型是DISPATCH_SOURCE_TYPE_TIMER,可以實(shí)現(xiàn)定時(shí)器的功能。
dispatch源監(jiān)聽(tīng)系統(tǒng)內(nèi)核對(duì)象并處理,其可以實(shí)現(xiàn)更加精準(zhǔn)的定時(shí)效果。
GCD的定時(shí)器不是通過(guò)runloop實(shí)現(xiàn)的,所以不會(huì)被runloop強(qiáng)引用,需要當(dāng)前對(duì)象強(qiáng)引用,否則會(huì)直接被釋放。因此GCD的定時(shí)器也沒(méi)有循環(huán)引用的問(wèn)題。
使用步驟:
NSLog(@"當(dāng)前時(shí)間");
//設(shè)置定時(shí)器回調(diào)執(zhí)行所在的隊(duì)列
dispatch_queue_t queue = dispatch_get_main_queue();
//創(chuàng)建dispatch_source_t類型的對(duì)象gcdTimer
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設(shè)置定時(shí)器開(kāi)始時(shí)間,2秒后
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
//時(shí)間間隔
uint64_t intervalTime = (int64_t)(1 * NSEC_PER_SEC);
//允許誤差時(shí)間,設(shè)置為0即不允許有誤差
uint64_t errorTime = 0;
//按照上面的參數(shù)設(shè)置定時(shí)器
dispatch_source_set_timer(self.gcdTimer, startTime, intervalTime, errorTime);
//設(shè)置定時(shí)器的回調(diào)
dispatch_source_set_event_handler(self.gcdTimer, ^{
NSLog(@"GCD定時(shí)器調(diào)用");
});
//啟動(dòng)定時(shí)器
dispatch_resume(self.gcdTimer);
啟動(dòng)定時(shí)器,然后返回控制器,打印結(jié)果如下
2020-03-03 17:24:33.348304+0800 Test[57422:3669949] 當(dāng)前時(shí)間
2020-03-03 17:24:35.348971+0800 Test[57422:3669949] GCD定時(shí)器調(diào)用,當(dāng)前線程<NSThread: 0x600000cc4080>{number = 1, name = main}
2020-03-03 17:24:36.348850+0800 Test[57422:3669949] GCD定時(shí)器調(diào)用,當(dāng)前線程<NSThread: 0x600000cc4080>{number = 1, name = main}
2020-03-03 17:24:37.303353+0800 Test[57422:3669949] 控制器被銷毀
由打印結(jié)果可以知道GCD的定時(shí)器不會(huì)引起循環(huán)引用。
暫停GCD定時(shí)器:
dispatch_suspend(self.gcdTimer);
取消GCD定時(shí)器:
dispatch_cancel(self.gcdTimer);
GCD定時(shí)器暫停之后仍然可以繼續(xù)執(zhí)行,NSTimer則不可以,NSTimer只能直接銷毀,需要再次啟動(dòng)則需要重建創(chuàng)建NSTimer定時(shí)器。
GCD定時(shí)器調(diào)用了"dispatch_cancel"之后則無(wú)法繼續(xù)執(zhí)行,通過(guò)打印調(diào)用“dispatch_cancel”之前和之后gcdTimer對(duì)象,可以發(fā)現(xiàn)之后對(duì)象中會(huì)標(biāo)識(shí)出“cancelled”。
注意點(diǎn):
dispatch_suspend 狀態(tài)下無(wú)法釋放
如果調(diào)用 dispatch_suspend 后 timer 是無(wú)法被釋放的。一般情況下會(huì)發(fā)生崩潰并報(bào)“EXC_BAD_INSTRUCTION”錯(cuò)誤,看下 GCD 源碼dispatch source release 的時(shí)候判斷了當(dāng)前是否是在暫停狀態(tài)。
所以,dispatch_suspend 狀態(tài)下直接釋放當(dāng)前控制器或者釋放定時(shí)器,會(huì)導(dǎo)致定時(shí)器崩潰。
并且初始狀態(tài)(未調(diào)用dispatch_resume)、掛起狀態(tài),都不能直接調(diào)用dispatch_source_cancel(timer),調(diào)用就會(huì)導(dǎo)致app閃退。
建議一:盡量不使用dispatch_suspend,在dealloc方法中,在dispatch_resume狀態(tài)下直接使用dispatch_source_cancel來(lái)取消定時(shí)器。
建議二:使用懶加載創(chuàng)建定時(shí)器,并且記錄當(dāng)timer 處于dispatch_suspend的狀態(tài)。這些時(shí)候,只要在 調(diào)用dealloc 時(shí)判斷下,已經(jīng)調(diào)用過(guò) dispatch_suspend 則再調(diào)用下 dispatch_resume后再cancel,然后再釋放timer。
參考:iOS中如何正確釋放GCD定時(shí)器(dispatch_source_t)以及防止Crash?
3. CADisplayLink定時(shí)器
CADisplayLink是基于屏幕刷新的周期,所以其一般很準(zhǔn)時(shí),每秒刷新60次。其本質(zhì)也是通過(guò)RunLoop,所以當(dāng)RunLoop選擇其他模式或被耗時(shí)操作過(guò)多時(shí),仍舊會(huì)造成延遲。NSTimer中的循環(huán)引用問(wèn)題他也存在。
使用步驟:
//創(chuàng)建接受定時(shí)器回調(diào)的對(duì)象
YYTimerResponse *timeresponse = [[YYTimerResponse alloc] init];
// 創(chuàng)建CADisplayLink
self.displayLink = [CADisplayLink displayLinkWithTarget:timeresponse selector:@selector(timered)];
//設(shè)置定時(shí)器周期,這個(gè)屬性即將被廢棄, 在iOS10新增了“preferredFramesPerSecond”代替他
// self.displayLink.frameInterval = 60;
self.displayLink.preferredFramesPerSecond = 1;
// 添加至RunLoop中
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
使用“frameInterval”屬性設(shè)置定時(shí)器間隔時(shí),因?yàn)槠聊灰幻腌娝⑿?0次,所以設(shè)置為60,表示一秒鐘調(diào)用一次。
使用“preferredFramesPerSecond”時(shí),設(shè)置的就是幾秒鐘刷新一次,該屬性iOS10.0后才能用。