NSTimer、GCD定时器、CADisplayLink详细分析

前言

​ 您知道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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容