美文网首页iOS知识
NSTimer、GCD定时器、CADisplayLink详细分析

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

作者: 阿饼six | 来源:发表于2020-01-13 13:39 被阅读0次

    前言

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

    相关文章

      网友评论

        本文标题:NSTimer、GCD定时器、CADisplayLink详细分析

        本文链接:https://www.haomeiwen.com/subject/xmwgactx.html