前言
? 您知道NSTimer是否一定需要手動調用invalidate方法?如何避免NSTimer的內存泄漏問題?NSTimer準時嗎?為什么大家都說GCD定時器比NSTimer時間更精確,這句話絕對正確嗎?NSTimer如果遇到延遲調用,會疊加觸發嗎?CADisplayLink又是干什么的呢?本文就帶著這些問題進行詳細的一一解答。
一、NSTimer
1、概念
? 定時器,在一段確定的時間后定時器開始工作,向target目標發送指定的消息(調用對應方法)。
2、NSTimer與target關系
? NSTimer會強引用target,直到timer調用invalidate()方法。
開發者要不要手動調用invalidate()方法,分為兩種情況:
1)如果repeats為NO,則不需要手動調用invalidate:當定時器執行的時候一直是強引用target,當定時器執行一次結束后,系統自動調用invalidate方法,從而解除強引用。也就是說repeats為NO時,不會發生循環引用。驗證代碼如下:
// 詳情頁,從上個界面跳轉過來
@interface DetailViewController ()
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation DetailViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
// repeats為NO時,系統會自動執行invalidate,且不會發生循環引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:NO];
NSLog(@"定時器開始工作");
}
- (void)dealloc {
NSLog(@"DetailViewController dealloc");
}
- (void)timerAction:(NSTimer *)timer {
self.num ++;
NSLog(@"num = %ld", self.num);
}
@end
操作步驟:從ViewController跳轉到DetailViewController,然后馬上點擊返回按鈕。
運行結果:當定時器工作時,點擊返回按鈕后,DetailViewController并沒有馬上釋放,而是5s之后才釋放。
2020-01-10 09:44:20.567568+0800 OCTest[17155:820752] 定時器開始工作
2020-01-10 09:44:25.568842+0800 OCTest[17155:820752] num = 1
2020-01-10 09:44:25.569196+0800 OCTest[17155:820752] DetailViewController dealloc
2)如果repeats為YES,則需要手動調用invalidate:那在什么時候調用invalidate呢?在DetailViewController的dealloc方法里嗎?代碼驗證下:
// repeats為YES
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
操作步驟:大體代碼都和上面類似,只修改生成timer的代碼。從ViewController進入DetailViewController,過段時間再點擊返回按鈕。
運行結果:定時器一直在運行,點擊返回按鈕后,DetailViewController沒有走dealloc方法,也就是沒有釋放。
定時器開始工作
num = 1
num = 2
num = 3
...
此時就會發生內存泄漏,這里簡單說明下原因:NavigationController強持有DetailViewController,DetailViewController強持有timer(這里無論強持有或者弱持有都一樣),timer強持有DetailViewController,RunLoop強持有timer;點擊返回按鈕后,總有RunLoop持有timer,timer持有DetailViewController,所以就會發生內存泄漏。更多細節,請查看另一篇博客-iOS 內存管理。
這里使用NSProxy進行消息轉發,解決內存泄漏問題:
創建一個TimerProxy類:
// .h文件
@interface TimerProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
// .m文件
@interface TimerProxy ()
@property(nonatomic, weak) id target;
@end
@implementation TimerProxy
+ (instancetype)proxyWithTarget:(id)target {
TimerProxy *proxy = [TimerProxy alloc]; //注意:沒有init方法
proxy.target = target;
return proxy;
}
// NSProxy接收到消息會自動進入到調用這個方法 進入消息轉發流程
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
那么DetailViewController的代碼是:
@interface DetailViewController ()
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation DetailViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];
NSLog(@"定時器開始工作");
}
- (void)dealloc {
NSLog(@"DetailViewController dealloc");
[self.timer invalidate];
}
- (void)timerAction:(NSTimer *)timer {
self.num ++;
NSLog(@"num = %ld", self.num);
}
@end
操作步驟和上面類似,
運行結果:DetailViewController會被釋放,此時在dealloc方法里調用timer的invalidate方法是合適的。
定時器開始工作
num = 1
num = 2
num = 3
DetailViewController dealloc
3、NSTimer需要添加到RunLoop中
? 創建NSTimer一般有兩種方法,一種直接創建使用,一種需要手動添加到RunLoop中。
1)直接創建使用:通過scheduledTimer創建一個定時器,系統默認把timer添加到當前的RunLoop中,模式是NSDefaultRunLoopMode。
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];
2)手動添加到RunLoop中:通過timerWithTimeInterval創建一個定時器,需要手動把timer添加到RunLoop中,并指定RunLoop的Mode。
self.timer = [NSTimer timerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
這里順便說明invalidate方法的兩個作用:
- 停止定時器
- 把定時器從RunLoop中移除,并把定時器對target的強引用移除
至于RunLoop各種Mode怎么使用,請看-iOS RunLoop。
4、NSTimer準時嗎
? 答案是否定的。因為NSTimer需要添加到RunLoop中,那么必然會受到RunLoop的影響,具體原因有兩個:
- 受RunLoop循環處理的時間影響
- 受RunLoop模式的影響
驗證時間影響:在ViewDidLoad中添加一個5s之后延時方法,并執行休眠5s(模擬RunLoop處理繁重任務)
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];
NSLog(@"定時器開始工作");
// 執行繁重任務
[self performSelector:@selector(performDelay) withObject:nil afterDelay:5.0];
}
- (void)performDelay {
NSLog(@"Begin delay");
sleep(5);
NSLog(@"End delay");
}
運行結果:timer執行兩個周期之后1秒,開始執行繁重任務,5秒后繁重任務結束,重新開始執行定時器任務。注意:處理繁重任務中,定時器任務并沒有執行,也就是存在延時;處理繁重任務之后,timer并沒有連著觸發多次消息,而只是觸發了一次,并且執行完繁重的任務之后的觸發是正常的。也就是說NSTimer遇到RunLoop有繁重的任務會進行延遲,如果延遲時間超過一個周期,不會疊加在一起運行,即在一個周期內只會觸發一次,并且后面的timer的觸發時間總是倍數于第一次添加timer的間隙。
2020-01-10 11:39:17.899898+0800 OCTest[18665:918402] 定時器開始工作
2020-01-10 11:39:19.901209+0800 OCTest[18665:918402] num = 1
2020-01-10 11:39:21.901262+0800 OCTest[18665:918402] num = 2
2020-01-10 11:39:22.901337+0800 OCTest[18665:918402] Begin delay
2020-01-10 11:39:27.902903+0800 OCTest[18665:918402] End delay
2020-01-10 11:39:27.903364+0800 OCTest[18665:918402] num = 3
2020-01-10 11:39:29.900309+0800 OCTest[18665:918402] num = 4
驗證模式影響:在ViewDidLoad中添加一個UITableView,當手指一直拽著tableView時,如果timer的Mode是NSDefaultRunLoopMode,那么定時器任務不會觸發。
創建UITableView的代碼省略,
直接顯示運行結果:拖拽著tableView,當前RunLoop模式由NSDefaultRunLoopMode切換到UITrackingRunLoopMode,此時不會觸發定時器消息;當拖拽結束并滾動完成減速后,35.17s觸發了33.57s本應該觸發的消息,然后接著觸發35.57s要觸發的消息,所以這里連續觸發了兩次。
2020-01-10 13:24:25.573946+0800 OCTest[19514:973998] 定時器開始工作
2020-01-10 13:24:27.575234+0800 OCTest[19514:973998] num = 1
2020-01-10 13:24:29.575002+0800 OCTest[19514:973998] num = 2
// 開始拽著tableView
2020-01-10 13:24:30.658728+0800 OCTest[19514:973998] scrollViewWillBeginDragging
// 停止拖拽tableView
2020-01-10 13:24:34.635138+0800 OCTest[19514:973998] scrollViewDidEndDragging
// tableView完成減速
2020-01-10 13:24:35.169677+0800 OCTest[19514:973998] scrollViewDidEndDecelerating
// 13:24:35.17:這個時間周期是33s應該觸發的,但是被延遲了
2020-01-10 13:24:35.170381+0800 OCTest[19514:973998] num = 3
// 13:24:35.57:35s時間觸發
2020-01-10 13:24:35.575338+0800 OCTest[19514:973998] num = 4
2020-01-10 13:24:37.575340+0800 OCTest[19514:973998] num = 5
綜上所述,NSTimer時間會被RunLoop處理時間和RunLoop模式切換影響。當然,如果把timer定時器Mode改為NSRunLoopCommonModes,那么就不會受模式切換影響,但仍然受RunLoop處理時間影響。
5、NSTimer如何在子線程運行
? NSTimer雖然能在子線程運行,但是處理起來較為麻煩。先要創建線程,啟動線程,然后創建定時器,把定時器添加到當前的RunLoop中,最后運行RunLoop,還要注意內存泄漏問題,銷毀線程問題等。
直接上代碼:
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) NSTimer *timer;
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
// thread會強引用self,直到線程結束
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
[thread start];
}
- (void)dealloc {
NSLog(@"DetailViewController dealloc");
}
- (void)startThread {
NSLog(@"thread = %@", [NSThread currentThread]);
// 創建timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];
// 把timer添加到當前子線程的RunLoop
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
/*運行當前RunLoop,代碼運行于此,將不再執行下去,整個線程處于活躍。
當線程中不再有需要執行的事件時,再會放開事件循環,代碼繼續執行下去。
*/
[[NSRunLoop currentRunLoop] run];
}
- (void)timerAction:(NSTimer *)timer {
NSLog(@"num = %ld, thread = %@", self.num ++, [NSThread currentThread]);
if (self.num > 3) {
[self.timer invalidate]; //需要在dealloc之前調用invalidate
}
}
運行結果:DetailViewController完美釋放。
thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
num = 0, thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
num = 1, thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
num = 2, thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
num = 3, thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
TimerProxy dealloc
DetailViewController dealloc
必須在dealloc之前手動調用invalidate,才能避免內存泄漏。過程詳解:調用invalidate之后,子線程RunLoop移除timer,RunLoop沒有任何事件源,RunLoop結束,從而當前子線程結束,移除對self的強引用,點擊返回按鈕,會執行dealloc方法。
二、GCD定時器
? GCD定時器創建時不需要指定RunLoop的Mode,自然不受RunLoop模式切換的影響,但如果把GCD定時器放在主線程運行,仍然會受到RunLoop循環處理時間的影響。至于遇到繁重任務的情況,和NSTimer情況類似。GCD定時器如果在主線程運行,遇到MainRunLoop有繁重的任務會進行延遲,如果延遲時間超過一個周期,不會疊加在一起運行,即在一個周期內只會觸發一次,并且后面的timer的觸發時間總是倍數于第一次添加timer的間隙。
@interface DetailViewController ()
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) dispatch_source_t timer;
@end
@implementation DetailViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
// 創建一個定時器
dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
self.timer = sourceTimer; //持有
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(2.0 * NSEC_PER_SEC);
dispatch_source_set_timer(sourceTimer, start, interval, 0);
// 設置回調
__weak typeof(self) wself = self;
dispatch_source_set_event_handler(sourceTimer, ^{
NSLog(@"num = %ld", wself.num ++); //注意:需要使用weakSelf,不然會內存泄漏
});
// 啟動定時器
dispatch_resume(sourceTimer);
NSLog(@"定時器開始工作");
}
- (void)dealloc {
NSLog(@"DetailViewController dealloc");
// 如果前面block回調使用了weakSelf,那么cancel可以寫在這里
dispatch_source_cancel(self.timer);
}
@end
操作步驟:從ViewController進入DetailViewController,定時器運行,當num=2時點擊返回按鈕。
運行結果:DetailViewController立刻釋放。
2020-01-10 14:57:44.440598+0800 OCTest[20989:1098517] 定時器開始工作
2020-01-10 14:57:46.441211+0800 OCTest[20989:1098517] num = 0
2020-01-10 14:57:48.441782+0800 OCTest[20989:1098517] num = 1
2020-01-10 14:57:50.441348+0800 OCTest[20989:1098517] num = 2
2020-01-10 14:57:51.347448+0800 OCTest[20989:1098517] DetailViewController dealloc
或者在event_handler回調中主動調用dispatch_source_cancel,這樣取消定時器后也能避免內存泄漏。
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"num = %ld", self.num ++);
if (self.num > 5) {
dispatch_source_cancel(self.timer);
}
});
操作步驟:從ViewController進入DetailViewController,定時器運行,當num=2時點擊返回按鈕。
運行結果:點擊返回按鈕后,DetailViewController并沒有馬上釋放,定時器的block一直運行,直到num>5時調用dispatch_source_cancel后,DetailViewController才進行釋放。
2020-01-10 15:11:05.283164+0800 OCTest[21284:1119575] 定時器開始工作
2020-01-10 15:11:07.283189+0800 OCTest[21284:1119575] num = 0
2020-01-10 15:11:09.283260+0800 OCTest[21284:1119575] num = 1
2020-01-10 15:11:11.283546+0800 OCTest[21284:1119575] num = 2
2020-01-10 15:11:13.284427+0800 OCTest[21284:1119575] num = 3
2020-01-10 15:11:15.284450+0800 OCTest[21284:1119575] num = 4
2020-01-10 15:11:17.283821+0800 OCTest[21284:1119575] num = 5
2020-01-10 15:11:17.284562+0800 OCTest[21284:1119575] DetailViewController dealloc
當然,GCD定時器也能在子線程運行,不用添加到RunLoop中。
// 在global_queue上運行timer
dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
運行結果:event_handler在多個線程進行回調處理。
定時器開始工作
num = 0, thread = <NSThread: 0x60000211fcc0>{number = 5, name = (null)}
num = 1, thread = <NSThread: 0x60000216ef00>{number = 4, name = (null)}
num = 2, thread = <NSThread: 0x60000216ef00>{number = 4, name = (null)}
DetailViewController dealloc
注意:如果GCD定時器在子線程運行,主線程RunLoop即使有繁重的任務,也會準時觸發。
代碼如下:
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) dispatch_source_t timer;
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
self.timer = sourceTimer;
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(2.0 * NSEC_PER_SEC);
dispatch_source_set_timer(sourceTimer, start, interval, 0);
__weak typeof(self) wself = self;
dispatch_source_set_event_handler(sourceTimer, ^{
NSLog(@"num = %ld, thread = %@", wself.num ++, [NSThread currentThread]);
/*
//如果在block里有繁重的任務處理,GCD定時器也會延時
if (wself.num == 3) {
[wself performDelay];
}
*/
});
dispatch_resume(sourceTimer);
NSLog(@"定時器開始工作");
// 模擬在主線程有繁重任務
[self performSelector:@selector(performDelay) withObject:nil afterDelay:5.0];
}
- (void)performDelay {
NSLog(@"Begin delay");
sleep(4);
NSLog(@"End delay");
}
運行結果:GCD定時器在子線程運行,不會受到主線程RunLoop執行時間的影響。
2020-01-13 09:47:26.236410+0800 OCTest[24444:1339137] 定時器開始工作
2020-01-13 09:47:28.236864+0800 OCTest[24444:1339777] num = 0, thread = <NSThread: 0x600000d8bcc0>{number = 9, name = (null)}
2020-01-13 09:47:30.237588+0800 OCTest[24444:1339415] num = 1, thread = <NSThread: 0x600000d4a340>{number = 10, name = (null)}
2020-01-13 09:47:31.237452+0800 OCTest[24444:1339137] Begin delay
2020-01-13 09:47:32.237753+0800 OCTest[24444:1339777] num = 2, thread = <NSThread: 0x600000d8bcc0>{number = 9, name = (null)}
2020-01-13 09:47:34.237759+0800 OCTest[24444:1339777] num = 3, thread = <NSThread: 0x600000d8bcc0>{number = 9, name = (null)}
2020-01-13 09:47:35.238945+0800 OCTest[24444:1339137] End delay
2020-01-13 09:47:36.237684+0800 OCTest[24444:1339415] num = 4, thread = <NSThread: 0x600000d4a340>{number = 10, name = (null)}
2020-01-13 09:47:38.237792+0800 OCTest[24444:1339777] num = 5, thread = <NSThread: 0x600000d8bcc0>{number = 9, name = (null)}
總結:如果GCD定時器在主線程運行,那么會受到RunLoop運行時間影響,即和NSTimer類似,時間可能不會很精確;如果GCD定時器在子線程運行,那么不會受到MainRunLoop的影響,時間就很精確。當然,如果GCD的block回調處理繁重任務,時間也會進行相應的延時。
三、CADisplayLink
1、概念
? CADisplayLink是一個執行頻率(fps)和屏幕刷新相同的定時器(可以修改preferredFramesPerSecond屬性來修改具體執行的頻率)。時間精度比NSTimer高,但是也要添加到RunLoop里。通常情況下CADisaplayLink用于構建幀動畫,看起來相對更加流暢,而NSTimer則有更廣泛的用處。
2、基本使用
? CADisplayLink和NSTimer類似,會容易造成循環引用問題,所以還是需要一個中間類TimerProxy來解決內存泄漏問題。如果設置RunLoop的模式是NSDefaultRunLoopMode,那么也會受到RunLoop模式切換的影響。在Dealloc方法里必須調用invalidate方法釋放定時器。
代碼如下:
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) CADisplayLink *timer;
- (void)viewDidLoad {
[super viewDidLoad];
self.num = 0;
CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:)];
self.timer = timer;
if (@available(iOS 10.0, *)) {
timer.preferredFramesPerSecond = 30; //30幀
} else {
timer.frameInterval = 2; //屏幕刷新60幀,每2幀刷一次,就是每秒30幀頻率
}
/**
添加到當前的RunLoop
NSDefaultRunLoopMode:默認模式,會受到RunLoop模式切換的影響
NSRunLoopCommonModes:不會受RunLoop模式切換的影響
*/
[timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)dealloc {
NSLog(@"DetailViewController dealloc");
[self.timer invalidate];
}
- (void)timerAction:(CADisplayLink *)timer {
NSLog(@"num = %ld", self.num ++);
}
3、制作FPS工具
? 根據CADisplayLink是一個執行頻率(fps)和屏幕刷新相同的定時器原理,可以制作一個FPS檢測器。具體代碼如下:
#define kFPSLabelSize CGSizeMake(55, 20)
// .h
@interface FPSLabel : UILabel
@end
// .m
@implementation FPSLabel {
CADisplayLink *_link;
NSUInteger _count;
NSTimeInterval _lastTime;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (!self) { return nil; }
self.layer.cornerRadius = 5;
self.clipsToBounds = YES;
self.textAlignment = NSTextAlignmentCenter;
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
_link = [CADisplayLink displayLinkWithTarget:[TimerProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
return self;
}
- (void)dealloc {
[_link invalidate];
}
- (CGSize)sizeThatFits:(CGSize)size {
return kFPSLabelSize;
}
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return; // 計算一秒的次數
_lastTime = link.timestamp;
float fps = _count / delta;
_count = 0; // 重新計數
CGFloat progress = fps / 60.0;
// 根據色調,飽和度,亮度生成顏色
UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
[text addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, text.length-3)];
[text addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(text.length-3, 3)];
[text addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:14] range:NSMakeRange(0, text.length)];
self.attributedText = text;
}
@end
使用FPSLabel:
_fpsLabel = [FPSLabel new];
CGRect frame = self.view.frame;
_fpsLabel.frame = CGRectMake(15, frame.size.height-15-kFPSLabelSize.height, kFPSLabelSize.width, kFPSLabelSize.height);
[self.view addSubview:_fpsLabel];
三個定時器最終總結:
- NSTimer:使用頻繁,一般在主線程中運行,添加到主RunLoop中;受到RunLoop模式切換影響和RunLoop運行時間影響;使用時,注意內存泄漏問題、RunLoop模式切換問題、調用invalidate方法時機問題等。
- GCD定時器:使用頻繁,不需要主動添加到RunLoop中,不受到模式切換的影響;如果GCD定時器在主線程運行,那么還是會受到主RunLoop運行時間的影響;如果GCD定時器在子線程運行,那么不會受到主RunLoop的影響,所以這個場景下,時間精確度比NSTimer要高。使用時,需要注意內存泄漏問題、dispatch_source_cancel調用時機問題等。
- CADisplayLink:使用較少,一般使用在與幀動畫有關的場景,保持和屏幕幀率一致的定時器,也可以制作FPS檢測工具。使用時,也要注意內存泄漏問題、RunLoop模式切換問題、調用invalidate方法時機問題等。
如果對RunLoop感興趣,請查看-iOS RunLoop。