iOS 定时器

NSTimer 简单介绍

1
2
3
4
5
6
7
8
9
// 创建方式一
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES];

// 创建方式二
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES];

// 值得一提的是iOS 10 以后分别为2种方式增加 block 回调,用来处理循环引用问题
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
  • 方式一
    创建定时器后需要手动将定时器添加到runLoop,否则不会执行

    1
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  • 方式二
    会自动将创建的定时器以默认模式添加到runLoop中,当屏幕滚动时runLoop则会进入另外一种模式TrackingRunLoopMode,导致定时器暂停,所以一般我们也需要将其手动添加到NSRunLoopCommonModes

    1
    NSRunLoopCommonModes = NSDefaultRunLoopMode + UITrackingRunLoopMode

对于重复定时器,必须通过调用其invalidate方法来自行使定时器对象无效。调用此方法,将定时器从当前运行循环中删除; 所以,调用invalidate必须和构建定时器时,保持在同一线程中,这样就可以使计时器立即失效会并禁用,就不会再影响运行循环。

精准度

我们都知道NSTimer会有精度问题,但是是怎么造成的呢?

  1. 定时器计算下一个触发时间是根据初始触发时间计算的,下一次触发时间是定时器的整数倍+容差tolerance
  2. 定时器是添加到runloop中的。如果runloop阻塞了,调用或执行方法所花费的时间长于指定的时间间隔(第1点计算得到的时间,就会推迟到下一个runloop周期。
  3. 定时器是不会尝试补偿在调用或执行指定方法时可能发生的任何错过的触发。

内存泄漏

网上也有一些很好的解决方式:通过分类增加block,实现方式跟系统提供的一样
在博客里作者也很好的解释了循环引用问题

  • Runloop对定时源的观察者要进行保留以便时间点到了进行调用,即定时器对象被Runloop强保留着,而定时器内部持有控制器的target,控制器为了方便后续使用,又持有了定时器。这就造成了循环使用。

    虽然系统提供了invalidate来是定时器失效,但是应该在哪里调用这个方法合适呢?delloc?不可能的了!

这里我介绍一种YYKit里面的方法,利用消息转发实现的,这种方式相对方便,也适用在CADisplayLink造成的内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建一个继承自NSProxy的子类
- (instancetype)initWithTarget:(id)target
{
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target
{
return [[ANWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
- (BOOL)respondsToSelector:(SEL)aSelector
{
return [_target respondsToSelector:aSelector];
}
  • 使用:
1
2
3
4
5
6
7
8
9
10
11
@implementation MyView {
NSTimer *_timer;
}

- (void)initTimer {
ANWeakProxy *proxy = [ANWeakProxy proxyWithTarget:self];
_timer = [NSTimer timerWithTimeInterval:1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
}

- (void)tick:(NSTimer *)timer {...}
@end

CADisplayLink 是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类

  • 每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息,CADisplayLink类对应的selector就会被调用一次,所以可以使用CADisplayLink做一些和屏幕操作相关的操作。
  • NSTimer 以指定的模式注册到 RunLoop 后,每当指定的时间到达后,RunLoop 会向指定的 target 发送一次指定的 selector 消息。

同NSTimer一样,也会因为同样的问题造成精度问题。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去,造成界面卡顿的感觉。

  • 创建方式
1
2
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(fire)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

CADisplayLink、NSTimer 都只能添加到一个runloop中,但是可以多模式

内存泄漏

NSTimer 类似,会出现循环引用。
解决方法同NSTimer

1
_link = [CADisplayLink displayLinkWithTarget:[ANWeakProxy proxyWithTarget:self] selector:@selector(fire)];

GCD

dispatch_source_t 的使用

关于GCD定时器探究,这里介绍一篇比较清晰的文章 iOS倒计时的探究与选择

1
2
3
4
5
6
7
8
9
10
11
12
_requestTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));

//设置GCD定时器
if (_requestTimer) {
dispatch_source_set_timer(_requestTimer,dispatch_walltime(NULL, 0), requestTimeMargin * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_requestTimer, ^{
// do something
});
// GCD定时器启动,默认是关闭的
dispatch_resume(_requestTimer);
NSLog(@"定时器开启");
}

NSObject 中的performSelector

这种一次性的定时器相对于 GCD 中的 dispatch_after,使用还是比较少的。

1
2
3
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

与NSTimer scheduledTimerWithTimeInterval 类似,在内部会帮我们创建NSTimer的同时,添加到当前线程的runloop中

如果线程是主线程(程序一启动,就默认开启主线程的runloop),而我们创建的子线程的runloop默认是关闭的,不手动激活,而且基于NSTimer,精度问题也存在!

感谢您的阅读,本文由 Anrue 版权所有。如若转载,请注明出处:Anrue(https://github.com/anru1314/2018/10/09/iOS/2018-10-09-定时器/
五天六晚 -- 云南自由行攻略
自定义转场动画