1.NSTimer
iOS中最基本的定時器布隔。其通過RunLoop來實現(xiàn)离陶,一般情況下較為準確,但當當前循環(huán)耗時操作較多時衅檀,會出現(xiàn)延遲問題招刨。同時,也受所加入的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的對象方法
這兩個方法多了一個參數(shù)date沉眶,這個參數(shù)可以指定定時器什么時候開啟,創(chuà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));
可以看到定時器創(chuàng)建的類方法都分為invocation和selector兩種方式杉适。
1.1Timer定時器的創(chuàng)建
1.1.1傳NSInvocation方法創(chuàng)建定時器
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(@"定時器被調(diào)用");
}
1.1.2傳SEL方式創(chuàng)建定時器
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timered) userInfo:nil repeats:YES];
1.1.3block方式創(chuàng)建定時器
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"定時器被調(diào)用");
}];
1.1.4NSTimer的fire方法
調(diào)用了fire方法之后會立即執(zhí)行定時器的方法谎倔,fire方法不會改變預(yù)定周期性調(diào)度。即調(diào)用完fire方法之后不會從當前時間重新開始計算時間間隔猿推,還是會從上一次計算時間間隔片习。
定時器如果不循環(huán)調(diào)用,提前調(diào)用了fire方法,不會在時間到了之后在調(diào)用一次藕咏,因為執(zhí)行一次之后任務(wù)就結(jié)束了状知。
NSLog(@"當前時間");
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];
});
運行結(jié)果:
2020-03-02 17:54:02.436750+0800 Test[46183:3046157] 當前時間
2020-03-02 17:54:07.438237+0800 Test[46183:3046157] 定時器被調(diào)用
2020-03-02 17:54:12.439445+0800 Test[46183:3046157] 定時器被調(diào)用
定時器時間間隔設(shè)置了10秒,第五秒的時候調(diào)用了一次fire方法侈离,定時器第二次調(diào)用是在第10秒的時候试幽,而不是15秒的時候。
定時器如果不循環(huán)調(diào)用卦碾,提前調(diào)用了fire方法铺坞,不會在時間到了之后在調(diào)用一次,因為執(zhí)行一次之后任務(wù)就結(jié)束了洲胖。
NSLog(@"當前時間");
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];
});
運行結(jié)果:
2020-03-02 17:56:39.859278+0800 Test[46250:3050494] 當前時間
2020-03-02 17:56:44.860940+0800 Test[46250:3050494] 定時器被調(diào)用
定時器在5秒之后調(diào)用一次就不再調(diào)用济榨。
1.1.5timerWithTimeInterval和scheduledTimerWithTimeInterval的區(qū)別
scheduledTimerWithTimeInterval創(chuàng)建的時候就已經(jīng)添加到runloop,
通過timerWithTimeInterval創(chuàng)建定時器之后需要手動添加到runloop才能開始運行,因為定時器的運行是依賴runloop的绿映,xcode中對方法的解釋
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定時器循環(huán)引用出現(xiàn)的原因
把定時器在一個控制器中創(chuàng)建擒滑,在控制器的dealloc方法中銷毀定時器。
通過NSInvocation叉弦、selector和block三種方式創(chuàng)建定時器丐一,分別在控制器的dealloc中銷毀定時器
- (void)dealloc{
NSLog(@"控制器被銷毀");
[self.timer invalidate];
self.timer = nil;
}
運行發(fā)現(xiàn),當控制器被pop時淹冰,只有通過block方式創(chuàng)建的定時器會調(diào)用“控制器被銷毀”库车。說明其他兩種方式都會造成控制器無法釋放,造成內(nèi)存泄漏樱拴。
如果把是否循環(huán)的參數(shù)改成NO柠衍,表示定時器只執(zhí)行一次,則控制器也能被銷毀晶乔。
1.2.1定時器造成循環(huán)引用的原因
因為定時器要被加到runloop中才能運行珍坊,所以定時器被runloop強引用,因為定時器在運行時需要調(diào)用傳入的target的方法正罢,target一般都是控制器阵漏,所以定時器強引用了控制器,定時器的銷毀又放在控制器的dealloc方法中翻具,所以一直無法釋放袱饭。
1.2.2 不會造成循環(huán)引用的情況
- 1.非重復(fù)計時器,即repeat參數(shù)傳NO的呛占,因為執(zhí)行完一次之后會直接失效虑乖。相當于調(diào)用了invalidate方法,runloop會把定時器移除晾虑。所以控制器就可以被銷毀了疹味。蘋果文檔描述如下
//非重復(fù)計時器在觸發(fā)后立即使自身失效仅叫。
A nonrepeating timer invalidates itself immediately after it fires.
- 2.通過iOS10.0之后的新方法,定時器調(diào)用block糙捺,方法的介紹中描述如下
//阻塞定時器執(zhí)行體;在執(zhí)行時诫咱,計時器本身作為參數(shù)傳遞給這個塊,以幫助避免循環(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強引用NSTimer,NSTimer強引用控制器洪灯,所以我們可以讓NSTimer不再強引用控制器坎缭,即在創(chuàng)建NSTimer的時候傳入的target為另一個對象,用來相應(yīng)定時器签钩。
具體步驟:
1.創(chuàng)建類YYTimerResponse類掏呼,在類中時間NSTimer的調(diào)用方法"- (void)timered"
#import "YYTimerResponse.h"
@implementation YYTimerResponse
- (void)timered{
NSLog(@"在YYTimerResponse中定時器被調(diào)用");
}
- (void)dealloc{
NSLog(@"YYTimerResponse被銷毀");
}
@end
2.在控制器中添加定時器,并且在dealloc中銷毀定時器
- (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;
}
運行之后定時器開始調(diào)用铅檩,控制器返回之后打印如下
2020-03-02 18:47:46.879168+0800 Test[47229:3134132] 在YYTimerResponse中定時器被調(diào)用
2020-03-02 18:47:47.879918+0800 Test[47229:3134132] 在YYTimerResponse中定時器被調(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被銷毀
因為此時不再強引用控制器憎夷,所以當控制器返回時,控制器的dealloc方法被調(diào)用昧旨,在dealloc方法中調(diào)用了[self.timer invalidate];拾给,所以NSTimer被移除runloop,控制器被銷毀所以控制器也不強引用NSTimer了,所以NSTimer也要被釋放兔沃,所以YYTimerResponse也被銷毀蒋得。
1.4在子線程啟動定時器
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"當前線程%@", [NSThread currentThread]);
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timered) userInfo:nil repeats:YES];
});
- (void)timered{
NSLog(@"定時器被調(diào)用,當前線程:%@", [NSThread currentThread]);
}
運行結(jié)果:
2020-03-02 19:04:50.483133+0800 Test[47650:3162147] 當前線程<NSThread: 0x6000013c1a00>{number = 3, name = (null)}
運行之后發(fā)現(xiàn)定時器沒有執(zhí)行
因為子線程的runloop默認沒有開啟
所以需要在創(chuàng)建完定時器之后調(diào)用“[[NSRunLoop currentRunLoop] run];”乒疏,開啟定時器额衙。
因為子線程也可以強引用NSTimer,所以此時控制器也不會被銷毀,所以還是需要像1.3中一樣解決循環(huán)引用的問題
所以代碼修改如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"當前線程%@", [NSThread currentThread]);
YYTimerResponse *timeResponse = [[YYTimerResponse alloc] init];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:timeResponse selector:@selector(timered) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
});
運行結(jié)果:發(fā)現(xiàn)控制器pop之后缰雇,定時器還是無法停止
把target設(shè)置為其他對象后,控制器的dealloc方法仍然無法調(diào)用的原因:因為在開啟異步操作的block中強引用了self,即子線程的runloop強引用了控制器追驴,所以控制器無法被銷毀械哟,把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(@"當前線程%@", [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] 當前線程<NSThread: 0x600001e4c240>{number = 3, name = (null)}
2020-03-03 16:03:49.479146+0800 Test[55881:3549309] 在YYTimerResponse中定時器被調(diào)用,當前線程<NSThread: 0x600001e4c240>{number = 3, name = (null)}
2020-03-03 16:03:50.480857+0800 Test[55881:3549309] 在YYTimerResponse中定時器被調(diào)用,當前線程<NSThread: 0x600001e4c240>{number = 3, name = (null)}
2020-03-03 16:03:51.483406+0800 Test[55881:3549309] 在YYTimerResponse中定時器被調(diào)用,當前線程<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弱引用之外暇咆,還手動把YYTimerResponse對象設(shè)置為nil,因為YYTimerResponse對象也被強引用了丙曙,無法銷毀爸业。
1.5定時器加入Runloop的模式(有時無法響應(yīng))
在進行UI交互的時候(如tableView的滑動時),runloop所在的模式是UITrackingRunLoopMode亏镰,而在把定時器默認加入runloop的時候會加入"NSDefaultRunLoopMode"
runloop執(zhí)行任務(wù)時會在Mode間切換扯旷,所以在UI交互時無法響應(yīng)定時器。
1.5.1解決方法1: 再加入runloop時把模式設(shè)置為NSRunLoopCommonModes
把定時器添加到runloop時模式設(shè)置為"NSRunLoopCommonModes",這個模式并不是一種Mode索抓,而是一種特殊的標記钧忽,關(guān)聯(lián)的有一個set(默認包含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:把定時器加入子線程
由于UI交互是在主線程桃煎,所以把定時器加入子線程的Runloop,就不用管加入的模式,因為是兩個runloop大刊,沒有關(guān)聯(lián)为迈。
2.GCD中的定時器
GCD中的Dispatch Source其中的一種類型是DISPATCH_SOURCE_TYPE_TIMER,可以實現(xiàn)定時器的功能缺菌。
dispatch源監(jiān)聽系統(tǒng)內(nèi)核對象并處理葫辐,其可以實現(xiàn)更加精準的定時效果。
GCD的定時器不是通過runloop實現(xiàn)的男翰,所以不會被runloop強引用另患,需要當前對象強引用,否則會直接被釋放蛾绎。因此GCD的定時器也沒有循環(huán)引用的問題昆箕。
使用步驟:
NSLog(@"當前時間");
//設(shè)置定時器回調(diào)執(zhí)行所在的隊列
dispatch_queue_t queue = dispatch_get_main_queue();
//創(chuàng)建dispatch_source_t類型的對象gcdTimer
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設(shè)置定時器開始時間,2秒后
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
//時間間隔
uint64_t intervalTime = (int64_t)(1 * NSEC_PER_SEC);
//允許誤差時間租冠,設(shè)置為0即不允許有誤差
uint64_t errorTime = 0;
//按照上面的參數(shù)設(shè)置定時器
dispatch_source_set_timer(self.gcdTimer, startTime, intervalTime, errorTime);
//設(shè)置定時器的回調(diào)
dispatch_source_set_event_handler(self.gcdTimer, ^{
NSLog(@"GCD定時器調(diào)用");
});
//啟動定時器
dispatch_resume(self.gcdTimer);
啟動定時器鹏倘,然后返回控制器,打印結(jié)果如下
2020-03-03 17:24:33.348304+0800 Test[57422:3669949] 當前時間
2020-03-03 17:24:35.348971+0800 Test[57422:3669949] GCD定時器調(diào)用,當前線程<NSThread: 0x600000cc4080>{number = 1, name = main}
2020-03-03 17:24:36.348850+0800 Test[57422:3669949] GCD定時器調(diào)用,當前線程<NSThread: 0x600000cc4080>{number = 1, name = main}
2020-03-03 17:24:37.303353+0800 Test[57422:3669949] 控制器被銷毀
由打印結(jié)果可以知道GCD的定時器不會引起循環(huán)引用顽爹。
暫停GCD定時器:
dispatch_suspend(self.gcdTimer);
取消GCD定時器:
dispatch_cancel(self.gcdTimer);
GCD定時器暫停之后仍然可以繼續(xù)執(zhí)行纤泵,NSTimer則不可以,NSTimer只能直接銷毀镜粤,需要再次啟動則需要重建創(chuàng)建NSTimer定時器捏题。
GCD定時器調(diào)用了"dispatch_cancel"之后則無法繼續(xù)執(zhí)行,通過打印調(diào)用“dispatch_cancel”之前和之后gcdTimer對象肉渴,可以發(fā)現(xiàn)之后對象中會標識出“cancelled”公荧。
注意點:
dispatch_suspend 狀態(tài)下無法釋放
如果調(diào)用 dispatch_suspend 后 timer 是無法被釋放的。一般情況下會發(fā)生崩潰并報“EXC_BAD_INSTRUCTION”錯誤同规,看下 GCD 源碼dispatch source release 的時候判斷了當前是否是在暫停狀態(tài)循狰。
所以,dispatch_suspend 狀態(tài)下直接釋放當前控制器或者釋放定時器券勺,會導(dǎo)致定時器崩潰绪钥。
并且初始狀態(tài)(未調(diào)用dispatch_resume)、掛起狀態(tài)关炼,都不能直接調(diào)用dispatch_source_cancel(timer)程腹,調(diào)用就會導(dǎo)致app閃退。
建議一:盡量不使用dispatch_suspend儒拂,在dealloc方法中跪楞,在dispatch_resume狀態(tài)下直接使用dispatch_source_cancel來取消定時器缀去。
建議二:使用懶加載創(chuàng)建定時器,并且記錄當timer 處于dispatch_suspend的狀態(tài)甸祭。這些時候缕碎,只要在 調(diào)用dealloc 時判斷下,已經(jīng)調(diào)用過 dispatch_suspend 則再調(diào)用下 dispatch_resume后再cancel池户,然后再釋放timer咏雌。
參考:iOS中如何正確釋放GCD定時器(dispatch_source_t)以及防止Crash?
3. CADisplayLink定時器
CADisplayLink是基于屏幕刷新的周期校焦,所以其一般很準時婶溯,每秒刷新60次粘都。其本質(zhì)也是通過RunLoop安皱,所以當RunLoop選擇其他模式或被耗時操作過多時想虎,仍舊會造成延遲。NSTimer中的循環(huán)引用問題他也存在耸成。
使用步驟:
//創(chuàng)建接受定時器回調(diào)的對象
YYTimerResponse *timeresponse = [[YYTimerResponse alloc] init];
// 創(chuàng)建CADisplayLink
self.displayLink = [CADisplayLink displayLinkWithTarget:timeresponse selector:@selector(timered)];
//設(shè)置定時器周期报亩,這個屬性即將被廢棄, 在iOS10新增了“preferredFramesPerSecond”代替他
// self.displayLink.frameInterval = 60;
self.displayLink.preferredFramesPerSecond = 1;
// 添加至RunLoop中
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
使用“frameInterval”屬性設(shè)置定時器間隔時井氢,因為屏幕一秒鐘刷新60次弦追,所以設(shè)置為60,表示一秒鐘調(diào)用一次花竞。
使用“preferredFramesPerSecond”時劲件,設(shè)置的就是幾秒鐘刷新一次,該屬性iOS10.0后才能用约急。