前言
您知道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。