Hello, World!

iOS中的定时器

字数统计: 1.4k阅读时长: 6 min
2019/02/23 Share

NSTimer

NSTimer是iOS开发当中最常用的定时器。其底层是通过Runloop来实现的,大部分情况下比较准确。但是当前循环耗时操作较多时,会出现延迟问题。同时,也受所加入的RunLoop的Mode影响。

创建之后手动添加到哪个线程的RunLoop中,就运行在哪个线程。

1
2
3
4
5
6
7

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

注意:在即RunLoop的UITrackingMode下,定时器会失效。解决办法即将定时器加到RunLoop的commonModes上即可。

在哪个线程创建就会被自动加入到哪个线程的RunLoop中,就运行在哪个线程。

1
2
3
4
5

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

NSTimer定时器设置了延时之后,如果需要让它立刻执行,需要使用fire方法

1
- (void)fire;

NSTimer定时器的释放一定要先将其终止,而后才能销毁对象

1
2
[timer invalidate];
timer = nil;

对于NSTimer,如何解除循环引用,有一种方式特地介绍一下。

NSProxy是除了NSObject之外的另一个基类,是一个抽象类,只能继承它,重写其消息转发的方法,将消息转发给另一个对象。

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
@interface WeakProxy : NSProxy

- (instancetype)initWithTarget:(id)target;

@end

@interface WeakProxy ()

@property (nonatomic, weak) id target;

@end

@implementation WeakProxy

- (instancetype)initWithTarget:(id)target {
self = [WeakProxy alloc];
self.target = target;
return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}

@end

除了重载消息转发机制的两个方法之外,NSProxy也没有其他功能了。即,使用NSProxy注定是用来转发消息的。

NSProxy可以用来模拟多继承,proxy对象处理多个不同Class对象的消息。
继承自NSProxy的代理类会自动转发消息,而继承自NSObject的则不会,需要自行根据消息转发机制来进行处理。
NSObject的Category中的方法不能转发。

CADisplayLink是基于屏幕刷新的周期,所以其一般很准时,默认每秒刷新60次。其本质也是通过RunLoop,所以不难看出,当RunLoop选择其他模式或耗时操作过多时,仍旧会造成延迟。

1
2
3
4
5
6
7
8
9
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;

- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

- (void)invalidate;

@property(nonatomic) NSInteger preferredFramesPerSecond;

CADisplayLink是以屏幕刷新频率将内容绘制到屏幕上的定时器,适合做UI的不停重绘,动画或视频的渲染等。
一旦CADisplayLink以特定的模式添加到RunLoop中,每当屏幕需要刷新的时候,RunLoop就会调用CADisplayLink绑定的target上的selector方法,则target就可获取CADisplayLink的每次调用的时间戳,用于准备下一帧显示的数据。可用于动画或视频。使用CADisplayLink同样要注意循环引用的问题。

在日常开发中,适当使用CADisplayLink甚至有优化作用。比如对于需要动态计算进度的进度条,由于进度反馈主要是为了UI更新,那么当计算进度的频率超过帧数时,就造成了很多无谓的计算。如果将计算进度的方法绑定到CADisplayLink上来调用,则只在每次屏幕刷新时计算进度,优化了性能。MBProcessHUD则是利用了这一特性。

GCDTimer

GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理,通过系统级调用,更加精准。

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
@implementation viewController {
dispatch_source_t timer;
}

- (void)viewDidLoad {
[super viewDidLoad];

// 创建定时器对象 gcd可以指定队列:即可以在主队列也可以在子队列上执行任务
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));

// 参数: 1 定时器 2 任务开始时间 3任务的间隔 4可接受的误差时间,设置0即不允许出现误差
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0*NSEC_PER_SEC, 0.0*NSEC_PER_SEC);

// 设置定时器任务
dispatch_source_set_event_handler(timer, ^{

});

// 启动任务,GCD计时器创建后需要手动启动
dispatch_resume(timer);
}

- (void)invalidate {
// 暂停
dispatch_suspend(timer);
// 销毁
dispatch_cancel(timer);
timer = nil;
}

@end

GCD Timer的封装

1、用一个字典存储定时器,在取消的时候,根据定时器的key找到相应的定时器
2、多线程会造成字典不安全,对字典读写操作的时候需要加锁

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
97
98
99
100
101
@interface GCDTimer : 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

@implementation GCDTimer

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_);
}

@end

CATALOG
  1. 1. NSTimer
  2. 2. CADisplayLink
  3. 3. GCDTimer