定义

  • 一个定时器对象,它可以让你与屏幕刷新频率相同的速率来刷新你的视图。就说CADisplayLink是用于同步屏幕刷新频率的计时器。
  • 在应用中创建一个新的CADisplayLink对象,把它添加到一个runloop中,并给它提供一个target和selector在屏幕刷新的时候调用。

API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//初始化
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;

//启动
//初始化后,要将其添加到 runLoop 中才能执行
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode //添加到runLoop
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode //从runLoop中移除

//暂停和继续
[link setPaused:YES];
[link setPaused:NO];

//停止(失效)
[link invalidate];
link = nil;

NSTimer

定义

  • OC中的计时器(时钟),可每隔一段时间执行一次。

API

1
2
3
4
5
//初始化
//需要手动将定时器添加到runLoop中
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
//系统会自动将其添加到 runLoop 中
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

区别

原理不同

  • CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink以特定模式注册到runloop后,每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。
  • NSTimer以指定的模式注册到runloop后,每当设定的周期时间到达后,runloop会向指定的target发送一次指定的selector消息。

周期设置不同

  • iOS设备的屏幕刷新频率(FPS)是60Hz,因此CADisplayLink的selector默认调用周期是每秒60次,这个周期可以通过frameInterval属性设置,CADisplayLink的selector每秒调用次数=60/frameInterval。比如当frameInterval设为2,每秒调用就变成30次。因此,CADisplayLink周期的设置方式略显不便。
  • NSTimer的selector调用周期可以在初始化时直接设定,相对就灵活的多。

精度不同

  • iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
  • NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。

使用场景

  • CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。
  • NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface ViewController ()
@property (strong, nonatomic) CADisplayLink *link;
@property (strong, nonatomic) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// 保证调用频率和屏幕的刷帧频率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

//block方式使用timer
// __weak typeof(self) weakSelf = self;
// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
// [weakSelf timerTest];
// }];
}

- (void)timerTest {
NSLog(@"%s", __func__);
}

- (void)linkTest {
NSLog(@"%s", __func__);
}

- (void)dealloc {
NSLog(@"%s", __func__);
[self.link invalidate];
// [self.timer invalidate];
}

循环引用

原因

  • self对CADisplayLink强引用,CADisplayLink对target强引用,target对self强引用,造成循环引用
  • NSTimer也有类似问题,NSTimer的scheduledTimerWithTimeInterval创建方式会造成循环引用

解决方案

  • _ _weak, 使用block 弱引用均无法解决
  • 使用代理对象

代理对象,继承NSObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface HXProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;//弱引用
@end

@implementation HXProxy1
+ (instancetype)proxyWithTarget:(id)target {
HXProxy1 *proxy = [[HXProxy1 alloc] init];
proxy.target = target;
return proxy;
}

//消息转发三种
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}

代理对象,继承NSProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@interface HXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation HXProxy

+ (instancetype)proxyWithTarget:(id)target {
// NSProxy对象不需要调用init,因为它本来就没有init方法
HXProxy *proxy = [HXProxy alloc];
proxy.target = target;
return proxy;
}

//消息转发
//只要调用HXProxy的某个方法就马上调用他的另一个方法methodSignatureForSelector
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

//消息转发三种 调用不到时先调用这个
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
//- (id)forwardingTargetForSelector:(SEL)aSelector NSProxy没有这个方法

GCD定时器执行任务封装

  • NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时
  • GCD的定时器会更加准时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//.h
@interface HXTimer : NSObject

+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;

+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;

+ (void)cancelTask:(NSString *)name;

@end

//.m
#import "HXTimer.h"
@implementation HXTimer

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_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();

// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, 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:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore_);

// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();

if (!repeats) { // 不重复的任务
[self cancelTask:name];
}
});

// 启动定时器
dispatch_resume(timer);

return name;
}

+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async {
if (!target || !selector) return nil;

return [self execTask:^{
if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
} start:start interval:interval repeats:repeats async:async];
}

+ (void)cancelTask:(NSString *)name {
if (name.length == 0) return;

dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);

dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}

dispatch_semaphore_signal(semaphore_);
}

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSString *task = [HXTimer execTask:self
selector:@selector(doTask)
start:2.0
interval:1.0
repeats:YES
async:NO];

// NSString *task = [HXTimer execTask:^{
// NSLog(@"111111 - %@", [NSThread currentThread]);
// } start:2.0 interval:-10 repeats:NO async:NO];

- (void)doTask {
NSLog(@"doTask - %@", [NSThread currentThread]);
}

参考