一、概述
IOS的常用的定时器有NSTimer、CADisplayLink、GCD定时器。
其中NSTimer、CADisplayLink有以下特点:
-
依赖RunLoop,某些方法创建时需要手动把定时器添加到RunLoop中
-
如果是通过target:selector方式创建的,需要注意定时器会对target产生强引用。
-
由于依赖RunLoop,如果某个RunLoop循环中有大量的耗时操作,则定时器的回掉可能不会准确的在定时间隔内触发。
GCD定时器由以下特点:
-
依赖于操作系统内核的,定时精度相对高。
-
不会产生循环引用问题。
二、NSTimer
2.1 使用方式
- 通过NSInvocation封装方法签名的方式创建定时器,需要手动添加定时器到runloop中
- (NSTimer*)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation*)invocation repeats:(BOOL)yesOrNo;
- 通过target:selector方式创建,需要手动添加定时器到runloop中(注意循环引用问题)。
- (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- 指定date时间后自动free,需要手动添加定时器到runloop中
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;
- scheduledTimerWithTimeInterval方法会自动将定时器加到RunLoop的DefaultMode中
- (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
2.2 BlockKit的NSTimer
利用NSTimer类对象来处理循环应用问题,将block利用userInfo进行传递到selector,再进行block回掉:
三、CADisplayLink
CADisplayLink是根据屏幕刷新率来做定时操作的,且依赖于RunLoop,也同时存在循环引用问题。CADisplayLink更多是用于一些动画、刷新率的计算。
3.1 使用方式
- 通过displayLinkWithTarget:selector:方式创建,需要手动添加定时器到runloop中(注意循环引用问题)。
- (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- 添加到RunLoop,除非计时器被停止,否则每次屏幕刷新时,计时器的方法都会被触发。
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
3.2 相关属性
- 时间戳timestamp
用来返回上一次屏幕刷新的时间戳,计算帧数可以通过 1 / (当前的时间戳-记录上一次的时间戳)。
- 预计的下一次屏幕刷新时间戳targetTimestamp
用来返回预计下一次屏幕刷新时间戳,计算fps最好不要用这个属性,这是是预计值,与真实值有差异的.当然如果要求不严的话,也是可以用这个的,好处就是不需要记录上一次的值了,少了一个变量。
- 间隔时间duration
默认是1 / 60,用于提供屏幕最大刷新频率(maximumFramesPerSecond)下每一帧的时间间隔。注意:是最大的 , 不是实时的帧数。
- 修改帧率 preferredFramesPerSecond
如果在特定帧率内无法提供对象的操作,可以通过降低帧率解决.一个拥有持续稳定但是较慢帧率的应用要比跳帧的应用顺滑的多。实际的屏幕帧率会和preferredFramesPerSecond有一定的出入,结果是由设置的值和屏幕最大帧率(maximumFramesPerSecond)相互影响产生的,具体规则如下:
如果屏幕最大帧率(preferredFramesPerSecond)是60,实际帧率只能是15, 20, 30, 60中的一种.如果设置大于60的值,屏幕实际帧率为60.如果设置的是26~35之间的值,实际帧率是30.如果设置为0,会使用最高帧率。
四、dispatch_source_t
GCD定时基于系统内核,定时精度相对上面两种方式会更准确。且GCD定时器不存在对target强引用问题,不需要特别在释放的地方取消定时器。只是需要注意"self.timer = timer",需要保住定时器的命,否则定时不会执行。

4.1 头文件SSTimer.h
//
// SSTimer.h
// IOSzlw
//
// Created by zhouliangwei on 2021/10/14.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SSTimer : NSObject
/// 同下面的方法,不过自动开始执行
+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
/// 创建一个定时器并返回,但是并不会自动执行,需要手动调用resume方法
/// - parameter: start 定时器启动时间
/// - parameter: ti 间隔多久开始执行selector
/// - parameter: s 执行的任务
/// - parameter: ui 绑定信息
/// - parameter: rep 是否重复
- (instancetype)initWithTimeInterval:(NSTimeInterval)start interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;
/// 扩充block
+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(SSTimer *timer))block;
/// 启动
- (void)resume;
/// 暂定
- (void)suspend;
/// 关闭
- (void)invalidate;
@property (readonly) BOOL repeats;
@property (readonly) NSTimeInterval timeInterval;
@property (readonly, getter=isValid) BOOL valid;
@property (nullable, readonly, retain) id userInfo;
@end
NS_ASSUME_NONNULL_END
4.2 实现SSTimer.m
//
// SSTimer.m
// IOSzlw
//
// Created by zhouliangwei on 2021/10/14.
//
#import "SSTimer.h"
#import "SSTimer.h"
#define lock(...) \
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);\
__VA_ARGS__;\
dispatch_semaphore_signal(_semaphore);
@implementation SSTimer {
BOOL _valid;
NSTimeInterval _timeInterval;
BOOL _repeats;
__weak id _target;
SEL _selector;
dispatch_source_t _timer;
dispatch_semaphore_t _semaphore;
id _userInfo;
BOOL _running;
}
+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
SSTimer *timer = [[SSTimer alloc] initWithTimeInterval:0 interval:ti target:aTarget selector:aSelector userInfo:userInfo repeats:yesOrNo];
[timer resume];
return timer;
}
+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(SSTimer *timer))block {
NSParameterAssert(block != nil);
SSTimer *timer = [[SSTimer alloc] initWithTimeInterval:0 interval:interval target:self selector:@selector(ss_executeBlockFromTimer:) userInfo:[block copy] repeats:repeats];
[timer resume];
return timer;
}
- (instancetype)initWithTimeInterval:(NSTimeInterval)start interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep {
self = [super init];
if (self) {
_valid = YES;
_timeInterval = ti;
_repeats = rep;
_target = t;
_selector = s;
_userInfo = ui;
_semaphore = dispatch_semaphore_create(1);
__weak typeof(self) weakSelf = self;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), ti * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{[weakSelf fire];});
}
return self;
}
- (void)fire {
if (!_valid) {return;}
lock(id target = _target;)
if (!target) {
[self invalidate];
} else {
// 执行selector
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:_selector withObject:self];
#pragma clang diagnostic pop
if (!_repeats) {
[self invalidate];
}
}
}
- (void)resume {
if (_running) return;
dispatch_resume(_timer);
_running = YES;
}
- (void)suspend {
if (!_running) return;
dispatch_suspend(_timer);
_running = NO;
}
- (void)invalidate {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (_valid) {
dispatch_source_cancel(_timer);
_timer = NULL;
_target = nil;
_userInfo = nil;
_valid = NO;
}
dispatch_semaphore_signal(_semaphore);
}
- (id)userInfo {
lock(id ui = _userInfo) return ui;
}
- (BOOL)repeats {
lock(BOOL re = _repeats) return re;
}
- (NSTimeInterval)timeInterval {
lock(NSTimeInterval ti = _timeInterval) return ti;
}
- (BOOL)isValid {
lock(BOOL va = _valid) return va;
}
- (void)dealloc {
[self invalidate];
}
+ (void)ss_executeBlockFromTimer:(SSTimer *)aTimer {
void (^block)(SSTimer *) = [aTimer userInfo];
if (block) block(aTimer);
}
@end