简介
在iOS中,计时器是比较常用的,用于统计累加数据或者倒计时等,例如手机号获取验证码。计时器大概有那么三种,分别是:NSTimer、CADisplayLink、dispatch_source_t
创建NSTimer方式:有两种
- 第一种:SEL方式
1 2 3 4 5 6 7 8 9
| - (void)demo{ // SEL // 1.系统自动帮我们加入到runloop中 self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES]; // 2.系统不会自动帮我们加入到runloop中,需要手动添加 self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; }
|
- 第二种:Block方式
1 2 3 4 5 6 7 8 9 10 11 12 13
| - (void)demo1{ // Block // 1.系统会自动帮我们加入到runloop中 self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer"); }]; // 2.系统不会自动帮我们加入到runloop中,需要我们手动添加 self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer1"); }]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; }
|
- 由创建方式可知:
NSTimer与NSRunloopMode关系
当NSTimer加入到NSRunloop中的NSDefaultRunLoopMode模式时出现一个问题,就是当页面有UIScrollView滑动执行时执行的模式是UITrackingRunLoopMode,NSDefaultRunLoopMode被挂起了,会导致定时器失效,等滑动结束时才恢复定时器。
需要定时器在 UIScrollView
滑动时也不影响的话,有两种解决方法:
- 给NSTimer分别添加到
UITrackingRunLoopMode
和 NSDefaultRunLoopMode
这两个模式中:
1 2 3 4
| [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; [[NSRunLoop mainRunLoop] addTimer:timer forMode: UITrackingRunLoopMode];
|
- 给NSTimer添加NSRunLoop的NSRunLoopCommonModes中,平常用这中就可以了,比较简便来。
1 2
| [[NSRunLoop mainRunLoop] addTimer:timer forMode: NSRunLoopCommonModes];
|
- NSRunLoopCommonModes相当于UITrackingRunLoopMode 和 NSDefaultRunLoopMode的伪模式
NSTimer与线程的关系
主线程默认开启runloop,子线程默认不开启runloop,需要手动开启runloop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| - (void)demo2{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // scheduledTimerWithTimeInterval self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer"); }]; [[NSRunLoop currentRunLoop] run]; //timerWithTimeInterval self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer"); }]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; }); }
|
子线程需要 [[NSRunLoop currentRunLoop] run] 才能执行NSTimer
NSTimer循环引用
- NSTimer使用不当就会造成内存泄漏,比如常见的使用方法:
1 2 3 4 5 6 7 8 9 10 11 12
| //定义 @property (nonatomic, strong) NSTimer *timer;
//实现 self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(showMsg) userInfo:nil repeats:YES];
// 销毁 -(void)dealloc { [self.timer invalidate]; self.timer = nil; }
|
由于 NSTimer
会引用住 self
,而 self
又持有 NSTimer
对象,所以形成循环引用,dealloc
会被执行,但 timer 也永远不会被释放
,一直在打印,造成内存泄漏
。
尝试解决办法:
1
| @property (nonatomic, weak) NSTimer *timer;
|
虽然 self
对 timer
是 弱引用
,但是控制的 delloc
方法的执行依赖于 timer
的 invalidate
,timer的invalidate又依赖于控制器的delloc方法,依旧是循环引用;
那换个思路能不能让NSTimer弱引用target:
1 2
| __weak typeof(self) weakSelf = self; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(showMsg) userInfo:nil repeats:YES];
|
weak关键字适用于block,当block引用了块外的变量时,会根据修饰变量的关键字来决定是强引用还是弱引用,如果变量使用weak关键字修饰,那block会对变量进行弱引用,如果没有 __weak
关键字,那就是强引用。
但是NSTimer的 scheduledTimerWithTimeInterval:target
方法内部 不会判断修饰target的关键字
,所以这里传 self
和 weakSelf
是没区别的,其内部会对target进行强引用,还是会产生 循环引用
。
采用下面的方法解决循环引用:
1 2 3 4 5 6 7 8
| - (void) viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if (self.timer) { [self.timer invalidate]; self.timer = nil; } }
|
在某些情况下,这种做法是可以解决问题的,但是有时却会引起其他问题,比如控制器push到下一个控制器,viewDidDisappear执行后,timer被释放,此时再pop回来,timer已经不复存在了。
所以,这种 方案
并 不是合理的
。
优化上面的方法这个时候可以采用配对使用在 viewWillAppear
开timer启,在 viewWillDisappear
关闭timer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; if (!self.timer) { self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(showMsg) userInfo:nil repeats:YES]; } }
-(void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; if (self.timer) { [self.timer invalidate]; self.timer = nil; } }
|
上面的方法只是维护起来比较麻烦
- 最终解决办法
NSTimer (TimerBlock) .h 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #import <Foundation/Foundation.h>
@interface NSTimer (TimerBlock)
/** 分类解决NSTimer在使用时造成的循环引用的问题
@param interval 间隔时间 @param block 回调 @param repeats 用于设置定时器是否重复触发
@return 返回NSTimer实体 */ + (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
|
NSTimer+TimerBlock.m 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #import "NSTimer+TimerBlock.h"
@implementation NSTimer (TimerBlock) + (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)reqeats{ return [self timerWithTimeInterval:interval target:self selector:@selector(blockSelector:) userInfo:[block copy] repeats:reqeats]; }
+ (void)blockSelector:(NSTimer *)timer{ void (^block)() = timer.userInfo; if (block) { block(); } } @end
|
上述创建方式调用者是NSTImer自己,只是NSTimer捕获了参数block。这样我们在使用timer时,由于target的改变,就不再有循环引用了。
1 2 3 4
| __weak typeof(self) weakSelf = self; // 避免 block 强引用 self self.timer = [NSTimer block_TimerWithTimeInterval:3 block:^{ // [weakSelf dosomething]; } repeats:YES];
|
- 给
self
添加中间件 Proxy
引入一个对象 Proxy
,Proxy
弱引用 self
,然后 Proxy
传入 NSTimer
。即 self
强引用NSTimer,NSTimer
强引用 Proxy
,Proxy
弱引用 self
,这样通过弱引用来解决了相互引用,此时不会形成环。
即:self 强引用 NSTimer,NSTimer 强引用 Proxy,Proxy 弱引用 self,打破循环
定义一个继承自 NSObject
的中间代理对象 Proxy
,VC
不持有 timer
,而是持有 Proxy
实例,让 Proxy
实例来弱引用 VC
,timer强引用 Proxy
实例,直接看代码:
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
| // ZJProxy.h @interface ZJProxy : NSObject +(instancetype)proxyWithTarget:(id)target; @end
// ZJProxy.m #import "ZJProxy.h"
@interface ZJProxy() @property (nonatomic ,weak) id target; @end
@implementation ZJProxy
+(instancetype)proxyWithTarget:(id)target { FFProxy *proxy = [[FFProxy alloc] init]; proxy.target = target; return proxy; }
// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。 -(id)forwardingTargetForSelector:(SEL)aSelector{ // 快速消息转发,让响应target的还是外部的self return self.target; } @end
|
- forwardingTargetForSelector:(SEL)aSelector是什么?
消息转发
,简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
本文中由于当前target是 ZJProxy
,但是 ZJProxy
没有实现 showMsg
方法(当然也不需要它实现),让系统去找target实例的方法实现,也就是去找ViewController中的方法实现。
- 使用
NSProxy
类
使用 iOS
的 NSProxy
类,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 26 27 28 29 30 31 32
| // ZJWeakProxy.h @interface ZJWeakProxy : NSProxy + (instancetype)proxyWithTarget:(id)target; @end
// ZJWeakProxy.m @interface ZJWeakProxy() @property (nonatomic ,weak)id target; @end @implementation FFWeakProxy + (instancetype)proxyWithTarget:(id)target { // NSProxy实例方法为alloc ZJWeakProxy *proxy = [ZJWeakProxy alloc]; proxy.target = target; return proxy; }
/** 这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行 为给定消息提供参数类型信息 */ - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.target methodSignatureForSelector:sel]; }
/** * NSInvocation封装了NSMethodSignature,通过invokeWithTarget方法将消息转发给其他对象。这里转发给控制器执行。 */ - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.target]; } @end
|
Controller里代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| - (void)viewDidLoad { [super viewDidLoad]; // 这里的target又发生了变化 self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[ZJWeakProxy proxyWithTarget:self] selector:@selector(showMsg) userInfo:nil repeats:YES]; }
// 销毁 -(void)dealloc { [self.timer invalidate]; self.timer = nil; }
|
NSTimer总结:
- 使用
NSTimer
比较方便快捷。
- 创建
NSTimer
必须加入到 Runloop
中才能生效,创建 NSTimer
都是要加到 Runloop
中的,不管是手动添加还是系统添加。
- 当
Timer
加入到 runloop
的模式的 NSDefaultRunLoopMode
,当UIScrollView滑动的时候会暂时失效
- 使用
NSTimer
会存在延时,计时不是很准。因为不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的 RunLoop
和 RunLoop Mode
有关。如果此 RunLoop正在执行一个连续性的运算,timer就会被延时触发
。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。
CADisplayLink
- 创建方式
1 2 3 4
| -(void)createCADisplayLink{ _displaylinkTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)]; [_displaylinkTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; }
|
当把 CADisplayLink
对象 add
到 runloop
中后,selector
就能被周期性调用,类似于重复的 NSTimer
被启动了;执行 invalidate
操作时,CADisplayLink
对象就会从 runloop
中移除,selector
调用也随即停止,类似于 NSTimer
的 invalidate
方法。
- 特性
屏幕刷新时调用 CADisplayLink
是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink
以特定模式注册到 runloop
后,每当屏幕显示内容刷新结束的时候,runloop
就会向 CADisplayLink
指定的 target
发送一次指定的 selector
消息, CADisplayLink
类对应的 selector
就会被调用一次。所以通常情况下,按照iOS设备屏幕的刷新率 60次/秒
延迟iOS设备的屏幕刷新频率是固定的,CADisplayLink
在正常情况下会在每次刷新结束都被调用,精确度相当高
。但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会。如果 CPU过于繁忙
,无法保证屏幕 60次/秒
的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。
- 使用场景
从原理上可以看出,CADisplayLink
适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。
- 重要属性
NSInteger类型的值,用来设置 间隔多少帧
调用一次 selector
方法,默认值是 1
,即每帧都调用一次。
readOnly
的 CFTimeInterval
值,表示 两次屏幕刷新之间的时间间隔
。需要注意的是,该属性在 target
的 selector
被首次调用以后才会被赋值。selector
的调用间隔时间计算方式是:调用间隔时间 = duration × frameInterval
。
dispatch_source_t
- 创建方式
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
| //dispatch_source_t -(void)createDispatch_source_t{ // 创建全局队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 使用全局队列创建计时器 _sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); // 定时器延迟时间 NSTimeInterval delayTime = 1.0f; // 定时器间隔时间 NSTimeInterval timeInterval = 1.0f; // 设置开始时间 dispatch_time_t startDelayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)); // 设置计时器 dispatch_source_set_timer(_sourceTimer,startDelayTime,timeInterval*NSEC_PER_SEC,0.1*NSEC_PER_SEC); // 执行事件 dispatch_source_set_event_handler(_sourceTimer,^{ // 销毁定时器 // dispatch_source_cancel(_sourceTimer); }); // 启动计时器 dispatch_resume(_sourceTimer); }
|
- 特性
默认是重复执行的,可以在事件响应回调中通过 dispatch_source_cancel
方法来设置为只执行一次,如下代码:
1 2 3 4
| dispatch_source_set_event_handler(_timer, ^{ //执行事件 dispatch_source_cancel(_sourceTimer);} );
|
- 优点:
- 时间准确
- 可以使用子线程,解决定时间跑在主线程上卡UI问题
- 需要将
dispatch_source_t timer
设置为 成员变量
,不然会立即释放