iOS中定时器有三种,分别是NSTimer、CADisplayLink、dispatch_source,下面就分别对这三种计时器进行说明
一、NSTimer
1.创建
/**
* TimerInterval: 执行之前等待的时间。比如设置成1.0,就代表1秒后执行方法,
* target: 需要执行方法的对象。
* selector : 需要执行的方法
* repeats : 是否需要循环
*/
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:NO];
2.释放
[timer invalidate];
timer = nil;
3.特性
存在延迟
不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时触发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。
必须加入Runloop
使用上面的创建方式,会自动把timer加入MainRunloop的NSDefaultRunLoopMode中。如果使用以下方式创建定时器,就必须手动加入Runloop:
NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
二、CADisplayLink
1.创建方法
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
2.释放方法
[self.displayLink invalidate];
self.displayLink = nil;
当把CADisplayLink对象add到runloop中后,selector就能被周期性调用,类似于重复的NSTimer被启动了;执行invalidate操作时,CADisplayLink对象就会从runloop中移除,selector调用也随即停止,类似于NSTimer的invalidate方法。
3.特性
屏幕刷新时调用
CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink以特定模式注册到runloop后,每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。所以通常情况下,按照iOS设备屏幕的刷新率60次/秒
延迟
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会。
如果CPU过于繁忙,无法保证屏幕60次/秒的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。
4.使用场景
从原理上可以看出,CADisplayLink适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。
5.重要属性
frameInterval
NSInteger类型的值,用来设置间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。
duration
readOnly的CFTimeInterval值,表示两次屏幕刷新之间的时间间隔。需要注意的是,该属性在target的selector被首次调用以后才会被赋值。selector的调用间隔时间计算方式是:调用间隔时间 = duration × frameInterval。
三、dispatch_source_t
1.创建方法
//需要将dispatch_source_t timer设置为成员变量,不然会立即释放
@property (nonatomic, strong) dispatch_source_t timer;
//定时器开始执行的延时时间
NSTimeInterval delayTime = 3.0f;
//定时器间隔时间
NSTimeInterval timeInterval = 3.0f;
//创建子线程队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//使用之前创建的队列来创建计时器
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置延时执行时间,delayTime为要延时的秒数
dispatch_time_t startDelayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC));
//设置计时器
dispatch_source_set_timer(_timer, startDelayTime, timeInterval * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
//执行事件
});
// 启动计时器
dispatch_resume(_timer);
2.注销方法
dispatch_source_cancel(_timer);
3.特性
默认是重复执行的,可以在事件响应回调中通过dispatch_source_cancel方法来设置为只执行一次,如下代码:
dispatch_source_set_event_handler(_timer, ^{
//执行事件
dispatch_source_cancel(_timer);
});
4.重要属性
/**
* source 分派源
* start 控制计时器触发的时刻。参数类型是 dispatch_time_t,这是一个opaque类型,我们不能直接操作它。我们得需要 dispatch_time 和 dispatch_walltime 函数来创建它们。另外,常量 DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER 通常很有用。
* interval 间隔时间
* leeway 计时器触发的精准程度
*/
dispatch_source_set_timer(dispatch_source_t source,
dispatch_time_t start,
uint64_t interval,
uint64_t leeway);
5.优点:
时间准确:因为dispatch_source不依赖于Runloop,而是直接和底层内核交互,准确性更高。
可以使用子线程,解决定时间跑在主线程上卡UI问题。
6.注意事项:
需要将dispatch_source_t timer设置为成员变量,不然会立即释放。
四、总结
1.实现原理
CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。 CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向 CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。
NSTimer以指定的模式注册到runloop后,每当设定的周期时间到达后,runloop会向指定的target发送一次指定的selector消息。
dispatch_source直接和底层内核交互,准确性更高。
2.精确度
dispatch_source > CADisplayLink > NSTimer
3.常见应用
CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。
NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。
dispatch_source对计时精确性要求比较高应用。
总结:使用方法三进行定时器最为精准,没有因为线程卡顿的原因计时不准确。
实际应用
1.解决NSTimer的循环引用
1.1 如果NSTimer使用block
__weak __typeof(&self)weakSelf = self;
1.2不使用block创建NSTimer
1.2.1使用NSObject
@interface MiddleObject : NSObject
@property(nonatomic, weak) id target;
+ (instancetype)objectWithTarget:(id)target;
@end
#import "MiddleObject.h"
@implementation MiddleObject
+ (instancetype)proxyWithTarget:(id)target{
MiddleObject *object = [MiddleObject alloc];
object.target = target;
return object;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [self.target forwardingTargetForSelector:aSelector];
}
@end
1.2.2使用NSProxy(建议使用这个方法解决效率高-专门用做处理消息转发)
@interface SPProxy : NSProxy
@property(nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
#import "SPProxy.h"
@implementation SPProxy
+ (instancetype)proxyWithTarget:(id)target{
SPProxy *proxy = [SPProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
使用:
- (void)startTimer
{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[SPProxy proxyWithTarget:self] selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%@-%s", [self class], __func__);
}
- dispatch_source_t封装
@interface SPTimer : NSObject
+ (NSString *)execTask:(void(^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async;
+ (void)cancelTask:(NSString *)timerId;
@end
#import "SPTimer.h"
@implementation SPTimer
static NSMutableDictionary *timers;
dispatch_semaphore_t semaphore;
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers = [NSMutableDictionary dictionary];
semaphore = dispatch_semaphore_create(1);
});
}
+ (NSString *)execTask:(void(^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async {
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
//创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, async ? dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL) : dispatch_get_main_queue());
//设置时间
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//唯一标识
NSString *name = [NSString stringWithFormat:@"%lf", [[NSDate now] timeIntervalSince1970]*1000];
//存到字典
timers[name] = timer;
dispatch_semaphore_signal(semaphore);
//设置回调
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) {
[self cancelTask:name];
}
});
//启动定时器
dispatch_resume(timer);
return name;
}
+ (void)cancelTask:(NSString *)timerId {
if (!timerId.length || !timers[timerId]) return;
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_source_cancel(timers[timerId]);
[timers removeObjectForKey:timerId];
dispatch_semaphore_signal(semaphore);
}
@end
使用
[SPTimer execTask:^{
NSLog(@"logicEDU-%@", [NSThread currentThread]);
} start:0 interval:1 repeats:YES async:NO];